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
commit skill (classify in-scope vs out-of-scope, atomic
Conventional Commits, out-of-scope guard).
- Lint gate → the
preflight skill (change-gated; no-ops when nothing
lint-relevant changed).
- Changelog → the
changelog skill (author/update + validate; an entry for
every PR, skipped entirely only when config.json sets changelog: false).
- Linear In Review → the
linear-sync skill (resolve state by team name,
idempotent transition).
- Post-PR triage → the
triage-pr skill (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.
Done criteria. Opening or updating the PR (Step 9) and moving Linear issues
to In Review (Step 10) are mid-pipeline — not the end of /send-it. The run
is incomplete until Step 11 (triage-pr) has started, or you printed an explicit
skip/degraded line with a stated reason (ℹ️ triage chain skipped … for
--skip-triage / triage: false, or ⚠️ triage-pr not installed … when the
sibling is absent). Reporting a draft PR URL as the final outcome without one of
those lines is a failed run.
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 (omit or true by default) |
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. The key may be omitted (defaults to true); the step is not optional on a default run. Set false only in repos that deliberately stop at the open PR, or where triage-pr isn't installed. --skip-triage does the same for a single run (A-1151) — always state why. |
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. The changelogScope
knob (added in 0.4.0) is gone (A-600); only the changelog: true|false
master switch remains.
Prerequisites
gh CLI 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 — required for the default pipeline. Without it the Step 11 chain
warns and the run finishes at the open PR — that soft-skip is a degraded
outcome, not a successful finish. The two siblings 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 --porcelain to list worktrees with their paths and
branches.
Resolve the argument:
- Absolute path (starts with
/): match against the worktree <path>
field.
- Otherwise: treat as a branch name and match against the
branch refs/heads/<name> field.
No match — exit immediately with: No worktree found for <arg>. Available: <comma-separated paths>.
Match — cd into the resolved worktree path. The cwd persists for the
rest of the workflow, so all subsequent git and gh calls operate on the
worktree.
Ensure dependencies are present. A freshly-created worktree has no
node_modules. If it is absent, run pnpm install --frozen-lockfile now —
before any step that invokes a bundled script or a validator — so --worktree
is 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 (
baseBranch from config.json; default main):
- 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 diff and git 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's gitBranchName.
- Otherwise — just
<slug> (no wip/ 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."
- Continue with the rest of the workflow on the new branch.
- 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 install to 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 ship pipeline: 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-preflight bypasses 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). Run node skills/preflight/scripts/lint-fix.mjs, re-run preflight,
and repeat until introduced violations clear. Commit the fixes (a style:/fix:
commit, or fold into the relevant Step 3 commit if not yet pushed) before
continuing.
- Exit 1 with
introducedCount == 0 and results.failedLinters non-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_commit and allow_squash_merge stay 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.mjs
It 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 — true if any commit carries a ! or a BREAKING CHANGE: trailer.
category — the dated changelog category enum value (feat→feature,
fix→fix, perf→perf, docs→docs, refactor→refactor, everything else
→chore).
releaseTriggering — true iff breaking or type ∈ {feat, fix, perf}. This is
the release decision: true cuts a release, false does not.
bump — major/minor/patch, the release magnitude when releaseTriggering
(a BREAKING CHANGE:/! → major; dominant feat: → minor; else patch). Ignored
when releaseTriggering is false.
(Advisory) publish-surface cross-check. shippablePaths /
shippableManifestKeys in config.json are a documentation hint of
the published surface — they do not decide release-type any more. Optionally
sanity-check the category against them: if releaseTriggering is true but the diff
(git diff --name-only origin/<base>...HEAD) touches no shippablePaths prefix
(nor a shippableManifestKeys key in package.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 is releaseTriggering: false. This is a soft note only; never let it
override the category decision or block.
Check per-bundle version bumps — only when config.json sets
bundleVersioning (multi-artefact repos; skip this step entirely when it's
unset). Each skill bundle carries its own version in its package.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.mjs
It prints { "configured", "unbumped": [{ name, currentVersion, suggestedBump, suggestedVersion, manifestPath, skillPath }], "bumped" }. For each unbumped
entry, 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 both manifestPath (version) and
skillPath (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>". On no, 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 --title was passed, use it verbatim (still
run derive-bump above for the changelog category, and warn — don't block
— if the supplied type contradicts the derived type/releaseTriggering).
Otherwise build it straight from the derived fields:
- Prefix =
type (add a scope when one is obvious, e.g. feat(<scope>):), plus
! when breaking — so feat: <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 a
chore: 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 releaseTriggering is false, note no release (<type>-only) in the PR body
so reviewers can confirm the non-release type was intentional.
Step 7: Author or update the dated changelog entry — delegate to the changelog skill
Disabled entirely? If config.json sets changelog: false, skip Steps 7
and 8 completely — author nothing, run no changelog scripts, make no
docs(changelog) commit — and note "changelog step disabled (no changelog flow in
this repo)" in the run summary. This is for repos with no changelog/ directory and
no changelog skill installed; the category decision from Step 6 still drives the PR
title. When changelog is unset or true, always author an entry (the
changelogScope knob 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 no version. changelog: false is 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 branch frontmatter field) →
update vs create. On update, preserve the filename and created_at.
Write/refresh changelog/<YYYYMMDD-HHMMSS>-<slug>.md (the <slug> from Step 6),
deriving title/release_note/issues from the branch. Set category and
breaking straight from derive-bump's output (Step 6): category is its
category field (feature/fix/perf/docs/refactor/chore — the changelog
enum), and breaking is its breaking flag. For a non-release entry
(releaseTriggering: false), release_note may be blank when there's no
user-facing impact.
Leave the post-merge fields (merged_at, commit, pr, stats)
and version as blank placeholders — the post-merge enricher fills them (a non-release
entry keeps version blank, as no release is cut for it). This includes pr: no
step here writes it back after the PR opens; the post-merge enricher resolves it
from the entry's branch:.
Run the enrichment scripts: node skills/changelog/scripts/set-affected-packages.mjs
then node 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-run writes 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: no
git commit, no git push, no gh pr create / gh pr edit, no Linear transition
(pass --dry-run down to linear-sync so save_issue is never called), and
--dry-run on 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 --draft if 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-ready flag — which armed
gh pr merge --auto --merge here — 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
Completion gate. Do not treat the Step 9 PR URL as the final report and
stop. Steps 9–10 are mid-pipeline. Continue into this step unless the opt-out in
sub-step 1 applies, or sub-step 2 finds triage-pr missing (and you print the
corresponding skip/degraded line with a reason). A draft-only report without
ℹ️ triage chain skipped … or ⚠️ triage-pr not installed … is a failed
/send-it run (A-1645).
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-triage
was passed, or config.json sets triage: 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: true ships in config.example.json, and initialise-skills
writes true when 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 on draft == false, so a draft never registers a check (prefer
triage: false in that repo's config over a per-run flag); or the user asked to
stop at the open PR. A missing triage-pr needs no flag — sub-step 2 handles it.
Confirm triage-pr is installed — look for ../triage-pr/SKILL.md beside 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 --copy
and finish the run. That warning is the degraded-outcome line the completion
gate accepts (alongside ℹ️ triage chain skipped …) — the run may stop, but it
must not report as a successful default finish. A missing sibling warns, never
fails the process exit — louder than Step 10's silent linear-sync skip,
because a skipped triage chain leaves PR work undone.
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
statusCheckRollup handed to a cold triage-pr reads as "nothing failing", and with
promoteOnGreen on (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 foreground sleep between 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.
gh itself 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
0 when the window expires → CI never started for this PR (a repo with
no workflows, a paths-filtered or draft == false-gated workflow this PR doesn't
match, or a stalled Actions queue). Report
⚠️ no checks registered within 3 minutes — handing off with --no-promote and add
--no-promote to 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-run goes on this
command too — always. A live triage-pr commits, pushes, and can flip the draft
to ready, so a dry run that omits it stops being a dry run. Follow the
triage-pr skill 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-apply verbatim
when they were passed to send-it, plus --no-promote when 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-pr reads its own
config.json (promoteOnGreen, humanEnvelope, reviewBots, maxCiRounds, …) —
send-it configures nothing about it, exactly as it configures nothing about
commit, preflight, changelog, or linear-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-pr halts — its human envelope, its slow-bot micro-gate, a hard blocker, or
maxCiRounds exhaustion. 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-run chains into triage-pr --dry-run. When a PR already exists for the
branch, hand off to triage-pr <number> --dry-run so the preview covers the failing
checks and unresolved findings too. When no PR exists — a dry run creates none —
print no PR to triage yet and exit 0. A dry run therefore makes read-only gh
calls; it still writes nothing, commits nothing, and pushes nothing.
Re-runs are safe. A second /send-it re-enters the chain against the same PR.
triage-pr re-fetches threads every pass: resolved threads are filtered out, and
proposed follow-up threads already carry the non-resolving follow-up-pending marker (A-679),
so they arrive as deferredThreads, 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 into triage-pr --dry-run when a PR already exists for the branch (Step 11),
so it makes read-only gh calls 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 --branch is given.
--base=<branch> — override config.json's baseBranch for this run. Applies
everywhere the base is used: the git fetch, the branch diff
(origin/<base>...HEAD), the PR --base, and the BASE_REF=origin/<branch> env
passed to derive-bump.mjs / check-skill-bumps.mjs. Use it for stacked PRs or a
non-main target.
--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-bump still runs (its category
drives the changelog entry); send-it warns if the supplied type contradicts the
derived type/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 11 triage-pr chain
(identical to config.json triage: 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 to triage-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 to triage-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. (--promote is deliberately not forwarded —
promotion is already triage-pr's default.)
--auto-apply — forwarded verbatim to triage-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> — cd into 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); the
changelog skill 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.json baseBranch, or
--base for 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 open PR (A-1151 / A-1645). 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-it can 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, or
triage: false, restores the old shape — only with a stated reason.
- send-it never merges, and never arms auto-merge. Taking the PR through triage
to green and ready-for-review is the end of its remit; landing it is a human
action. Do not conflate "leave the merge to the human" with "stop once the draft
exists".
- 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 == false registers 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-promote to a
triage-pr with nothing to do. Use --ready, or set triage: 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-pr against the same PR, whose follow-up-pending markers
(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 dated
changelog/<ts>-<slug>.md entry (Step 7), finalised post-merge by the in-repo enricher.
Error Handling
gh auth status fails — run gh auth login first; 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-pr isn'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-promote
and say why. An empty statusCheckRollup must 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.
1---2name: send-it3description: The all-in-one ship pipeline — commit uncommitted work, run the change-gated lint preflight, author the dated changelog entry, compose a Conventional Commits PR title, push, open or update a PR, move linked Linear issues to In Review, then chain into `triage-pr` (Step 11) to drive the PR to merge-ready. Incomplete until Step 11 has run, or `--skip-triage` / `triage: false` was used with a stated reason. Use when asked to ship, send it, finish a branch, open or update a PR, or wrap up and push. Thin orchestrator over `commit`, `preflight`, `changelog`, `linear-sync`, and `triage-pr`; owns branch guard, release-type decision, PR title, push, and PR. Serves monorepos and single-package repos alike.4license: MIT5---67# send-it89Bundle uncommitted work into atomic commits (via the10[`commit`](../commit/SKILL.md) skill), run the change-gated lint11[`preflight`](../preflight/SKILL.md), author or update the dated12`changelog/<ts>-<slug>.md` entry (via the [`changelog`](../changelog/SKILL.md)13skill), compose a **Conventional Commits PR title** (CI + humans; under the dual14merge policy, feature PRs land as merge commits and release-please ranks the15landed **commit subjects** for the bump — A-1176 / A-824), push the branch, open16or update a pull request against the base branch, and transition any linked Linear17issues to **In Review** (via the [`linear-sync`](../linear-sync/SKILL.md) skill).1819This skill is the single source of truth for the **ship flow**. It is a thin20orchestrator: it owns only the glue no sibling skill does — the branch guard,21worktree resolution, the release-type decision (by category), PR-title22composition, push, and the PR — and delegates the rest:2324- **Commit** → the `commit` skill (classify in-scope vs out-of-scope, atomic25 Conventional Commits, out-of-scope guard).26- **Lint gate** → the `preflight` skill (change-gated; no-ops when nothing27 lint-relevant changed).28- **Changelog** → the `changelog` skill (author/update + validate; an entry for29 **every** PR, skipped entirely only when `config.json` sets `changelog: false`).30- **Linear In Review** → the `linear-sync` skill (resolve state by team name,31 idempotent transition).32- **Post-PR triage** → the `triage-pr` skill (Phase A CI fix loop and the33 promote-on-proven-green flip, then Phase B review dispositions up to its human34 envelope — Step 11, A-1151).3536The delegated skills auto-detect their own scope, so monorepo features37(per-workspace ESLint fan-out, changelog `affected_packages`) no-op cleanly in a38single-package repo. send-it configures nothing about them.3940> **Install the delegated skills alongside `send-it`.** This bundle invokes and41> links its siblings by relative path (`../commit/SKILL.md`, `../preflight/SKILL.md`,42> `../changelog/SKILL.md`, `../linear-sync/SKILL.md`, `../triage-pr/SKILL.md`), so a43> `--skill send-it`-only install leaves the commit, lint, changelog, Linear, and44> triage steps unavailable and those links dangling. Install them together:45>46> ```bash47> npx skills add https://github.com/rheged-studio/agent-skills \48> --skill send-it --skill commit --skill preflight --skill changelog --skill linear-sync \49> --skill triage-pr \50> --agent claude-code --agent cursor --copy51> ```5253This flow intentionally does **not** run typecheck, tests, or format checks — CI54handles those. The only gate it runs is the change-gated `preflight` lint.5556> **Done criteria.** Opening or updating the PR (Step 9) and moving Linear issues57> to In Review (Step 10) are mid-pipeline — **not** the end of `/send-it`. The run58> is incomplete until Step 11 (`triage-pr`) has started, or you printed an explicit59> skip/degraded line with a stated reason (`ℹ️ triage chain skipped …` for60> `--skip-triage` / `triage: false`, or `⚠️ triage-pr not installed …` when the61> sibling is absent). Reporting a draft PR URL as the final outcome without one of62> those lines is a failed run.6364## Configuration6566A few knobs live in [`config.json`](config.json) beside this file; edit your67copied `config.json` to match the consuming repo (a neutral68[`config.example.json`](config.example.json) ships as a template):6970| Key | Meaning | Default |71| --- | --- | --- |72| `baseBranch` | The trunk the branch diff is taken against (`origin/<baseBranch>`) and the PR base. | `"main"` |73| `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/"]` |74| `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"]` |75| `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` |76| `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) |77| `triage` _(omit or `true` by default)_ | Whether the run chains into the [`triage-pr`](../triage-pr/SKILL.md) 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. The **key** may be omitted (defaults to `true`); the **step** is not optional on a default run. Set `false` only in repos that deliberately stop at the open PR, or where `triage-pr` isn't installed. `--skip-triage` does the same for a single run (A-1151) — always state why. | `true` |7879The team name, issue-ID prefixes, and workspace slug are **not** configured here —80they live in the `linear-sync` and `changelog` skills' own `config.json` files,81read by the delegated steps.8283> **Changelog scope (was `changelogScope`).** send-it authors a dated entry for84> **every** PR — the "record everything, filter later" model. Release notes come85> from filtering the changelog to the version-stamped (release-triggering) entries86> at release time, not from gating authoring at write time. The `changelogScope`87> knob (added in 0.4.0) is **gone** (A-600); only the `changelog: true|false`88> master switch remains.8990## Prerequisites9192- `gh` CLI installed and authenticated (`gh auth status`).93- The sibling skills (`commit`, `preflight`, `changelog`) installed.94- `linear-sync` — optional; without it (or the Linear MCP server) the In Review95 writeback is skipped **silently** (Step 10).96- `triage-pr` — **required for the default pipeline.** Without it the Step 11 chain97 **warns** and the run finishes at the open PR — that soft-skip is a degraded98 outcome, not a successful finish. The two siblings behave differently on purpose: a99 skipped Linear writeback changes nothing about the PR, whereas a skipped triage100 chain leaves work undone.101102## Process103104### Step 0: Worktree resolution (only if `--worktree=` is set)105106If `--worktree=<branch-or-path>` was passed, resolve and `cd` into that worktree107before any other step runs. Skip this step otherwise.1081091. Run `git worktree list --porcelain` to list worktrees with their paths and110 branches.1112. Resolve the argument:112 - **Absolute path** (starts with `/`): match against the `worktree <path>`113 field.114 - **Otherwise**: treat as a branch name and match against the115 `branch refs/heads/<name>` field.1163. **No match** — exit immediately with: `No worktree found for <arg>. Available:117 <comma-separated paths>`.1184. **Match** — `cd` into the resolved worktree path. The `cwd` persists for the119 rest of the workflow, so all subsequent `git` and `gh` calls operate on the120 worktree.1215. **Ensure dependencies are present.** A freshly-created worktree has no122 `node_modules`. If it is absent, run `pnpm install --frozen-lockfile` now —123 before any step that invokes a bundled script or a validator — so `--worktree`124 is self-sufficient:125126 ```bash127 [ -d node_modules ] || pnpm install --frozen-lockfile128 ```1291306. Continue to Step 1.131132This step does nothing when `--worktree` is omitted — no-arg send-it keeps working133unchanged from whatever directory the session is in.134135### Step 1: Branch guard1361371. Get the current branch: `git branch --show-current`.1382. **If on the base branch** (`baseBranch` from `config.json`; default `main`):139 - Run `git status --porcelain`. If clean, exit with: "Nothing to ship from the140 base branch. Create a feature branch first."141 - If there are uncommitted changes:142 - Inspect the diff (`git diff` and `git diff --cached`) and the changed file143 paths.144 - Derive a short kebab-case slug summarising the change (~3 words, lowercase,145 max ~40 chars). Examples: `add-readme-section`, `fix-config-typo`.146 - **Branch name resolution (in order):**147 1. `--branch=<name>` — use as-is.148 2. `--issue=<ID>` — use `<ID>-<slug>` **lower-cased** (e.g.149 `a-7-as-acquired`), matching Linear's `gitBranchName`.150 3. Otherwise — just `<slug>` (no `wip/` prefix).151 - If the chosen branch already exists locally or on `origin`, append `-2`,152 `-3`, … until unused.153 - Run `git checkout -b <branch>` to move the working tree onto it.154 - Inform the user: "Was on the base branch with uncommitted changes; created155 `<branch>` and continuing."156 - Continue with the rest of the workflow on the new branch.1573. **If on a feature branch:** continue.158159### Step 2: Refresh lockfile if `package.json` drifted160161Skip this step if no `package.json` was touched on the branch.1621631. `git diff --name-only origin/<base>...HEAD | grep -E '(^|/)package\.json$'`. If164 empty, skip.1652. Run `pnpm install --frozen-lockfile`. If it succeeds, the lockfile is already in166 sync — continue.1673. If it fails, run `pnpm install` to update the lockfile.1684. If the lockfile changed, stage and commit it before any other commits go in:169170 ```bash171 git add pnpm-lock.yaml172 git commit -m "chore: update lockfile"173 ```174175This keeps CI's `--frozen-lockfile` install green. (Skip silently in repos that176don't use pnpm.)177178### Step 3: Commit uncommitted changes — delegate to the `commit` skill179180send-it is the all-in-one ship pipeline: whatever's uncommitted should be committed181before the changelog/PR work begins — but only what belongs to _this_ branch.182183Follow the [`commit`](../commit/SKILL.md) skill to do this: classify uncommitted184files **in-scope vs out-of-scope** against the merge base (`git merge-base HEAD185origin/<base>`), show a staging plan flagging any out-of-scope files (never `git186add -A`; stray files from another branch/worktree are never staged silently), and187create **logical atomic Conventional Commits** (type + optional scope +188British-English body; `!` / `BREAKING CHANGE:` for breaking changes). If clean,189skip this step. Direct the `commit` skill to classify against **this** send-it190run's resolved base — `<base>` is `baseBranch` (from `config.json`), or `--base`191when passed — **not** the `commit` skill's own `config.json` `baseBranch`, which192differs on a `--base` run (the stacked-PR case). The scope classification and the193out-of-scope guard must be computed against the same base send-it ships against,194or a stacked PR would mis-classify files.195196The Conventional-Commit types this step writes are the input to Step 6's release197decision (`derive-bump.mjs` reads them back out of the commits), so the honest198types and `!` / `BREAKING CHANGE:` markers matter.199200This delegation covers only the _initial_ commit of uncommitted work. send-it's own201later, targeted commits stay here: the lockfile refresh (Step 2), the optional202bundle-version bump (Step 6), and the changelog entry (Step 8).203204### Step 4: Fetch the base branch and confirm there's something to ship205206```bash207git fetch origin <base>208```209210If `git log origin/<base>..HEAD` is empty, exit with: "No commits ahead of the base211branch. Nothing to ship."212213### Step 5: Lint gate — delegate to the `preflight` skill214215> **`--skip-preflight`** bypasses this whole step. Print a clear216> `⚠️ lint gate bypassed (--skip-preflight)` warning and jump to Step 6. Use it only217> when the gate misfires; CI still runs the repo's real linting.218219Run the change-gated lint preflight, following the [`preflight`](../preflight/SKILL.md)220skill:221222```bash223node skills/preflight/scripts/preflight.mjs224```225226Act on its exit-code contract, reading `.preflight-summary.json` to interpret a227non-zero exit:228229- **Exit 0 — pass.** No introduced violations; continue.230- **Exit 1 with `violations.introducedCount > 0` — introduced violations231 (blocking).** Run `node skills/preflight/scripts/lint-fix.mjs`, re-run preflight,232 and repeat until introduced violations clear. Commit the fixes (a `style:`/`fix:`233 commit, or fold into the relevant Step 3 commit if not yet pushed) before234 continuing.235- **Exit 1 with `introducedCount == 0` and `results.failedLinters` non-empty — a236 linter could not run (its binary is absent), not a real violation.** This is237 expected in a repo that doesn't use that toolchain (e.g. a docs/skills repo with238 no ESLint or markdownlint installed). Treat it as a **skip, not a block**: warn239 that `<linter>` was unavailable and continue. The repo's own CI owns whatever240 linting it actually runs.241- **Exit 2 — pre-existing violations only.** Not introduced by this branch — do not242 block shipping. Surface them and continue (optionally offer a debt issue per the243 preflight skill).244245Preflight is **change-gated**: it lints only the categories the branch touched, so246it no-ops when nothing lint-relevant changed. Skip this step entirely only if247`preflight` isn't installed.248249### Step 6: Decide release-type by category and compose the Conventional Commits PR title250251Versioning is driven by [release-please](https://github.com/googleapis/release-please)252reading **Conventional Commits**. The estate uses a **dual merge policy** (A-1176 /253[ADR-0005](../../../architecture/0005-dual-merge-policy.md)):254255- **Feature / ship PRs** land as **merge commits**. After merge, release-please ranks256 the landed **commit subjects** on trunk to decide the bump (A-824) — not the PR257 title alone.258- **Release-please version PRs** and **fan-out PRs** stay **squash** (orchestrator /259 fanout-spine). For those paths the squash subject remains the bump declaration.260- Both `allow_merge_commit` and `allow_squash_merge` stay enabled (A-1177) — squash261 is not disabled.262263send-it still composes a correct Conventional Commits **PR title** (CI's PR-title264lint + humans; the changelog-completeness gate still keys off a release-triggering265title) and writes the dated changelog entry (for every PR — see Step 7). It does266**not** bump versions, write any `CHANGELOG.md`, or tag.267268Release-type is decided by the change's **semantic category — the Conventional-Commit269type of the work send-it itself committed — not by which paths the diff touches**270(A-598). A docs-only edit is `docs:` (no release) even when it lives under a published271path like `skills/`; a `feat:` is a release wherever its files sit. (Earlier versions272keyed this off `shippablePaths`, which mis-titled a docs edit inside a published path273as `feat:`/`fix:` and cut a spurious release.)2742751. **Derive the slug, body, type, and category** from the branch commits via the276 bundled helper (zero-dep — no tsx):277278 ```bash279 node skills/send-it/scripts/derive-bump.mjs280 ```281282 It prints JSON:283 `{ "slug", "bump", "body", "type", "breaking", "category", "releaseTriggering" }`:284 - `type` — the **dominant** Conventional-Commit type across **all** branch commits285 (`feat`/`fix`/`perf`/`docs`/`refactor`/`chore`/`ci`/… — A-387); this is the286 PR-title prefix. Merge commits are excluded from the scan (`git log --no-merges`).287 - `breaking` — `true` if any commit carries a `!` or a `BREAKING CHANGE:` trailer.288 - `category` — the dated changelog `category` enum value (`feat`→`feature`,289 `fix`→`fix`, `perf`→`perf`, `docs`→`docs`, `refactor`→`refactor`, everything else290 →`chore`).291 - `releaseTriggering` — `true` iff `breaking` or `type ∈ {feat, fix, perf}`. This is292 the release decision: `true` cuts a release, `false` does not.293 - `bump` — `major`/`minor`/`patch`, the release **magnitude** when `releaseTriggering`294 (a `BREAKING CHANGE:`/`!` → major; dominant `feat:` → minor; else patch). Ignored295 when `releaseTriggering` is `false`.2962972. **(Advisory) publish-surface cross-check.** `shippablePaths` /298 `shippableManifestKeys` in [`config.json`](config.json) are a documentation hint of299 the published surface — they **do not** decide release-type any more. Optionally300 sanity-check the category against them: if `releaseTriggering` is `true` but the diff301 (`git diff --name-only origin/<base>...HEAD`) touches **no** `shippablePaths` prefix302 (nor a `shippableManifestKeys` key in `package.json`), note it in the PR body so a303 reviewer can confirm the release was intended — and likewise if a change touching a304 published path is `releaseTriggering: false`. This is a soft note only; never let it305 override the category decision or block.3063073. **Check per-bundle version bumps** — only when `config.json` sets308 `bundleVersioning` (multi-artefact repos; skip this step entirely when it's309 unset). Each skill bundle carries its own version in its `package.json` +310 `SKILL.md metadata.version`, bumped by hand and decoupled from the repo release.311 CI enforces that the two **agree**, but nothing enforces they were **bumped** when312 the bundle's content changed — so an edited bundle can ship with a stale version313 label. Close that gap:314315 ```bash316 node skills/send-it/scripts/check-skill-bumps.mjs317 ```318319 It prints `{ "configured", "unbumped": [{ name, currentVersion, suggestedBump,320 suggestedVersion, manifestPath, skillPath }], "bumped" }`. For **each** `unbumped`321 entry, surface the proposal and apply it on confirmation:322323 > `skills/<name>` changed but its version is still `<currentVersion>`. Suggested324 > bump: `<suggestedBump>` → `<suggestedVersion>` (matches the PR-title bump).325 > Apply? (yes / no / patch / minor / major)326327 On `yes` (or an explicit level), edit **both** `manifestPath` (`version`) and328 `skillPath` (`metadata.version`) to the chosen version — in lockstep, so the329 parity invariant CI checks still holds — then stage and commit just those two330 files: `git commit -m "chore(<name>): release <name>@<version>"`. On `no`, leave331 it and continue. Under `--dry-run`, print the proposal and edit nothing.3323334. **Compose the PR title** as a single Conventional Commits subject — CI's334 PR-title lint and the changelog-completeness gate still require it. For **feature335 PRs** (merge commits), the post-merge bump comes from the landed **commit336 subjects** (A-824); the title remains the human/CI declaration and should match337 the dominant type. For **squash** paths (release + fan-out), the squash subject338 is still the bump declaration. If `--title` was passed, use it verbatim (still339 run `derive-bump` above for the changelog `category`, and **warn** — don't block340 — if the supplied type contradicts the derived `type`/`releaseTriggering`).341 Otherwise build it straight from the derived fields:342 - **Prefix** = `type` (add a scope when one is obvious, e.g. `feat(<scope>):`), plus343 `!` when `breaking` — so `feat: <body>`, `fix: <body>`, `perf: <body>`,344 `docs: <body>`, `refactor: <body>`, `chore: <body>`, `feat!: <body>`, etc.345 - **Release-triggering** (`releaseTriggering: true`) → the prefix is already a346 release type (`feat`/`fix`/`perf`, or any `!`). Add the scope; that's it.347 - **Non-release** (`releaseTriggering: false`) → the prefix is a non-release type348 (`docs`/`refactor`/`chore`/`ci`/`build`/`test`/`style`).349350 > ⚠️ **Keep the title honest with the commits.** A mistyped prefix misleads351 > reviewers and the completeness gate — a `feat:` on a docs-only branch, or a352 > `chore:` on a real fix. For feature PRs the post-merge bump follows the landed353 > commit subjects; for squash paths the title _is_ the declaration. Derive the354 > title from the change's semantic category (the commit types) so they stay355 > aligned.356357 When `releaseTriggering` is `false`, note `no release (<type>-only)` in the PR body358 so reviewers can confirm the non-release type was intentional.359360### Step 7: Author or update the dated changelog entry — delegate to the `changelog` skill361362> **Disabled entirely?** If `config.json` sets `changelog: false`, **skip Steps 7363> and 8 completely** — author nothing, run no `changelog` scripts, make no364> `docs(changelog)` commit — and note "changelog step disabled (no changelog flow in365> this repo)" in the run summary. This is for repos with no `changelog/` directory and366> no `changelog` skill installed; the category decision from Step 6 still drives the PR367> title. When `changelog` is unset or `true`, **always author an entry** (the368> `changelogScope` knob was removed — A-600).369>370> **An entry for every PR.** send-it authors a dated `changelog/` entry for **every**371> PR, release-triggering or not — the "record everything, filter later" model. The372> dated changelog is the full record of merged work; release notes filter it to the373> version-stamped (release-triggering) entries at release time, so a non-release entry374> simply carries no `version`. `changelog: false` is the only thing that suppresses375> authoring.376377Follow the [`changelog`](../changelog/SKILL.md) skill to author or update the entry:3783791. Detect an existing entry for this branch (by the `branch` frontmatter field) →380 update vs create. On update, preserve the filename and `created_at`.3812. Write/refresh `changelog/<YYYYMMDD-HHMMSS>-<slug>.md` (the `<slug>` from Step 6),382 deriving `title`/`release_note`/`issues` from the branch. Set `category` and383 `breaking` straight from `derive-bump`'s output (Step 6): `category` is its384 `category` field (`feature`/`fix`/`perf`/`docs`/`refactor`/`chore` — the changelog385 enum), and `breaking` is its `breaking` flag. For a non-release entry386 (`releaseTriggering: false`), `release_note` may be blank when there's no387 user-facing impact.388389 Leave the post-merge fields (`merged_at`, `commit`, `pr`, `stats`)390 and `version` as blank placeholders — the post-merge enricher fills them (a non-release391 entry keeps `version` blank, as no release is cut for it). This includes `pr`: no392 step here writes it back after the PR opens; the post-merge enricher resolves it393 from the entry's `branch:`.3943. Run the enrichment scripts: `node skills/changelog/scripts/set-affected-packages.mjs`395 then `node skills/changelog/scripts/add-links.mjs`.3964. **Validate:** `node skills/changelog/scripts/validate-changelog.mjs`. It must pass397 before committing — if it fails, surface the error and abort; don't auto-fix.398399### Step 8: Commit the changelog entry and push400401> **`--dry-run` writes nothing from here on.** Steps 8–11 are the mutating half of the402> run. Under `--dry-run`, print what each would do and perform **none** of it: no403> `git commit`, no `git push`, no `gh pr create` / `gh pr edit`, no Linear transition404> (pass `--dry-run` down to `linear-sync` so `save_issue` is never called), and405> `--dry-run` on the Step 11 hand-off. A dry run may **read** — `gh pr view`, the406> triage-pr preview — but it never writes. Then exit 0.407408If a `changelog/` entry was written in Step 7 (i.e. `changelog` is not `false`), commit409only that file:410411```bash412git add changelog/<YYYYMMDD-HHMMSS>-<slug>.md413git commit -m "docs(changelog): <one-line summary>"414```415416Then push the branch:417418```bash419git push -u origin <branch>420```421422### Step 9: Create or update the PR423424`<title>` is the Conventional Commits PR title from Step 6 — set it on **both**425create and update (re-derive it every run so it stays in sync with the branch's426commits). Feature PRs are intended to merge via **merge commit**; release and427fan-out automation keep using squash outside this skill.4284291. Check for an existing PR: `gh pr view --json number,url 2>/dev/null`.4302. **If creating:** `gh pr create --base <base> --draft --title "<title>" --body431 "<body>"`. Use `--ready` (the flag) instead of `--draft` if the user passed432 `--ready`.4333. **If updating:** `gh pr edit <number> --title "<title>" --body "<body>"`.4344. Return the PR URL and number via `gh pr view --json url,number`.435436> **send-it never arms auto-merge.** It opens and updates the PR; landing it stays a437> human action (A-1151). The old `--merge-when-ready` flag — which armed438> `gh pr merge --auto --merge` here — is **gone as of 0.8.0**: from Step 11 onward, a439> run can be sitting at triage-pr's disposition envelope, and an armed auto-merge440> could land the branch while that plan is still awaiting approval. Merge by hand, or441> arm auto-merge yourself once you're happy with the PR.442443**PR body template:**444445```markdown446## Summary447448- Comprehensive summary of all changes on this branch449- What changed and why450451## Related Issues452453<!-- Linear identifiers extracted from the branch and commits -->454- <ISSUE-ID>455456## Test Plan457458- [ ] <test>459```460461Drop the `## Related Issues` section if no issues were found.462463### Step 10: Transition linked Linear issues to In Review — delegate to the `linear-sync` skill464465Follow the [`linear-sync`](../linear-sync/SKILL.md) skill with target state **In466Review**: read its `config.json` for `linearTeamName` and `issueKeys`, extract issue467IDs from the branch and commits, resolve the live state ID by team **name** (once),468and apply the transition idempotently (skip any issue already at or past In Review).469Skip silently if `linear-sync` or the Linear MCP server is unavailable.470471### Step 11: Drive the PR to merge-ready — delegate to the `triage-pr` skill472473> **Completion gate.** Do **not** treat the Step 9 PR URL as the final report and474> stop. Steps 9–10 are mid-pipeline. Continue into this step unless the opt-out in475> sub-step 1 applies, or sub-step 2 finds `triage-pr` missing (and you print the476> corresponding skip/degraded line with a reason). A draft-only report without477> `ℹ️ triage chain skipped …` or `⚠️ triage-pr not installed …` is a failed478> `/send-it` run (A-1645).479480send-it opens the PR; [`triage-pr`](../triage-pr/SKILL.md) takes it the rest of the481way (A-1151). **This step is part of the run — not an optional extra.** One482`/send-it` drives the whole pipeline: Phase A fixes in-scope CI failures and promotes483the proven-green draft to ready, then Phase B waits for the AI reviewers, verifies484every finding, and halts at its human envelope. This step runs **after** Step 10 so485the linked issues are already In Review before triage begins.4864871. **Check the opt-out first — before anything else in this step.** If `--skip-triage`488 was passed, or `config.json` sets `triage: false`, print489 `ℹ️ triage chain skipped (--skip-triage): <reason>` — or `(triage: false)` — report490 the PR URL, and **stop the run here**. Do not run the install check, and do not491 start the cold-start poll: a skipped chain must cost nothing. That is the pre-0.8.0492 behaviour.493494 > **Don't reach for it to finish sooner.** The opt-out exists for the cases where495 > the chain genuinely cannot work, not as a shortcut, and it is **never** the496 > default: `triage: true` ships in `config.example.json`, and `initialise-skills`497 > writes `true` when reconciling a consumer. Skipping leaves the PR un-triaged —498 > red CI unfixed, bot findings unread — which is the state this step exists to499 > prevent, so treat it the way Step 5 treats `--skip-preflight`: say **why** in the500 > report. The legitimate reasons are narrow: the PR changes the chain itself, so501 > the running prose and the prose on disk disagree (this bundle's own ship runs);502 > CI is gated on `draft == false`, so a draft never registers a check (prefer503 > `triage: false` in that repo's config over a per-run flag); or the user asked to504 > stop at the open PR. A missing `triage-pr` needs no flag — sub-step 2 handles it.5055062. **Confirm `triage-pr` is installed** — look for `../triage-pr/SKILL.md` beside this507 bundle. If it is absent, print508509 ```text510 ⚠️ triage-pr not installed — stopping at the open PR.511 Install it to chain: npx skills add <repo> --skill triage-pr --agent claude-code --copy512 ```513514 and finish the run. That warning **is** the degraded-outcome line the completion515 gate accepts (alongside `ℹ️ triage chain skipped …`) — the run may stop, but it516 must not report as a successful default finish. A missing sibling **warns, never517 fails the process exit** — louder than Step 10's silent `linear-sync` skip,518 because a skipped triage chain leaves PR work undone.5195203. **Wait for CI to register — the cold-start gate.** Step 9 created or updated the PR521 moments ago, so GitHub Actions may not have registered a single check yet. An empty522 `statusCheckRollup` handed to a cold `triage-pr` reads as "nothing failing", and with523 `promoteOnGreen` on (its default) that would flip the draft to ready **before CI ever524 ran**. triage-pr's "no failures yet is not green" rule guards its own watch loop, not525 a cold entry — so send-it proves at least one check exists before handing off. Poll526 every **10 seconds** for up to **3 minutes**, in a **single** shell loop (not 18527 separate calls — a foreground `sleep` between tool calls is slow and some harnesses528 refuse it). Stay quiet while polling; no interim "still waiting" pings:529530 Capture `gh`'s exit status separately from the count — a failed call returns an531 empty string, and treating that as "zero checks" would silently convert an auth or532 API error into a full-window wait and a bogus "CI never started" verdict:533534 ```bash535 for _ in $(seq 1 18); do536 if ! checks=$(gh pr view <number> --json statusCheckRollup --jq '[.statusCheckRollup[]?] | length'); then537 echo "gh pr view failed — cannot verify CI has started" >&2538 exit 1539 fi540 [ "$checks" -gt 0 ] && break541 sleep 10542 done543 ```544545 - **At least one check registered** → continue to sub-step 4.546 - **`gh` itself fails** → stop and surface the error (authentication, rate limit, a547 deleted PR). Do **not** fall through to the no-checks branch: an unverifiable548 state is not the same as a verified-empty one, and only the latter is safe to549 hand off.550 - **Still `0` when the window expires** → CI never started for this PR (a repo with551 no workflows, a `paths`-filtered or `draft == false`-gated workflow this PR doesn't552 match, or a stalled Actions queue). Report553 `⚠️ no checks registered within 3 minutes — handing off with --no-promote` and add554 `--no-promote` to the hand-off below, so an empty rollup can never be read as a555 proven green and flip the draft to ready. Nothing else about the chain changes.5565574. **Hand off.** **If send-it was run with `--dry-run`, `--dry-run` goes on this558 command too — always.** A live `triage-pr` commits, pushes, and can flip the draft559 to ready, so a dry run that omits it stops being a dry run. Follow the560 [`triage-pr`](../triage-pr/SKILL.md) skill against the PR from Step 9, naming its561 number explicitly so it never re-resolves to a different PR:562563 ```text564 triage-pr <number> [--dry-run] [--ci-only] [--no-promote] [--auto-apply]565 ```566567 Forward `--dry-run`, `--ci-only`, `--no-promote`, and `--auto-apply` **verbatim**568 when they were passed to send-it, plus `--no-promote` when sub-step 3's cold-start569 gate added it — that one is a safety flag this step owns, not a user flag, and570 dropping it would let an unverified rollup promote a draft. Add nothing beyond571 those. `triage-pr` reads its own572 `config.json` (`promoteOnGreen`, `humanEnvelope`, `reviewBots`, `maxCiRounds`, …) —573 send-it configures nothing about it, exactly as it configures nothing about574 `commit`, `preflight`, `changelog`, or `linear-sync`.5755765. **Run the full chain.** Don't stop between phases: Phase A's fix→push→watch loop,577 the promotion gate, then Phase B's review wait and verify-then-propose. Halt where578 `triage-pr` halts — its human envelope, its slow-bot micro-gate, a hard blocker, or579 `maxCiRounds` exhaustion. The envelope **is** the run's natural stopping point: don't580 answer it on the user's behalf, and don't print a send-it "all done" over the top of581 it.5825836. **Report once.** `triage-pr`'s own final report is the run's report — prepend584 send-it's line items (branch, PR URL, changelog entry, Linear transitions) to it585 rather than emitting a second, competing summary. Respect triage-pr's quiet rule586 (A-1178): no interim pings around the hand-off.587588> **`--dry-run` chains into `triage-pr --dry-run`.** When a PR already exists for the589> branch, hand off to `triage-pr <number> --dry-run` so the preview covers the failing590> checks and unresolved findings too. When no PR exists — a dry run creates none —591> print `no PR to triage yet` and exit 0. A dry run therefore makes **read-only** `gh`592> calls; it still writes nothing, commits nothing, and pushes nothing.593>594> **Re-runs are safe.** A second `/send-it` re-enters the chain against the same PR.595> `triage-pr` re-fetches threads every pass: resolved threads are filtered out, and596> proposed follow-up threads already carry the non-resolving `follow-up-pending` marker (A-679),597> so they arrive as `deferredThreads`, not fresh findings. The envelope therefore598> re-prompts only for genuinely new bot findings.599600## Flags601602- `--dry-run` — print what would be written/submitted (changelog preview, branch,603 conventional PR title, any version-bump proposals), make no commits and no push. It604 chains into `triage-pr --dry-run` when a PR already exists for the branch (Step 11),605 so it makes **read-only** `gh` calls but still writes nothing. Exit 0.606- `--branch=<name>` — override the auto-derived branch name when running on the base607 branch with uncommitted changes.608- `--issue=<ID>` — prefix the auto-derived slug with a Linear issue ID (e.g.609 `--issue=A-7` → `a-7-<slug>`, lower-cased). Ignored if `--branch` is given.610- `--base=<branch>` — override `config.json`'s `baseBranch` for this run. Applies611 everywhere the base is used: the `git fetch`, the branch diff612 (`origin/<base>...HEAD`), the PR `--base`, and the `BASE_REF=origin/<branch>` env613 passed to `derive-bump.mjs` / `check-skill-bumps.mjs`. Use it for stacked PRs or a614 non-`main` target.615- `--title="<conventional subject>"` — set the PR title verbatim instead of deriving616 it (escape hatch for when derivation picks the wrong type). It must still be a valid617 Conventional Commits subject (CI lints it). `derive-bump` still runs (its `category`618 drives the changelog entry); send-it **warns** if the supplied type contradicts the619 derived `type`/`releaseTriggering`.620- `--skip-preflight` — skip the Step 5 lint gate entirely, printing a bypass warning.621- `--skip-triage` — end the run at the open PR: skip the Step 11 `triage-pr` chain622 (identical to `config.json` `triage: false`). Restores the pre-0.8.0623 bounded-finisher behaviour for one run. **Not a shortcut** — it leaves the PR624 un-triaged; see Step 11 for the narrow cases where it applies, and state the reason625 in the report.626- `--ci-only` — forwarded verbatim to `triage-pr` (Step 11): run its Phase A and stop627 at green, never promoting the draft. **No effect on send-it's own steps.**628- `--no-promote` — forwarded verbatim to `triage-pr`: never flip the draft to ready;629 stop at green. send-it also adds this itself when the cold-start gate times out.630 **No effect on send-it's own steps.** (`--promote` is deliberately _not_ forwarded —631 promotion is already triage-pr's default.)632- `--auto-apply` — forwarded verbatim to `triage-pr`: skip its Phase B human envelope633 and restore its legacy auto path (impact-gated fix-now; Linear-only gate for634 follow-ups). **No effect on send-it's own steps.**635- `--ready` — open the PR ready-for-review instead of draft (default is draft).636- `--worktree=<branch-or-path>` — `cd` into a worktree before running (Step 0).637638`--merge-when-ready` was **removed in 0.8.0**. send-it no longer arms639`gh pr merge --auto --merge`; see the Step 9 callout.640641## Notes642643- **Prose follows the host repo's language convention.** Author the PR title, PR644 body, and commit messages in the consuming repo's documented prose language. Across645 this estate that is **British English** (`colour`, `behaviour`, `-ise`/`-yse`); the646 `changelog` skill applies the same rule to the entry it writes. This governs prose647 only — never identifiers, dependency names, or upstream API field names.648- **Trunk-based:** PRs target the base branch (`config.json` `baseBranch`, or649 `--base` for this run).650- **send-it bumps only per-bundle versions, never the repo version.** The optional651 Step 6 bundle-version check moves a changed skill's own `metadata.version`; the652 repo-level npm release stays owned by release-please (feature PRs: landed commit653 subjects; squash paths: squash subject / PR title).654- **send-it drives the pipeline now, not just the open PR (A-1151 / A-1645).** Through655 0.7.0 it was a bounded finisher: seconds of work, ending in a report and an open656 PR. From 0.8.0 the default run continues into `triage-pr` (Step 11), so a single657 `/send-it` can stay unattended for roughly 30 minutes — CI fix rounds plus the658 review wait — and ends on a **prompt** (triage-pr's disposition envelope), not a659 report. That is a deliberate shift in what the command is. `--skip-triage`, or660 `triage: false`, restores the old shape — only with a stated reason.661- **send-it never merges, and never arms auto-merge.** Taking the PR through triage662 to green and ready-for-review is the end of its remit; landing it is a human663 action. Do not conflate "leave the merge to the human" with "stop once the draft664 exists".665- **CI gated on non-draft PRs makes the chain a tax.** send-it opens drafts by666 default, so a repo whose workflows carry `if: github.event.pull_request.draft ==667 false` registers zero checks until the PR is ready — the Step 11 cold-start gate668 then waits its full 3 minutes every run and hands off with `--no-promote` to a669 triage-pr with nothing to do. Use `--ready`, or set `triage: false`, in those repos.670- **Idempotent:** re-running send-it updates the existing PR title and changelog671 entry; the Linear writeback skips issues already In Review or beyond; the Step 11672 chain re-enters `triage-pr` against the same PR, whose `follow-up-pending` markers673 (A-679) keep already-dispositioned findings out of the envelope.674- **send-it does not bump versions or write any `CHANGELOG.md`.** release-please675 ranks Conventional Commits on trunk after merge (merge-commit history for feature676 PRs; squash subject for release/fan-out), bumps the manifest in the release PR, and677 the release workflow publishes + tags. send-it only writes the dated678 `changelog/<ts>-<slug>.md` entry (Step 7), finalised post-merge by the in-repo enricher.679680## Error Handling681682- **`gh auth status` fails** — run `gh auth login` first; abort until authenticated.683- **changelog validation fails** — surface the error; don't auto-fix. The user684 resolves the entry and re-runs.685- **No commits ahead of the base** — exit "No commits ahead of the base branch.686 Nothing to ship."687- **Branch push fails** — verify push access; ensure the remote is configured.688- **PR create/update fails** — verify the PR isn't closed; verify the branch is689 pushed.690- **`triage-pr` isn't installed** — warn and finish at the open PR (Step 11). A691 missing sibling never fails a send-it run.692- **No checks register within the cold-start window** — hand off with `--no-promote`693 and say why. An empty `statusCheckRollup` must never be read as green.694- **The triage chain fails, is aborted, or the envelope is declined** — the commits,695 changelog entry, PR, and Linear transitions from Steps 3–10 all stand. Re-run696 `/triage-pr <number>` directly rather than re-running the whole of send-it.