send-it
Bundle uncommitted work into atomic commits (via the
skill), run the change-gated lint
, author or update the dated
entry (via the
skill), compose a
Conventional Commits PR title (the squash subject
release-please reads to decide the version bump), push the branch, open or update
a pull request against the base branch, and transition any linked Linear issues to
In Review (via the
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 skill (classify in-scope vs out-of-scope, atomic
Conventional Commits, out-of-scope guard).
- Lint gate → the skill (change-gated; no-ops when nothing
lint-relevant changed).
- Changelog → the skill (author/update + validate; an entry for
every PR, skipped entirely only when sets ).
- Linear In Review → the skill (resolve state by team name,
idempotent transition).
The delegated skills auto-detect their own scope, so monorepo features
(per-workspace ESLint fan-out, changelog
) no-op cleanly in a
single-package repo. send-it configures nothing about them.
Install the delegated skills alongside . This bundle invokes and
links its siblings by relative path (
,
,
,
), so a
-only
install leaves the commit, lint, changelog, and Linear steps unavailable and those
links dangling. Install them together:
bash
npx 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
This flow intentionally does
not run typecheck, tests, or format checks — CI
handles those. The only gate it runs is the change-gated
lint.
Configuration
A few knobs live in
beside this file; edit your
copied
to match the consuming repo (a neutral
ships as a template):
| Key | Meaning | Default |
|---|
| The trunk the branch diff is taken against () and the PR base. | |
| (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. | |
| (advisory) | keys that form the published- surface — same advisory role as , no longer a release gate. | ["name", "version", "files", "publishConfig"]
|
| (optional) | Whether to author a dated entry at all (Steps 7–8). Set for repos with no changelog flow — no directory and no skill installed (e.g. a repo with no release pipeline). When , send-it skips changelog authoring entirely, and the category decision continues to drive only the PR title. Omit it (or set ) whenever the skill is installed. | |
| (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) |
The team name, issue-ID prefixes, and workspace slug are
not configured here —
they live in the
and
skills' own
files,
read by the delegated steps.
Changelog 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. The
knob (added in 0.4.0) is
gone (A-600); only the
master switch remains.
Prerequisites
- CLI installed and authenticated ().
- The sibling skills (, , , ) installed.
Process
Step 0: Worktree resolution (only if is set)
If
--worktree=<branch-or-path>
was passed, resolve and
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
field.
- Otherwise: treat as a branch name and match against the
field.
-
No match — exit immediately with:
No worktree found for <arg>. Available: <comma-separated paths>
.
-
Match —
into the resolved worktree path. The
persists for the
rest of the workflow, so all subsequent
and
calls operate on the
worktree.
-
Ensure dependencies are present. A freshly-created worktree has no
. If it is absent, run
pnpm install --frozen-lockfile
now —
before any step that invokes a bundled script or a validator — so
is self-sufficient:
bash
[ -d node_modules ] || pnpm install --frozen-lockfile
-
Continue to Step 1.
This step does nothing when
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 ( from ; default ):
- Run . If clean, exit with: "Nothing to ship from the
base branch. Create a feature branch first."
- If there are uncommitted changes:
- Inspect the diff ( and ) and the changed file
paths.
- Derive a short kebab-case slug summarising the change (~3 words, lowercase,
max ~40 chars). Examples: , .
- Branch name resolution (in order):
- — use as-is.
- — use lower-cased (e.g.
), matching Linear's .
- Otherwise — just (no prefix).
- If the chosen branch already exists locally or on , append ,
, … until unused.
- Run to move the working tree onto it.
- Inform the user: "Was on the base branch with uncommitted changes; created
and continuing."
- Continue with the rest of the workflow on the new branch.
- If on a feature branch: continue.
Step 2: Refresh lockfile if drifted
Skip this step if no
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
to update the lockfile.
-
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"
This keeps CI's
install green. (Skip silently in repos that
don't use pnpm.)
Step 3: Commit uncommitted changes — delegate to the skill
send-it is the all-in-one finisher: whatever's uncommitted should be committed
before the changelog/PR work begins — but only what belongs to this branch.
Follow the
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
; stray files from another branch/worktree are never staged silently), and
create
logical atomic Conventional Commits (type + optional scope +
British-English body;
/
for breaking changes). If clean,
skip this step. Direct the
skill to classify against
this send-it
run's resolved base —
is
(from
), or
when passed —
not the
skill's own
, which
differs on a
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 (
reads them back out of the commits), so the honest
types and
/
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
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 skill
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
skill:
bash
node skills/preflight/scripts/preflight.mjs
Act on its exit-code contract, reading
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 /
commit, or fold into the relevant Step 3 commit if not yet pushed) before
continuing.
- Exit 1 with and 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 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
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 repo squash-merges, so the
squash subject is
the PR title — and that single conventional title is what release-please parses to
decide the bump. send-it composes a correct conventional title and writes the dated
changelog entry (for every PR — see Step 7). It does
not bump versions, write any
, 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
(no release) even when it lives under a published
path like
; a
is a release wherever its files sit. (Earlier versions
keyed this off
, which mis-titled a docs edit inside a published path
as
/
and cut a spurious release.)
-
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.mjs
It prints JSON:
{ "slug", "bump", "body", "type", "breaking", "category", "releaseTriggering" }
:
- — the Conventional-Commit type of the lead commit (///
////…); this is the PR-title prefix.
- — if any commit carries a or a trailer.
- — the dated changelog enum value (→,
→, →, →, →, everything else
→).
- — iff or . This is
the release decision: cuts a release, does not.
- — //, the release magnitude when
(a / → major; lead → minor; else patch). Ignored when
is .
-
(Advisory) publish-surface cross-check. /
in
are a documentation hint of
the published surface — they
do not decide release-type any more. Optionally
sanity-check the category against them: if
is
but the diff
(
git diff --name-only origin/<base>...HEAD
) touches
no prefix
(nor a
key in
), 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
. This is a soft note only; never let it
override the category decision or block.
-
Check per-bundle version bumps — only when
sets
(multi-artefact repos; skip this step entirely when it's
unset). Each skill bundle carries its own version in its
+
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:
bash
node skills/send-it/scripts/check-skill-bumps.mjs
It prints
{ "configured", "unbumped": [{ name, currentVersion, suggestedBump, suggestedVersion, manifestPath, skillPath }], "bumped" }
. For
each
entry, surface the proposal and apply it on confirmation:
changed but its version is still
. Suggested
bump:
→
(matches the PR-title bump).
Apply? (yes / no / patch / minor / major)
On
(or an explicit level), edit
both (
) and
(
) 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
, leave
it and continue. Under
, print the proposal and edit nothing.
-
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. If
was
passed, use it verbatim (still run
above for the changelog
, and
warn — don't block — if the supplied type contradicts the
derived
/
). Otherwise build it straight from the derived
fields:
- Prefix = (add a scope when one is obvious, e.g. ), plus
when — so , , ,
, , , , etc.
- Release-triggering () → the prefix is already a
release type (//, or any ); release-please cuts the bump from
it. Add the scope; that's it.
- Non-release () → the prefix is a non-release type
(//////); release-please cuts
nothing.
⚠️
The PR title is the version. A mistyped prefix silently ships the wrong
semver — a
on a docs PR cuts a needless release; a
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.
When
is
, note
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 skill
Disabled entirely? If
sets
,
skip Steps 7
and 8 completely — author nothing, run no
scripts, make no
commit — and note "changelog step disabled (no changelog flow in
this repo)" in the run summary. This is for repos with no
directory and
no
skill installed; the category decision from Step 6 still drives the PR
title. When
is unset or
,
always author an entry (the
knob was removed — A-600).
An entry for every PR. send-it authors a dated
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
.
is the only thing that suppresses
authoring.
Follow the
skill to author or update the entry:
-
Detect an existing entry for this branch (by the
frontmatter field) →
update vs create. On update, preserve the filename and
.
-
Write/refresh
changelog/<YYYYMMDD-HHMMSS>-<slug>.md
(the
from Step 6),
deriving
/
/
from the branch. Set
and
straight from
's output (Step 6):
is its
field (
/
/
/
/
/
— the changelog
enum), and
is its
flag. For a non-release entry
(
),
may be blank when there's no
user-facing impact.
Leave the post-merge fields (
,
,
,
,
)
and
as blank placeholders — the release step finalises them (a non-release
entry keeps
blank, as no release is cut for it). This includes
: no
step here writes it back after the PR opens; the release/enrich step resolves it
post-merge from the entry's
.
-
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
If a
entry was written in Step 7 (i.e.
is not
), commit
only that file:
bash
git add changelog/<YYYYMMDD-HHMMSS>-<slug>.md
git commit -m "docs(changelog): <one-line summary>"
Then push the branch:
bash
git push -u origin <branch>
Step 9: Create or update the PR
is the Conventional Commits PR title from Step 6 — release-please reads it
as the squash subject, so set it on
both create and update (re-derive it every
run so it stays in sync with the branch's commits).
- 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 (the flag) instead of if the user passed
.
- If updating:
gh pr edit <number> --title "<title>" --body "<body>"
.
- If was passed: after create/update, run
gh pr merge --auto --squash <number>
to enable auto-merge once requirements are met.
- Return the PR URL via
gh pr view --json url -q '.url'
.
PR body template:
markdown
## 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
section if no issues were found.
Step 10: Transition linked Linear issues to In Review — delegate to the skill
Follow the
skill with target state
In
Review: read its
for
and
, 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
or the Linear MCP server is unavailable.
Flags
- — print what would be written/submitted (changelog preview, branch,
conventional PR title, any version-bump proposals), make no commits, no push, no
calls. Exit 0.
- — override the auto-derived branch name when running on the base
branch with uncommitted changes.
- — prefix the auto-derived slug with a Linear issue ID (e.g.
→ , lower-cased). Ignored if is given.
- — override 's for this run. Applies
everywhere the base is used: the , the branch diff
(), the PR , and the env
passed to / . Use it for stacked PRs or a
non- 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). still runs (its
drives the changelog entry); send-it warns if the supplied type contradicts the
derived /.
- — skip the Step 5 lint gate entirely, printing a bypass warning.
- — open the PR ready-for-review instead of draft (default is draft).
- — after create/update, enable
gh pr merge --auto --squash
.
--worktree=<branch-or-path>
— into a worktree before running (Step 0).
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 (, , /); the
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 ( , or
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 ; the
repo-level npm release stays owned by release-please via the PR title.
- 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
entry (Step 7), finalised at release.
Error Handling
- fails — run 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.