send-it
The all-in-one ship finisher — bundle uncommitted work into atomic commits, run the change-gated lint preflight, author or update the dated changelog entry, compose a Conventional Commits PR title (the release-please bump signal), push, open or update a PR, and transition linked Linear issues to In Review. Use when asked to ship, send it, finish a branch, open or update a PR for the current work, or wrap up and push. A thin orchestrator that delegates the commit step to the `commit` skill, the lint gate to the `preflight` skill, the changelog to the `changelog` skill, and the Linear writeback to the `linear-sync` skill; it owns the branch guard, the release-type decision (by the change's semantic category), PR-title composition, push, and PR. One skill serves monorepos and single-package repos alike.
NPX Install
npx skill4agent add acme-skunkworks/agent-skills send-itTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →send-it
commitpreflightchangelog/<ts>-<slug>.mdchangeloglinear-sync- Commit → the skill (classify in-scope vs out-of-scope, atomic Conventional Commits, out-of-scope guard).
commit - Lint gate → the skill (change-gated; no-ops when nothing lint-relevant changed).
preflight - Changelog → the skill (author/update + validate; an entry for every PR, skipped entirely only when
changelogsetsconfig.json).changelog: false - Linear In Review → the skill (resolve state by team name, idempotent transition).
linear-sync
affected_packagesInstall the delegated skills alongside. This bundle invokes and links its siblings by relative path (send-it,../commit/SKILL.md,../preflight/SKILL.md,../changelog/SKILL.md), so a../linear-sync/SKILL.md-only install leaves the commit, lint, changelog, and Linear steps unavailable and those links dangling. Install them together:--skill send-itbashnpx skills add https://github.com/acme-skunkworks/agent-skills \ --skill send-it --skill commit --skill preflight --skill changelog --skill linear-sync \ --agent claude-code --agent cursor --copy
preflightConfiguration
config.jsonconfig.jsonconfig.example.json| Key | Meaning | Default |
|---|---|---|
| The trunk the branch diff is taken against ( | |
| 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. | |
| | |
| Whether to author a dated | |
| Enables the per-bundle version-bump check (Step 6) for repos that ship many independently-versioned skill bundles. An object | unset (disabled) |
linear-syncchangelogconfig.jsonChangelog scope (was). 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 thechangelogScopemaster switch remains.changelog: true|false
Prerequisites
- CLI installed and authenticated (
gh).gh auth status - The sibling skills (,
commit,preflight,changelog) installed.linear-sync
Process
Step 0: Worktree resolution (only if --worktree=
is set)
--worktree=--worktree=<branch-or-path>cd-
Runto list worktrees with their paths and branches.
git worktree list --porcelain -
Resolve the argument:
- Absolute path (starts with ): match against the
/field.worktree <path> - Otherwise: treat as a branch name and match against the
field.
branch refs/heads/<name>
- Absolute path (starts with
-
No match — exit immediately with:.
No worktree found for <arg>. Available: <comma-separated paths> -
Match —into the resolved worktree path. The
cdpersists for the rest of the workflow, so all subsequentcwdandgitcalls operate on the worktree.gh -
Ensure dependencies are present. A freshly-created worktree has no. If it is absent, run
node_modulesnow — before any step that invokes a bundled script or a validator — sopnpm install --frozen-lockfileis self-sufficient:--worktreebash[ -d node_modules ] || pnpm install --frozen-lockfile -
Continue to Step 1.
--worktreeStep 1: Branch guard
- Get the current branch: .
git branch --show-current - If on the base branch (from
baseBranch; defaultconfig.json):main- Run . If clean, exit with: "Nothing to ship from the base branch. Create a feature branch first."
git status --porcelain - If there are uncommitted changes:
- Inspect the diff (and
git diff) and the changed file paths.git diff --cached - 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):
- — use as-is.
--branch=<name> - — use
--issue=<ID>lower-cased (e.g.<ID>-<slug>), matching Linear'sa-7-as-acquired.gitBranchName - Otherwise — just (no
<slug>prefix).wip/
- If the chosen branch already exists locally or on , append
origin,-2, … until unused.-3 - Run to move the working tree onto it.
git checkout -b <branch> - Inform the user: "Was on the base branch with uncommitted changes; created
and continuing."
<branch>
- 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
package.jsonpackage.json-
. If empty, skip.
git diff --name-only origin/<base>...HEAD | grep -E '(^|/)package\.json$' -
Run. If it succeeds, the lockfile is already in sync — continue.
pnpm install --frozen-lockfile -
If it fails, runto update the lockfile.
pnpm install -
If the lockfile changed, stage and commit it before any other commits go in:bash
git add pnpm-lock.yaml git commit -m "chore: update lockfile"
--frozen-lockfileStep 3: Commit uncommitted changes — delegate to the commit
skill
commitcommitgit merge-base HEAD origin/<base>git add -A!BREAKING CHANGE:commit<base>baseBranchconfig.json--basecommitconfig.jsonbaseBranch--basederive-bump.mjs!BREAKING CHANGE:Step 4: Fetch the base branch and confirm there's something to ship
git fetch origin <base>git log origin/<base>..HEADStep 5: Lint gate — delegate to the preflight
skill
preflightbypasses this whole step. Print a clear--skip-preflightwarning and jump to Step 6. Use it only when the gate misfires; CI still runs the repo's real linting.⚠️ lint gate bypassed (--skip-preflight)
preflightnode skills/preflight/scripts/preflight.mjs.preflight-summary.json- Exit 0 — pass. No introduced violations; continue.
- Exit 1 with — introduced violations (blocking). Run
violations.introducedCount > 0, re-run preflight, and repeat until introduced violations clear. Commit the fixes (anode skills/preflight/scripts/lint-fix.mjs/style:commit, or fold into the relevant Step 3 commit if not yet pushed) before continuing.fix: - Exit 1 with and
introducedCount == 0non-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 thatresults.failedLinterswas unavailable and continue. The repo's own CI owns whatever linting it actually runs.<linter> - 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).
preflightStep 6: Decide release-type by category and compose the Conventional Commits PR title
CHANGELOG.mddocs:skills/feat:shippablePathsfeat:fix:-
Derive the slug, body, type, and category from the branch commits via the bundled helper (zero-dep — no tsx):bash
node skills/send-it/scripts/derive-bump.mjsIt prints JSON::{ "slug", "bump", "body", "type", "breaking", "category", "releaseTriggering" }- — the Conventional-Commit type of the lead commit (
type/feat/fix/perf/docs/refactor/chore/…); this is the PR-title prefix.ci - —
breakingif any commit carries atrueor a!trailer.BREAKING CHANGE: - — the dated changelog
categoryenum value (category→feat,feature→fix,fix→perf,perf→docs,docs→refactor, everything else →refactor).chore - —
releaseTriggeringifftrueorbreaking. This is the release decision:type ∈ {feat, fix, perf}cuts a release,truedoes not.false - —
bump/major/minor, the release magnitude whenpatch(areleaseTriggering/BREAKING CHANGE:→ major; lead!→ minor; else patch). Ignored whenfeat:isreleaseTriggering.false
-
(Advisory) publish-surface cross-check./
shippablePathsinshippableManifestKeysare a documentation hint of the published surface — they do not decide release-type any more. Optionally sanity-check the category against them: ifconfig.jsonisreleaseTriggeringbut the diff (true) touches nogit diff --name-only origin/<base>...HEADprefix (nor ashippablePathskey inshippableManifestKeys), note it in the PR body so a reviewer can confirm the release was intended — and likewise if a change touching a published path ispackage.json. This is a soft note only; never let it override the category decision or block.releaseTriggering: false -
Check per-bundle version bumps — only whensets
config.json(multi-artefact repos; skip this step entirely when it's unset). Each skill bundle carries its own version in itsbundleVersioning+package.json, 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:SKILL.md metadata.versionbashnode skills/send-it/scripts/check-skill-bumps.mjsIt prints. For each{ "configured", "unbumped": [{ name, currentVersion, suggestedBump, suggestedVersion, manifestPath, skillPath }], "bumped" }entry, surface the proposal and apply it on confirmation:unbumpedchanged but its version is stillskills/<name>. Suggested bump:<currentVersion>→<suggestedBump>(matches the PR-title bump). Apply? (yes / no / patch / minor / major)<suggestedVersion>On(or an explicit level), edit bothyes(manifestPath) andversion(skillPath) to the chosen version — in lockstep, so the parity invariant CI checks still holds — then stage and commit just those two files:metadata.version. Ongit commit -m "chore(<name>): release <name>@<version>", leave it and continue. Underno, print the proposal and edit nothing.--dry-run -
Compose the PR title as a single Conventional Commits subject — this is the release-please bump signal and is enforced by CI's PR-title lint. Ifwas passed, use it verbatim (still run
--titleabove for the changelogderive-bump, and warn — don't block — if the supplied type contradicts the derivedcategory/type). Otherwise build it straight from the derived fields:releaseTriggering- Prefix = (add a scope when one is obvious, e.g.
type), plusfeat(<scope>):when!— sobreaking,feat: <body>,fix: <body>,perf: <body>,docs: <body>,refactor: <body>,chore: <body>, etc.feat!: <body> - Release-triggering () → the prefix is already a release type (
releaseTriggering: true/feat/fix, or anyperf); release-please cuts the bump from it. Add the scope; that's it.! - Non-release () → the prefix is a non-release type (
releaseTriggering: false/docs/refactor/chore/ci/build/test); release-please cuts nothing.style
⚠️ The PR title is the version. A mistyped prefix silently ships the wrong semver — aon a docs PR cuts a needless release; afeat:on a real fix ships nothing. There is no changeset file to cross-check against: the title is the declaration. It comes straight from the change's semantic category (the commit types) — keep the commit types honest and the title follows.chore:WhenisreleaseTriggering, notefalsein the PR body so reviewers can confirm the non-release type was intentional.no release (<type>-only) - Prefix =
Step 7: Author or update the dated changelog entry — delegate to the changelog
skill
changelogDisabled entirely? Ifsetsconfig.json, skip Steps 7 and 8 completely — author nothing, run nochangelog: falsescripts, make nochangelogcommit — and note "changelog step disabled (no changelog flow in this repo)" in the run summary. This is for repos with nodocs(changelog)directory and nochangelog/skill installed; the category decision from Step 6 still drives the PR title. Whenchangelogis unset orchangelog, always author an entry (thetrueknob was removed — A-600).changelogScopeAn entry for every PR. send-it authors a datedentry 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 nochangelog/.versionis the only thing that suppresses authoring.changelog: false
changelog-
Detect an existing entry for this branch (by thefrontmatter field) → update vs create. On update, preserve the filename and
branch.created_at -
Write/refresh(the
changelog/<YYYYMMDD-HHMMSS>-<slug>.mdfrom Step 6), deriving<slug>/title/release_notefrom the branch. Setissuesandcategorystraight frombreaking's output (Step 6):derive-bumpis itscategoryfield (category/feature/fix/perf/docs/refactor— the changelog enum), andchoreis itsbreakingflag. For a non-release entry (breaking),releaseTriggering: falsemay be blank when there's no user-facing impact.release_noteLeave the post-merge fields (,merged_at,commit,pr,merge_strategy) andstatsas blank placeholders — the release step finalises them (a non-release entry keepsversionblank, as no release is cut for it). This includesversion: no step here writes it back after the PR opens; the release/enrich step resolves it post-merge from the entry'spr.branch: -
Run the enrichment scripts:then
node skills/changelog/scripts/set-affected-packages.mjs.node skills/changelog/scripts/add-links.mjs -
Validate:. It must pass before committing — if it fails, surface the error and abort; don't auto-fix.
node skills/changelog/scripts/validate-changelog.mjs
Step 8: Commit the changelog entry and push
changelog/changelogfalsegit add changelog/<YYYYMMDD-HHMMSS>-<slug>.md
git commit -m "docs(changelog): <one-line summary>"git push -u origin <branch>Step 9: Create or update the PR
<title>- Check for an existing PR: .
gh pr view --json number,url 2>/dev/null - If creating: . Use
gh pr create --base <base> --draft --title "<title>" --body "<body>"(the flag) instead of--readyif the user passed--draft.--ready - If updating: .
gh pr edit <number> --title "<title>" --body "<body>" - If was passed: after create/update, run
--merge-when-readyto enable auto-merge once requirements are met.gh pr merge --auto --squash <number> - Return the PR URL via .
gh pr view --json url -q '.url'
## 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>## Related IssuesStep 10: Transition linked Linear issues to In Review — delegate to the linear-sync
skill
linear-synclinear-syncconfig.jsonlinearTeamNameissueKeyslinear-syncFlags
- — print what would be written/submitted (changelog preview, branch, conventional PR title, any version-bump proposals), make no commits, no push, no
--dry-runcalls. Exit 0.gh - — override the auto-derived branch name when running on the base branch with uncommitted changes.
--branch=<name> - — prefix the auto-derived slug with a Linear issue ID (e.g.
--issue=<ID>→--issue=A-7, lower-cased). Ignored ifa-7-<slug>is given.--branch - — override
--base=<branch>'sconfig.jsonfor this run. Applies everywhere the base is used: thebaseBranch, the branch diff (git fetch), the PRorigin/<base>...HEAD, and the--baseenv passed toBASE_REF=origin/<branch>/derive-bump.mjs. Use it for stacked PRs or a non-check-skill-bumps.mjstarget.main - — 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).
--title="<conventional subject>"still runs (itsderive-bumpdrives the changelog entry); send-it warns if the supplied type contradicts the derivedcategory/type.releaseTriggering - — skip the Step 5 lint gate entirely, printing a bypass warning.
--skip-preflight - — open the PR ready-for-review instead of draft (default is draft).
--ready - — after create/update, enable
--merge-when-ready.gh pr merge --auto --squash - —
--worktree=<branch-or-path>into a worktree before running (Step 0).cd
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); the-yseskill applies the same rule to the entry it writes. This governs prose only — never identifiers, dependency names, or upstream API field names.changelog - Trunk-based: PRs target the base branch (
config.json, orbaseBranchfor this run).--base - send-it bumps only per-bundle versions, never the repo version. The optional
Step 6 bundle-version check moves a changed skill's own ; the repo-level npm release stays owned by release-please via the PR title.
metadata.version - Idempotent: re-running send-it updates the existing PR title and changelog entry; the Linear writeback skips issues already In Review or beyond.
- send-it does not bump versions or write any . release-please reads the merged Conventional-Commit PR title, bumps the manifest in the release PR, and the release workflow publishes + tags. send-it only writes the dated
CHANGELOG.mdentry (Step 7), finalised at release.changelog/<ts>-<slug>.md
Error Handling
- fails — run
gh auth statusfirst; abort until authenticated.gh auth login - 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.