Imported from rheged-studio/markdownlint-config (
.agents/skills/send-it/SKILL.md). Install upstream withnpx skills add rheged-studio/markdownlint-config --skill send-it. Copyright stays with the author (MIT).
send-it
Bundle uncommitted work into atomic commits (via the
commit skill), run the change-gated lint
preflight, author or update the dated
changelog/<ts>-<slug>.md entry (via the changelog
skill), compose a Conventional Commits PR title (CI + humans; under the dual
merge policy, feature PRs land as merge commits and release-please ranks the
landed commit subjects for the bump — A-1176 / A-824), push the branch, open
or update a pull request against the base branch, and transition any linked Linear
issues to In Review (via the linear-sync skill).
This skill is the single source of truth for the ship flow. It is a thin orchestrator: it owns only the glue no sibling skill does — the branch guard, worktree resolution, the release-type decision (by category), PR-title composition, push, and the PR — and delegates the rest:
- Commit → the
commitskill (classify in-scope vs out-of-scope, atomic Conventional Commits, out-of-scope guard). - Lint gate → the
preflightskill (change-gated; no-ops when nothing lint-relevant changed). - Changelog → the
changelogskill (author/update + validate; an entry for every PR, skipped entirely only whenconfig.jsonsetschangelog: false). - Linear In Review → the
linear-syncskill (resolve state by team name, idempotent transition). - Post-PR triage → the
triage-prskill (Phase A CI fix loop and the promote-on-proven-green flip, then Phase B review dispositions up to its human envelope — Step 11, A-1151).
The delegated skills auto-detect their own scope, so monorepo features
(per-workspace ESLint fan-out, changelog affected_packages) no-op cleanly in a
single-package repo. send-it configures nothing about them.
Install the delegated skills alongside
send-it. This bundle invokes and links its siblings by relative path (../commit/SKILL.md,../preflight/SKILL.md,../changelog/SKILL.md,../linear-sync/SKILL.md,../triage-pr/SKILL.md), so a--skill send-it-only install leaves the commit, lint, changelog, Linear, and triage steps unavailable and those links dangling. Install them together:npx skills add https://github.com/rheged-studio/agent-skills \ --skill send-it --skill commit --skill preflight --skill changelog --skill linear-sync \ --skill triage-pr \ --agent claude-code --agent cursor --copy
This flow intentionally does not run typecheck, tests, or format checks — CI
handles those. The only gate it runs is the change-gated preflight lint.
Configuration
A few knobs live in config.json beside this file; edit your
copied config.json to match the consuming repo (a neutral
config.example.json ships as a template):
| Key | Meaning | Default |
|---|---|---|
baseBranch |
The trunk the branch diff is taken against (origin/<baseBranch>) and the PR base. |
"main" |
shippablePaths (advisory) |
Path prefixes that make up the published surface — a documentation hint for reviewers, not the release decision (A-598; see Step 6). Release-type is decided by the change's semantic category, so these no longer gate the title. Kept for the optional publish-surface cross-check note. | ["skills/"] |
shippableManifestKeys (advisory) |
package.json keys that form the published-files surface — same advisory role as shippablePaths, no longer a release gate. |
["name", "version", "files", "publishConfig"] |
changelog (optional) |
Whether to author a dated changelog/ entry at all (Steps 7–8). Set false for repos with no changelog flow — no changelog/ directory and no changelog skill installed (e.g. a private repo with no release pipeline). When false, send-it skips changelog authoring entirely, and the category decision continues to drive only the PR title. Omit it (or set true) whenever the changelog skill is installed. |
true |
bundleVersioning (optional) |
Enables the per-bundle version-bump check (Step 6) for repos that ship many independently-versioned skill bundles. An object { root, manifest, skillFile } naming the bundle parent dir and the manifest / skill-manifest filenames each bundle carries. Omit it entirely in single-package repos — the check then no-ops. |
unset (disabled) |
triage (optional) |
Whether the run chains into the triage-pr skill once the PR is open (Step 11) — the CI fix loop, the promote-on-proven-green flip, then Phase B up to triage-pr's human envelope. true (the default) makes one /send-it drive the whole pipeline; set false in repos that want send-it to stop at the open PR, or where triage-pr isn't installed. --skip-triage does the same for a single run (A-1151). |
true |
The team name, issue-ID prefixes, and workspace slug are not configured here —
they live in the linear-sync and changelog skills' own config.json files,
read by the delegated steps.
Changelog scope (was
changelogScope). send-it authors a dated entry for every PR — the "record everything, filter later" model. Release notes come from filtering the changelog to the version-stamped (release-triggering) entries at release time, not from gating authoring at write time. ThechangelogScopeknob (added in 0.4.0) is gone (A-600); only thechangelog: true|falsemaster switch remains.
Prerequisites
ghCLI installed and authenticated (gh auth status).- The sibling skills (
commit,preflight,changelog) installed. linear-sync— optional; without it (or the Linear MCP server) the In Review writeback is skipped silently (Step 10).triage-pr— optional; without it the Step 11 chain warns and the run finishes at the open PR. The two behave differently on purpose: a skipped Linear writeback changes nothing about the PR, whereas a skipped triage chain leaves work undone.
Process
Step 0: Worktree resolution (only if --worktree= is set)
If --worktree=<branch-or-path> was passed, resolve and cd into that worktree
before any other step runs. Skip this step otherwise.
-
Run
git worktree list --porcelainto list worktrees with their paths and branches. -
Resolve the argument:
- Absolute path (starts with
/): match against theworktree <path>field. - Otherwise: treat as a branch name and match against the
branch refs/heads/<name>field.
- Absolute path (starts with
-
No match — exit immediately with:
No worktree found for <arg>. Available: <comma-separated paths>. -
Match —
cdinto the resolved worktree path. Thecwdpersists for the rest of the workflow, so all subsequentgitandghcalls operate on the worktree. -
Ensure dependencies are present. A freshly-created worktree has no
node_modules. If it is absent, runpnpm install --frozen-lockfilenow — before any step that invokes a bundled script or a validator — so--worktreeis self-sufficient:[ -d node_modules ] || pnpm install --frozen-lockfile -
Continue to Step 1.
This step does nothing when --worktree is omitted — no-arg send-it keeps working
unchanged from whatever directory the session is in.
Step 1: Branch guard
- Get the current branch:
git branch --show-current. - If on the base branch (
baseBranchfromconfig.json; defaultmain):- Run
git status --porcelain. If clean, exit with: "Nothing to ship from the base branch. Create a feature branch first." - If there are uncommitted changes:
- Inspect the diff (
git diffandgit diff --cached) and the changed file paths. - Derive a short kebab-case slug summarising the change (~3 words, lowercase,
max ~40 chars). Examples:
add-readme-section,fix-config-typo. - Branch name resolution (in order):
--branch=<name>— use as-is.--issue=<ID>— use<ID>-<slug>lower-cased (e.g.a-7-as-acquired), matching Linear'sgitBranchName.- Otherwise — just
<slug>(nowip/prefix).
- If the chosen branch already exists locally or on
origin, append-2,-3, … until unused. - Run
git checkout -b <branch>to move the working tree onto it. - Inform the user: "Was on the base branch with uncommitted changes; created
<branch>and continuing."
- Inspect the diff (
- Continue with the rest of the workflow on the new branch.
- Run
- If on a feature branch: continue.
Step 2: Refresh lockfile if package.json drifted
Skip this step if no package.json was touched on the branch.
-
git diff --name-only origin/<base>...HEAD | grep -E '(^|/)package\.json$'. If empty, skip. -
Run
pnpm install --frozen-lockfile. If it succeeds, the lockfile is already in sync — continue. -
If it fails, run
pnpm installto update the lockfile. -
If the lockfile changed, stage and commit it before any other commits go in:
git add pnpm-lock.yaml git commit -m "chore: update lockfile"
This keeps CI's --frozen-lockfile install green. (Skip silently in repos that
don't use pnpm.)
Step 3: Commit uncommitted changes — delegate to the commit skill
send-it is the all-in-one finisher: whatever's uncommitted should be committed before the changelog/PR work begins — but only what belongs to this branch.
Follow the commit skill to do this: classify uncommitted
files in-scope vs out-of-scope against the merge base (git merge-base HEAD origin/<base>), show a staging plan flagging any out-of-scope files (never git add -A; stray files from another branch/worktree are never staged silently), and
create logical atomic Conventional Commits (type + optional scope +
British-English body; ! / BREAKING CHANGE: for breaking changes). If clean,
skip this step. Direct the commit skill to classify against this send-it
run's resolved base — <base> is baseBranch (from config.json), or --base
when passed — not the commit skill's own config.json baseBranch, which
differs on a --base run (the stacked-PR case). The scope classification and the
out-of-scope guard must be computed against the same base send-it ships against,
or a stacked PR would mis-classify files.
The Conventional-Commit types this step writes are the input to Step 6's release
decision (derive-bump.mjs reads them back out of the commits), so the honest
types and ! / BREAKING CHANGE: markers matter.
This delegation covers only the initial commit of uncommitted work. send-it's own later, targeted commits stay here: the lockfile refresh (Step 2), the optional bundle-version bump (Step 6), and the changelog entry (Step 8).
Step 4: Fetch the base branch and confirm there's something to ship
git fetch origin <base>
If git log origin/<base>..HEAD is empty, exit with: "No commits ahead of the base
branch. Nothing to ship."
Step 5: Lint gate — delegate to the preflight skill
--skip-preflightbypasses this whole step. Print a clear⚠️ lint gate bypassed (--skip-preflight)warning and jump to Step 6. Use it only when the gate misfires; CI still runs the repo's real linting.
Run the change-gated lint preflight, following the preflight
skill:
node skills/preflight/scripts/preflight.mjs
Act on its exit-code contract, reading .preflight-summary.json to interpret a
non-zero exit:
- Exit 0 — pass. No introduced violations; continue.
- Exit 1 with
violations.introducedCount > 0— introduced violations (blocking). Runnode skills/preflight/scripts/lint-fix.mjs, re-run preflight, and repeat until introduced violations clear. Commit the fixes (astyle:/fix:commit, or fold into the relevant Step 3 commit if not yet pushed) before continuing. - Exit 1 with
introducedCount == 0andresults.failedLintersnon-empty — a linter could not run (its binary is absent), not a real violation. This is expected in a repo that doesn't use that toolchain (e.g. a docs/skills repo with no ESLint or markdownlint installed). Treat it as a skip, not a block: warn that<linter>was unavailable and continue. The repo's own CI owns whatever linting it actually runs. - Exit 2 — pre-existing violations only. Not introduced by this branch — do not block shipping. Surface them and continue (optionally offer a debt issue per the preflight skill).
Preflight is change-gated: it lints only the categories the branch touched, so
it no-ops when nothing lint-relevant changed. Skip this step entirely only if
preflight isn't installed.
Step 6: Decide release-type by category and compose the Conventional Commits PR title
Versioning is driven by release-please reading Conventional Commits. The estate uses a dual merge policy (A-1176 / ADR-0005):
- Feature / ship PRs land as merge commits. After merge, release-please ranks the landed commit subjects on trunk to decide the bump (A-824) — not the PR title alone.
- Release-please version PRs and fan-out PRs stay squash (orchestrator / fanout-spine). For those paths the squash subject remains the bump declaration.
- Both
allow_merge_commitandallow_squash_mergestay enabled (A-1177) — squash is not disabled.
send-it still composes a correct Conventional Commits PR title (CI's PR-title
lint + humans; the changelog-completeness gate still keys off a release-triggering
title) and writes the dated changelog entry (for every PR — see Step 7). It does
not bump versions, write any CHANGELOG.md, or tag.
Release-type is decided by the change's semantic category — the Conventional-Commit
type of the work send-it itself committed — not by which paths the diff touches
(A-598). A docs-only edit is docs: (no release) even when it lives under a published
path like skills/; a feat: is a release wherever its files sit. (Earlier versions
keyed this off shippablePaths, which mis-titled a docs edit inside a published path
as feat:/fix: and cut a spurious release.)
-
Derive the slug, body, type, and category from the branch commits via the bundled helper (zero-dep — no tsx):
node skills/send-it/scripts/derive-bump.mjsIt prints JSON:
{ "slug", "bump", "body", "type", "breaking", "category", "releaseTriggering" }:type— the dominant Conventional-Commit type across all branch commits (feat/fix/perf/docs/refactor/chore/ci/… — A-387); this is the PR-title prefix. Merge commits are excluded from the scan (git log --no-merges).breaking—trueif any commit carries a!or aBREAKING CHANGE:trailer.category— the dated changelogcategoryenum value (feat→feature,fix→fix,perf→perf,docs→docs,refactor→refactor, everything else →chore).releaseTriggering—trueiffbreakingortype ∈ {feat, fix, perf}. This is the release decision:truecuts a release,falsedoes not.bump—major/minor/patch, the release magnitude whenreleaseTriggering(aBREAKING CHANGE:/!→ major; dominantfeat:→ minor; else patch). Ignored whenreleaseTriggeringisfalse.
-
(Advisory) publish-surface cross-check.
shippablePaths/shippableManifestKeysinconfig.jsonare a documentation hint of the published surface — they do not decide release-type any more. Optionally sanity-check the category against them: ifreleaseTriggeringistruebut the diff (git diff --name-only origin/<base>...HEAD) touches noshippablePathsprefix (nor ashippableManifestKeyskey inpackage.json), note it in the PR body so a reviewer can confirm the release was intended — and likewise if a change touching a published path isreleaseTriggering: false. This is a soft note only; never let it override the category decision or block. -
Check per-bundle version bumps — only when
config.jsonsetsbundleVersioning(multi-artefact repos; skip this step entirely when it's unset). Each skill bundle carries its own version in itspackage.json+SKILL.md metadata.version, bumped by hand and decoupled from the repo release. CI enforces that the two agree, but nothing enforces they were bumped when the bundle's content changed — so an edited bundle can ship with a stale version label. Close that gap:node skills/send-it/scripts/check-skill-bumps.mjsIt prints
{ "configured", "unbumped": [{ name, currentVersion, suggestedBump, suggestedVersion, manifestPath, skillPath }], "bumped" }. For eachunbumpedentry, surface the proposal and apply it on confirmation:skills/<name>changed but its version is still<currentVersion>. Suggested bump:<suggestedBump>→<suggestedVersion>(matches the PR-title bump). Apply? (yes / no / patch / minor / major)On
yes(or an explicit level), edit bothmanifestPath(version) andskillPath(metadata.version) to the chosen version — in lockstep, so the parity invariant CI checks still holds — then stage and commit just those two files:git commit -m "chore(<name>): release <name>@<version>". Onno, leave it and continue. Under--dry-run, print the proposal and edit nothing. -
Compose the PR title as a single Conventional Commits subject — CI's PR-title lint and the changelog-completeness gate still require it. For feature PRs (merge commits), the post-merge bump comes from the landed commit subjects (A-824); the title remains the human/CI declaration and should match the dominant type. For squash paths (release + fan-out), the squash subject is still the bump declaration. If
--titlewas passed, use it verbatim (still runderive-bumpabove for the changelogcategory, and warn — don't block — if the supplied type contradicts the derivedtype/releaseTriggering). Otherwise build it straight from the derived fields:- Prefix =
type(add a scope when one is obvious, e.g.feat(<scope>):), plus!whenbreaking— sofeat: <body>,fix: <body>,perf: <body>,docs: <body>,refactor: <body>,chore: <body>,feat!: <body>, etc. - Release-triggering (
releaseTriggering: true) → the prefix is already a release type (feat/fix/perf, or any!). Add the scope; that's it. - Non-release (
releaseTriggering: false) → the prefix is a non-release type (docs/refactor/chore/ci/build/test/style).
⚠️ Keep the title honest with the commits. A mistyped prefix misleads reviewers and the completeness gate — a
feat:on a docs-only branch, or achore:on a real fix. For feature PRs the post-merge bump follows the landed commit subjects; for squash paths the title is the declaration. Derive the title from the change's semantic category (the commit types) so they stay aligned.When
releaseTriggeringisfalse, noteno release (<type>-only)in the PR body so reviewers can confirm the non-release type was intentional. - Prefix =
Step 7: Author or update the dated changelog entry — delegate to the changelog skill
Disabled entirely? If
config.jsonsetschangelog: false, skip Steps 7 and 8 completely — author nothing, run nochangelogscripts, make nodocs(changelog)commit — and note "changelog step disabled (no changelog flow in this repo)" in the run summary. This is for repos with nochangelog/directory and nochangelogskill installed; the category decision from Step 6 still drives the PR title. Whenchangelogis unset ortrue, always author an entry (thechangelogScopeknob was removed — A-600).An entry for every PR. send-it authors a dated
changelog/entry for every PR, release-triggering or not — the "record everything, filter later" model. The dated changelog is the full record of merged work; release notes filter it to the version-stamped (release-triggering) entries at release time, so a non-release entry simply carries noversion.changelog: falseis the only thing that suppresses authoring.
Follow the changelog skill to author or update the entry:
-
Detect an existing entry for this branch (by the
branchfrontmatter field) → update vs create. On update, preserve the filename andcreated_at. -
Write/refresh
changelog/<YYYYMMDD-HHMMSS>-<slug>.md(the<slug>from Step 6), derivingtitle/release_note/issuesfrom the branch. Setcategoryandbreakingstraight fromderive-bump's output (Step 6):categoryis itscategoryfield (feature/fix/perf/docs/refactor/chore— the changelog enum), andbreakingis itsbreakingflag. For a non-release entry (releaseTriggering: false),release_notemay be blank when there's no user-facing impact.Leave the post-merge fields (
merged_at,commit,pr,stats) andversionas blank placeholders — the post-merge enricher fills them (a non-release entry keepsversionblank, as no release is cut for it). This includespr: no step here writes it back after the PR opens; the post-merge enricher resolves it from the entry'sbranch:. -
Run the enrichment scripts:
node skills/changelog/scripts/set-affected-packages.mjsthennode skills/changelog/scripts/add-links.mjs. -
Validate:
node skills/changelog/scripts/validate-changelog.mjs. It must pass before committing — if it fails, surface the error and abort; don't auto-fix.
Step 8: Commit the changelog entry and push
--dry-runwrites nothing from here on. Steps 8–11 are the mutating half of the run. Under--dry-run, print what each would do and perform none of it: nogit commit, nogit push, nogh pr create/gh pr edit, no Linear transition (pass--dry-rundown tolinear-syncsosave_issueis never called), and--dry-runon the Step 11 hand-off. A dry run may read —gh pr view, the triage-pr preview — but it never writes. Then exit 0.
If a changelog/ entry was written in Step 7 (i.e. changelog is not false), commit
only that file:
git add changelog/<YYYYMMDD-HHMMSS>-<slug>.md
git commit -m "docs(changelog): <one-line summary>"
Then push the branch:
git push -u origin <branch>
Step 9: Create or update the PR
<title> is the Conventional Commits PR title from Step 6 — set it on both
create and update (re-derive it every run so it stays in sync with the branch's
commits). Feature PRs are intended to merge via merge commit; release and
fan-out automation keep using squash outside this skill.
- Check for an existing PR:
gh pr view --json number,url 2>/dev/null. - If creating:
gh pr create --base <base> --draft --title "<title>" --body "<body>". Use--ready(the flag) instead of--draftif the user passed--ready. - If updating:
gh pr edit <number> --title "<title>" --body "<body>". - Return the PR URL and number via
gh pr view --json url,number.
send-it never arms auto-merge. It opens and updates the PR; landing it stays a human action (A-1151). The old
--merge-when-readyflag — which armedgh pr merge --auto --mergehere — is gone as of 0.8.0: from Step 11 onward, a run can be sitting at triage-pr's disposition envelope, and an armed auto-merge could land the branch while that plan is still awaiting approval. Merge by hand, or arm auto-merge yourself once you're happy with the PR.
PR body template:
## Summary
- Comprehensive summary of all changes on this branch
- What changed and why
## Related Issues
<!-- Linear identifiers extracted from the branch and commits -->
- <ISSUE-ID>
## Test Plan
- [ ] <test>
Drop the ## Related Issues section if no issues were found.
Step 10: Transition linked Linear issues to In Review — delegate to the linear-sync skill
Follow the linear-sync skill with target state In
Review: read its config.json for linearTeamName and issueKeys, extract issue
IDs from the branch and commits, resolve the live state ID by team name (once),
and apply the transition idempotently (skip any issue already at or past In Review).
Skip silently if linear-sync or the Linear MCP server is unavailable.
Step 11: Drive the PR to merge-ready — delegate to the triage-pr skill
send-it opens the PR; triage-pr takes it the rest of the
way (A-1151). This step is part of the run — not an optional extra. One
/send-it drives the whole pipeline: Phase A fixes in-scope CI failures and promotes
the proven-green draft to ready, then Phase B waits for the AI reviewers, verifies
every finding, and halts at its human envelope. This step runs after Step 10 so
the linked issues are already In Review before triage begins.
-
Check the opt-out first — before anything else in this step. If
--skip-triagewas passed, orconfig.jsonsetstriage: false, printℹ️ triage chain skipped (--skip-triage): <reason>— or(triage: false)— report the PR URL, and stop the run here. Do not run the install check, and do not start the cold-start poll: a skipped chain must cost nothing. That is the pre-0.8.0 behaviour.Don't reach for it to finish sooner. The opt-out exists for the cases where the chain genuinely cannot work, not as a shortcut, and it is never the default:
triage: trueships inconfig.example.json, andinitialise-skillswritestruewhen reconciling a consumer. Skipping leaves the PR un-triaged — red CI unfixed, bot findings unread — which is the state this step exists to prevent, so treat it the way Step 5 treats--skip-preflight: say why in the report. The legitimate reasons are narrow: the PR changes the chain itself, so the running prose and the prose on disk disagree (this bundle's own ship runs); CI is gated ondraft == false, so a draft never registers a check (prefertriage: falsein that repo's config over a per-run flag); or the user asked to stop at the open PR. A missingtriage-prneeds no flag — sub-step 2 handles it. -
Confirm
triage-pris installed — look for../triage-pr/SKILL.mdbeside this bundle. If it is absent, print⚠️ triage-pr not installed — stopping at the open PR. Install it to chain: npx skills add <repo> --skill triage-pr --agent claude-code --copyand finish the run normally. A missing sibling warns, never fails — the same soft-skip contract Step 10 applies to
linear-sync. -
Wait for CI to register — the cold-start gate. Step 9 created or updated the PR moments ago, so GitHub Actions may not have registered a single check yet. An empty
statusCheckRolluphanded to a coldtriage-prreads as "nothing failing", and withpromoteOnGreenon (its default) that would flip the draft to ready before CI ever ran. triage-pr's "no failures yet is not green" rule guards its own watch loop, not a cold entry — so send-it proves at least one check exists before handing off. Poll every 10 seconds for up to 3 minutes, in a single shell loop (not 18 separate calls — a foregroundsleepbetween tool calls is slow and some harnesses refuse it). Stay quiet while polling; no interim "still waiting" pings:Capture
gh's exit status separately from the count — a failed call returns an empty string, and treating that as "zero checks" would silently convert an auth or API error into a full-window wait and a bogus "CI never started" verdict:for _ in $(seq 1 18); do if ! checks=$(gh pr view <number> --json statusCheckRollup --jq '[.statusCheckRollup[]?] | length'); then echo "gh pr view failed — cannot verify CI has started" >&2 exit 1 fi [ "$checks" -gt 0 ] && break sleep 10 done- At least one check registered → continue to sub-step 4.
ghitself fails → stop and surface the error (authentication, rate limit, a deleted PR). Do not fall through to the no-checks branch: an unverifiable state is not the same as a verified-empty one, and only the latter is safe to hand off.- Still
0when the window expires → CI never started for this PR (a repo with no workflows, apaths-filtered ordraft == false-gated workflow this PR doesn't match, or a stalled Actions queue). Report⚠️ no checks registered within 3 minutes — handing off with --no-promoteand add--no-promoteto the hand-off below, so an empty rollup can never be read as a proven green and flip the draft to ready. Nothing else about the chain changes.
-
Hand off. If send-it was run with
--dry-run,--dry-rungoes on this command too — always. A livetriage-prcommits, pushes, and can flip the draft to ready, so a dry run that omits it stops being a dry run. Follow thetriage-prskill against the PR from Step 9, naming its number explicitly so it never re-resolves to a different PR:triage-pr <number> [--dry-run] [--ci-only] [--no-promote] [--auto-apply]Forward
--dry-run,--ci-only,--no-promote, and--auto-applyverbatim when they were passed to send-it, plus--no-promotewhen sub-step 3's cold-start gate added it — that one is a safety flag this step owns, not a user flag, and dropping it would let an unverified rollup promote a draft. Add nothing beyond those.triage-prreads its ownconfig.json(promoteOnGreen,humanEnvelope,reviewBots,maxCiRounds, …) — send-it configures nothing about it, exactly as it configures nothing aboutcommit,preflight,changelog, orlinear-sync. -
Run the full chain. Don't stop between phases: Phase A's fix→push→watch loop, the promotion gate, then Phase B's review wait and verify-then-propose. Halt where
triage-prhalts — its human envelope, its slow-bot micro-gate, a hard blocker, ormaxCiRoundsexhaustion. The envelope is the run's natural stopping point: don't answer it on the user's behalf, and don't print a send-it "all done" over the top of it. -
Report once.
triage-pr's own final report is the run's report — prepend send-it's line items (branch, PR URL, changelog entry, Linear transitions) to it rather than emitting a second, competing summary. Respect triage-pr's quiet rule (A-1178): no interim pings around the hand-off.
--dry-runchains intotriage-pr --dry-run. When a PR already exists for the branch, hand off totriage-pr <number> --dry-runso the preview covers the failing checks and unresolved findings too. When no PR exists — a dry run creates none — printno PR to triage yetand exit 0. A dry run therefore makes read-onlyghcalls; it still writes nothing, commits nothing, and pushes nothing.Re-runs are safe. A second
/send-itre-enters the chain against the same PR.triage-prre-fetches threads every pass: resolved threads are filtered out, and proposed follow-up threads already carry the non-resolvingfollow-up-pendingmarker (A-679), so they arrive asdeferredThreads, not fresh findings. The envelope therefore re-prompts only for genuinely new bot findings.
Flags
--dry-run— print what would be written/submitted (changelog preview, branch, conventional PR title, any version-bump proposals), make no commits and no push. It chains intotriage-pr --dry-runwhen a PR already exists for the branch (Step 11), so it makes read-onlyghcalls but still writes nothing. Exit 0.--branch=<name>— override the auto-derived branch name when running on the base branch with uncommitted changes.--issue=<ID>— prefix the auto-derived slug with a Linear issue ID (e.g.--issue=A-7→a-7-<slug>, lower-cased). Ignored if--branchis given.--base=<branch>— overrideconfig.json'sbaseBranchfor this run. Applies everywhere the base is used: thegit fetch, the branch diff (origin/<base>...HEAD), the PR--base, and theBASE_REF=origin/<branch>env passed toderive-bump.mjs/check-skill-bumps.mjs. Use it for stacked PRs or a non-maintarget.--title="<conventional subject>"— set the PR title verbatim instead of deriving it (escape hatch for when derivation picks the wrong type). It must still be a valid Conventional Commits subject (CI lints it).derive-bumpstill runs (itscategorydrives the changelog entry); send-it warns if the supplied type contradicts the derivedtype/releaseTriggering.--skip-preflight— skip the Step 5 lint gate entirely, printing a bypass warning.--skip-triage— end the run at the open PR: skip the Step 11triage-prchain (identical toconfig.jsontriage: false). Restores the pre-0.8.0 bounded-finisher behaviour for one run. Not a shortcut — it leaves the PR un-triaged; see Step 11 for the narrow cases where it applies, and state the reason in the report.--ci-only— forwarded verbatim totriage-pr(Step 11): run its Phase A and stop at green, never promoting the draft. No effect on send-it's own steps.--no-promote— forwarded verbatim totriage-pr: never flip the draft to ready; stop at green. send-it also adds this itself when the cold-start gate times out. No effect on send-it's own steps. (--promoteis deliberately not forwarded — promotion is already triage-pr's default.)--auto-apply— forwarded verbatim totriage-pr: skip its Phase B human envelope and restore its legacy auto path (impact-gated fix-now; Linear-only gate for follow-ups). No effect on send-it's own steps.--ready— open the PR ready-for-review instead of draft (default is draft).--worktree=<branch-or-path>—cdinto a worktree before running (Step 0).
--merge-when-ready was removed in 0.8.0. send-it no longer arms
gh pr merge --auto --merge; see the Step 9 callout.
Notes
- Prose follows the host repo's language convention. Author the PR title, PR
body, and commit messages in the consuming repo's documented prose language. Across
this estate that is British English (
colour,behaviour,-ise/-yse); thechangelogskill applies the same rule to the entry it writes. This governs prose only — never identifiers, dependency names, or upstream API field names. - Trunk-based: PRs target the base branch (
config.jsonbaseBranch, or--basefor this run). - send-it bumps only per-bundle versions, never the repo version. The optional
Step 6 bundle-version check moves a changed skill's own
metadata.version; the repo-level npm release stays owned by release-please (feature PRs: landed commit subjects; squash paths: squash subject / PR title). - send-it drives the pipeline now, not just the finish (A-1151). Through 0.7.0 it
was a bounded finisher: seconds of work, ending in a report and an open PR. From
0.8.0 the default run continues into
triage-pr(Step 11), so a single/send-itcan stay unattended for roughly 30 minutes — CI fix rounds plus the review wait — and ends on a prompt (triage-pr's disposition envelope), not a report. That is a deliberate shift in what the command is.--skip-triage, ortriage: false, restores the old shape. - send-it never merges, and never arms auto-merge. Taking the PR to green and ready-for-review is the end of its remit; landing it is a human action.
- CI gated on non-draft PRs makes the chain a tax. send-it opens drafts by
default, so a repo whose workflows carry
if: github.event.pull_request.draft == falseregisters zero checks until the PR is ready — the Step 11 cold-start gate then waits its full 3 minutes every run and hands off with--no-promoteto a triage-pr with nothing to do. Use--ready, or settriage: false, in those repos. - Idempotent: re-running send-it updates the existing PR title and changelog
entry; the Linear writeback skips issues already In Review or beyond; the Step 11
chain re-enters
triage-pragainst the same PR, whosefollow-up-pendingmarkers (A-679) keep already-dispositioned findings out of the envelope. - send-it does not bump versions or write any
CHANGELOG.md. release-please ranks Conventional Commits on trunk after merge (merge-commit history for feature PRs; squash subject for release/fan-out), bumps the manifest in the release PR, and the release workflow publishes + tags. send-it only writes the datedchangelog/<ts>-<slug>.mdentry (Step 7), finalised post-merge by the in-repo enricher.
Error Handling
gh auth statusfails — rungh auth loginfirst; abort until authenticated.- changelog validation fails — surface the error; don't auto-fix. The user resolves the entry and re-runs.
- No commits ahead of the base — exit "No commits ahead of the base branch. Nothing to ship."
- Branch push fails — verify push access; ensure the remote is configured.
- PR create/update fails — verify the PR isn't closed; verify the branch is pushed.
triage-prisn't installed — warn and finish at the open PR (Step 11). A missing sibling never fails a send-it run.- No checks register within the cold-start window — hand off with
--no-promoteand say why. An emptystatusCheckRollupmust never be read as green. - The triage chain fails, is aborted, or the envelope is declined — the commits,
changelog entry, PR, and Linear transitions from Steps 3–10 all stand. Re-run
/triage-pr <number>directly rather than re-running the whole of send-it.