@acme-skunkworks/agent-skills 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/package.json +2 -6
- package/skills/changelog/README.md +59 -0
- package/skills/changelog/SKILL.md +187 -0
- package/skills/changelog/config.example.json +5 -0
- package/skills/changelog/config.json +5 -0
- package/skills/changelog/package.json +31 -0
- package/skills/changelog/references/changelog-contract.md +121 -0
- package/skills/changelog/scripts/add-links.mjs +97 -0
- package/skills/changelog/scripts/lib/changelog.mjs +46 -0
- package/skills/changelog/scripts/lib/config.mjs +53 -0
- package/skills/changelog/scripts/lib/derive-packages.mjs +39 -0
- package/skills/changelog/scripts/lib/frontmatter.mjs +369 -0
- package/skills/changelog/scripts/preflight-changelog-ci.mjs +152 -0
- package/skills/changelog/scripts/set-affected-packages.mjs +99 -0
- package/skills/changelog/scripts/validate-changelog.mjs +264 -0
- package/skills/linear-sync/README.md +47 -0
- package/skills/linear-sync/SKILL.md +115 -0
- package/skills/linear-sync/config.example.json +4 -0
- package/skills/linear-sync/config.json +4 -0
- package/skills/linear-sync/package.json +31 -0
- package/skills/preflight/README.md +70 -0
- package/skills/preflight/SKILL.md +148 -0
- package/skills/preflight/config.example.json +6 -0
- package/skills/preflight/package.json +33 -0
- package/skills/preflight/scripts/classify-lint.mjs +176 -0
- package/skills/preflight/scripts/lib/diff-lines.mjs +83 -0
- package/skills/preflight/scripts/lib/paths.mjs +26 -0
- package/skills/preflight/scripts/lib/scope.mjs +530 -0
- package/skills/preflight/scripts/lint-fix.mjs +78 -0
- package/skills/preflight/scripts/preflight.mjs +416 -0
- package/skills/send-it/README.md +75 -0
- package/skills/send-it/SKILL.md +391 -0
- package/skills/send-it/config.example.json +5 -0
- package/skills/send-it/config.json +5 -0
- package/skills/send-it/package.json +33 -0
- package/skills/send-it/scripts/derive-bump.mjs +139 -0
- package/skills/triage-pr/README.md +56 -0
- package/skills/triage-pr/SKILL.md +291 -0
- package/skills/triage-pr/config.json +4 -0
- package/skills/triage-pr/package.json +32 -0
- package/skills/triage-pr/references/review-discipline.md +73 -0
- package/skills/triage-pr/scripts/review-threads.mjs +549 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: send-it
|
|
3
|
+
description: >-
|
|
4
|
+
The all-in-one ship finisher — bundle uncommitted work into atomic commits, run
|
|
5
|
+
the change-gated lint preflight, author or update the dated changelog entry,
|
|
6
|
+
compose a Conventional Commits PR title (the release-please bump signal), push,
|
|
7
|
+
open or update a PR, and transition linked Linear issues to In Review. Use when
|
|
8
|
+
asked to ship, send it, finish a branch, open or update a PR for the current
|
|
9
|
+
work, or wrap up and push. A thin orchestrator that delegates the lint gate to
|
|
10
|
+
the `preflight` skill, the changelog to the `changelog` skill, and the Linear
|
|
11
|
+
writeback to the `linear-sync` skill; it owns the branch guard, atomic commits,
|
|
12
|
+
shippability decision, PR-title composition, push, and PR. Shippable paths and
|
|
13
|
+
the published surface are read from config.json so one skill serves monorepos
|
|
14
|
+
and single-package repos alike.
|
|
15
|
+
license: MIT
|
|
16
|
+
compatibility: >-
|
|
17
|
+
Requires the `git` and `gh` CLIs (`gh` authenticated). Node.js ≥22 for the
|
|
18
|
+
bundled `derive-bump.mjs` helper (Node built-ins only — no npm dependencies, no
|
|
19
|
+
build step, no tsx). Delegates to the `preflight`, `changelog`, and
|
|
20
|
+
`linear-sync` skills — install them alongside this one. The In Review writeback
|
|
21
|
+
needs the Linear MCP server (via `linear-sync`); it is skipped if unavailable.
|
|
22
|
+
metadata:
|
|
23
|
+
version: 0.1.0
|
|
24
|
+
allowed-tools: Write, Read, Edit, Glob, Grep, Bash(git:*), Bash(gh:*), Bash(pnpm:*), Bash(node:*), mcp__linear-server__get_issue, mcp__linear-server__save_issue, mcp__linear-server__list_issue_statuses
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# send-it
|
|
28
|
+
|
|
29
|
+
Bundle uncommitted work into atomic commits, run the change-gated lint
|
|
30
|
+
[`preflight`](../preflight/SKILL.md), author or update the dated
|
|
31
|
+
`changelog/<ts>-<slug>.md` entry (via the [`changelog`](../changelog/SKILL.md)
|
|
32
|
+
skill), compose a **Conventional Commits PR title** (the squash subject
|
|
33
|
+
release-please reads to decide the version bump), push the branch, open or update
|
|
34
|
+
a pull request against the base branch, and transition any linked Linear issues to
|
|
35
|
+
**In Review** (via the [`linear-sync`](../linear-sync/SKILL.md) skill).
|
|
36
|
+
|
|
37
|
+
This skill is the single source of truth for the **ship flow**. It is a thin
|
|
38
|
+
orchestrator: it owns only the glue no sibling skill does — the branch guard,
|
|
39
|
+
worktree resolution, atomic commits, the shippability decision, PR-title
|
|
40
|
+
composition, push, and the PR — and delegates the rest:
|
|
41
|
+
|
|
42
|
+
- **Lint gate** → the `preflight` skill (change-gated; no-ops when nothing
|
|
43
|
+
lint-relevant changed).
|
|
44
|
+
- **Changelog** → the `changelog` skill (author/update + validate; gated on
|
|
45
|
+
shippability).
|
|
46
|
+
- **Linear In Review** → the `linear-sync` skill (resolve state by team name,
|
|
47
|
+
idempotent transition).
|
|
48
|
+
|
|
49
|
+
The delegated skills auto-detect their own scope, so monorepo features
|
|
50
|
+
(per-workspace ESLint fan-out, changelog `affected_packages`) no-op cleanly in a
|
|
51
|
+
single-package repo. send-it configures nothing about them.
|
|
52
|
+
|
|
53
|
+
This flow intentionally does **not** run typecheck, tests, or format checks — CI
|
|
54
|
+
handles those. The only gate it runs is the change-gated `preflight` lint.
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
Three knobs live in [`config.json`](config.json) beside this file; edit your
|
|
59
|
+
copied `config.json` to match the consuming repo (a neutral
|
|
60
|
+
[`config.example.json`](config.example.json) ships as a template):
|
|
61
|
+
|
|
62
|
+
| Key | Meaning | Default |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `baseBranch` | The trunk the branch diff is taken against (`origin/<baseBranch>`) and the PR base. | `"main"` |
|
|
65
|
+
| `shippablePaths` | Path prefixes whose changes reach consumers. A change touching any makes the PR **shippable**. | `["skills/"]` |
|
|
66
|
+
| `shippableManifestKeys` | `package.json` keys whose change is itself shippable (the published-`files` surface). | `["name", "version", "files", "publishConfig"]` |
|
|
67
|
+
|
|
68
|
+
The team name, issue-ID prefixes, and workspace slug are **not** configured here —
|
|
69
|
+
they live in the `linear-sync` and `changelog` skills' own `config.json` files,
|
|
70
|
+
read by the delegated steps.
|
|
71
|
+
|
|
72
|
+
## Prerequisites
|
|
73
|
+
|
|
74
|
+
- `gh` CLI installed and authenticated (`gh auth status`).
|
|
75
|
+
- The sibling skills (`preflight`, `changelog`, `linear-sync`) installed.
|
|
76
|
+
|
|
77
|
+
## Process
|
|
78
|
+
|
|
79
|
+
### Step 0: Worktree resolution (only if `--worktree=` is set)
|
|
80
|
+
|
|
81
|
+
If `--worktree=<branch-or-path>` was passed, resolve and `cd` into that worktree
|
|
82
|
+
before any other step runs. Skip this step otherwise.
|
|
83
|
+
|
|
84
|
+
1. Run `git worktree list --porcelain` to list worktrees with their paths and
|
|
85
|
+
branches.
|
|
86
|
+
2. Resolve the argument:
|
|
87
|
+
- **Absolute path** (starts with `/`): match against the `worktree <path>`
|
|
88
|
+
field.
|
|
89
|
+
- **Otherwise**: treat as a branch name and match against the
|
|
90
|
+
`branch refs/heads/<name>` field.
|
|
91
|
+
3. **No match** — exit immediately with: `No worktree found for <arg>. Available:
|
|
92
|
+
<comma-separated paths>`.
|
|
93
|
+
4. **Match** — `cd` into the resolved worktree path. The `cwd` persists for the
|
|
94
|
+
rest of the workflow, so all subsequent `git` and `gh` calls operate on the
|
|
95
|
+
worktree.
|
|
96
|
+
5. **Ensure dependencies are present.** A freshly-created worktree has no
|
|
97
|
+
`node_modules`. If it is absent, run `pnpm install --frozen-lockfile` now —
|
|
98
|
+
before any step that invokes a bundled script or a validator — so `--worktree`
|
|
99
|
+
is self-sufficient:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
[ -d node_modules ] || pnpm install --frozen-lockfile
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
6. Continue to Step 1.
|
|
106
|
+
|
|
107
|
+
This step does nothing when `--worktree` is omitted — no-arg send-it keeps working
|
|
108
|
+
unchanged from whatever directory the session is in.
|
|
109
|
+
|
|
110
|
+
### Step 1: Branch guard
|
|
111
|
+
|
|
112
|
+
1. Get the current branch: `git branch --show-current`.
|
|
113
|
+
2. **If on the base branch** (`baseBranch` from `config.json`; default `main`):
|
|
114
|
+
- Run `git status --porcelain`. If clean, exit with: "Nothing to ship from the
|
|
115
|
+
base branch. Create a feature branch first."
|
|
116
|
+
- If there are uncommitted changes:
|
|
117
|
+
- Inspect the diff (`git diff` and `git diff --cached`) and the changed file
|
|
118
|
+
paths.
|
|
119
|
+
- Derive a short kebab-case slug summarising the change (~3 words, lowercase,
|
|
120
|
+
max ~40 chars). Examples: `add-readme-section`, `fix-config-typo`.
|
|
121
|
+
- **Branch name resolution (in order):**
|
|
122
|
+
1. `--branch=<name>` — use as-is.
|
|
123
|
+
2. `--issue=<ID>` — use `<ID>-<slug>` **lower-cased** (e.g.
|
|
124
|
+
`asw-7-as-acquired`), matching Linear's `gitBranchName`.
|
|
125
|
+
3. Otherwise — just `<slug>` (no `wip/` prefix).
|
|
126
|
+
- If the chosen branch already exists locally or on `origin`, append `-2`,
|
|
127
|
+
`-3`, … until unused.
|
|
128
|
+
- Run `git checkout -b <branch>` to move the working tree onto it.
|
|
129
|
+
- Inform the user: "Was on the base branch with uncommitted changes; created
|
|
130
|
+
`<branch>` and continuing."
|
|
131
|
+
- Continue with the rest of the workflow on the new branch.
|
|
132
|
+
3. **If on a feature branch:** continue.
|
|
133
|
+
|
|
134
|
+
### Step 2: Refresh lockfile if `package.json` drifted
|
|
135
|
+
|
|
136
|
+
Skip this step if no `package.json` was touched on the branch.
|
|
137
|
+
|
|
138
|
+
1. `git diff --name-only origin/<base>...HEAD | grep -E '(^|/)package\.json$'`. If
|
|
139
|
+
empty, skip.
|
|
140
|
+
2. Run `pnpm install --frozen-lockfile`. If it succeeds, the lockfile is already in
|
|
141
|
+
sync — continue.
|
|
142
|
+
3. If it fails, run `pnpm install` to update the lockfile.
|
|
143
|
+
4. If the lockfile changed, stage and commit it before any other commits go in:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git add pnpm-lock.yaml
|
|
147
|
+
git commit -m "chore: update lockfile"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This keeps CI's `--frozen-lockfile` install green. (Skip silently in repos that
|
|
151
|
+
don't use pnpm.)
|
|
152
|
+
|
|
153
|
+
### Step 3: Commit uncommitted changes
|
|
154
|
+
|
|
155
|
+
send-it is the all-in-one finisher: whatever's uncommitted should be committed
|
|
156
|
+
before the changelog/PR work begins — but only what belongs to *this* branch.
|
|
157
|
+
|
|
158
|
+
1. `git status --porcelain`. If clean, skip this step.
|
|
159
|
+
2. Inspect uncommitted files: `git status --porcelain` for the list, `git diff` and
|
|
160
|
+
`git diff --cached` for hunks.
|
|
161
|
+
3. **Filter for branch relevance.** Multi-worktree and multi-agent setups can leave
|
|
162
|
+
stray files in the working tree that belong to other branches. Decide which
|
|
163
|
+
uncommitted files are in scope:
|
|
164
|
+
- Compute the merge base: `git merge-base HEAD origin/<base>`.
|
|
165
|
+
- Files the branch already touches: `git diff --name-only <merge-base>...HEAD`.
|
|
166
|
+
- **In scope** by default: any uncommitted file already touched on the branch,
|
|
167
|
+
or sitting in a directory the branch already touches, or any uncommitted file
|
|
168
|
+
when the branch has no commits yet (first run on a fresh branch).
|
|
169
|
+
- **Out of scope** (suspicious): uncommitted files in directories the branch
|
|
170
|
+
hasn't touched, when the branch already has its own commits.
|
|
171
|
+
4. Show the user the staging plan: in-scope files grouped by proposed commit, plus
|
|
172
|
+
an explicit list of **out-of-scope files** flagged as "uncertain — possibly from
|
|
173
|
+
another branch/worktree." Ask: "Stage in-scope files and create the commits
|
|
174
|
+
below? (yes / no / customise)". Out-of-scope files are never staged
|
|
175
|
+
automatically.
|
|
176
|
+
5. Group in-scope files into **logical atomic commits**:
|
|
177
|
+
- One commit per coherent unit (a feature, a bug fix, a refactor, a docs change,
|
|
178
|
+
a tooling tweak). Don't bundle unrelated edits.
|
|
179
|
+
- Use Conventional Commits–style subjects (`feat:`, `fix:`, `chore:`, `docs:`,
|
|
180
|
+
`refactor:`, `perf:`, `test:`), with a scope when one is obvious.
|
|
181
|
+
6. On confirmation, create the commits with `git add <specific files>` (never
|
|
182
|
+
`git add -A`) and `git commit -m "<subject>"`.
|
|
183
|
+
|
|
184
|
+
If a pre-commit hook reformats files, the commit still succeeds with the formatted
|
|
185
|
+
content.
|
|
186
|
+
|
|
187
|
+
### Step 4: Fetch the base branch and confirm there's something to ship
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
git fetch origin <base>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If `git log origin/<base>..HEAD` is empty, exit with: "No commits ahead of the base
|
|
194
|
+
branch. Nothing to ship."
|
|
195
|
+
|
|
196
|
+
### Step 5: Lint gate — delegate to the `preflight` skill
|
|
197
|
+
|
|
198
|
+
Run the change-gated lint preflight, following the [`preflight`](../preflight/SKILL.md)
|
|
199
|
+
skill:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
node skills/preflight/scripts/preflight.mjs
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Act on its exit-code contract, reading `.preflight-summary.json` to interpret a
|
|
206
|
+
non-zero exit:
|
|
207
|
+
|
|
208
|
+
- **Exit 0 — pass.** No introduced violations; continue.
|
|
209
|
+
- **Exit 1 with `violations.introducedCount > 0` — introduced violations
|
|
210
|
+
(blocking).** Run `node skills/preflight/scripts/lint-fix.mjs`, re-run preflight,
|
|
211
|
+
and repeat until introduced violations clear. Commit the fixes (a `style:`/`fix:`
|
|
212
|
+
commit, or fold into the relevant Step 3 commit if not yet pushed) before
|
|
213
|
+
continuing.
|
|
214
|
+
- **Exit 1 with `introducedCount == 0` and `results.failedLinters` non-empty — a
|
|
215
|
+
linter could not run (its binary is absent), not a real violation.** This is
|
|
216
|
+
expected in a repo that doesn't use that toolchain (e.g. a docs/skills repo with
|
|
217
|
+
no ESLint or markdownlint installed). Treat it as a **skip, not a block**: warn
|
|
218
|
+
that `<linter>` was unavailable and continue. The repo's own CI owns whatever
|
|
219
|
+
linting it actually runs.
|
|
220
|
+
- **Exit 2 — pre-existing violations only.** Not introduced by this branch — do not
|
|
221
|
+
block shipping. Surface them and continue (optionally offer a debt issue per the
|
|
222
|
+
preflight skill).
|
|
223
|
+
|
|
224
|
+
Preflight is **change-gated**: it lints only the categories the branch touched, so
|
|
225
|
+
it no-ops when nothing lint-relevant changed. Skip this step entirely only if
|
|
226
|
+
`preflight` isn't installed.
|
|
227
|
+
|
|
228
|
+
### Step 6: Decide shippability and compose the Conventional Commits PR title
|
|
229
|
+
|
|
230
|
+
Versioning is driven by [release-please](https://github.com/googleapis/release-please)
|
|
231
|
+
reading **Conventional Commits**. The repo squash-merges, so the **squash subject is
|
|
232
|
+
the PR title** — and that single conventional title is what release-please parses to
|
|
233
|
+
decide the bump. send-it composes a correct conventional title and (for shippable
|
|
234
|
+
changes) writes the dated changelog entry. It does **not** bump versions, write any
|
|
235
|
+
`CHANGELOG.md`, or tag.
|
|
236
|
+
|
|
237
|
+
1. **Derive the slug, bump level, and a draft body** from the branch commits via the
|
|
238
|
+
bundled helper (zero-dep — no tsx):
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
node skills/send-it/scripts/derive-bump.mjs
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
It prints JSON: `{ "slug": "…", "bump": "…", "body": "…" }`, where `bump` is
|
|
245
|
+
`major` / `minor` / `patch` (first match wins: a `BREAKING CHANGE:` trailer or a
|
|
246
|
+
`!` in any conventional subject → major; first commit `feat:`/`feat(<scope>):` →
|
|
247
|
+
minor; else patch) and `body` is the first commit's subject with its conventional
|
|
248
|
+
prefix stripped.
|
|
249
|
+
|
|
250
|
+
2. **Decide whether this change is shippable.** Read `shippablePaths` and
|
|
251
|
+
`shippableManifestKeys` from [`config.json`](config.json). A change is
|
|
252
|
+
**shippable** (reaches consumers, so it must trigger a release) iff the branch
|
|
253
|
+
diff touches **either**:
|
|
254
|
+
- any path under a `shippablePaths` prefix, **or**
|
|
255
|
+
- `package.json`, **and** the diff modifies any of the `shippableManifestKeys`.
|
|
256
|
+
|
|
257
|
+
Verify with `git diff --name-only origin/<base>...HEAD`; for `package.json`, also
|
|
258
|
+
run `git diff origin/<base>...HEAD -- package.json` and check whether any listed
|
|
259
|
+
key appears in the hunks. Everything else — pure docs, CI/infra, agent tooling,
|
|
260
|
+
ADRs, the dated `changelog/` itself, release-please config, or a lone
|
|
261
|
+
`chore: update lockfile` — is **non-shippable**.
|
|
262
|
+
|
|
263
|
+
3. **Compose the PR title** as a single Conventional Commits subject — this is the
|
|
264
|
+
release-please bump signal and is enforced by CI's PR-title lint:
|
|
265
|
+
- **Shippable** → a **release-triggering** type from the bump: `major` →
|
|
266
|
+
`feat!: <body>`; `minor` → `feat: <body>`; `patch` → `fix: <body>`. Add a scope
|
|
267
|
+
when one is obvious (`feat(<scope>): …`).
|
|
268
|
+
- **Non-shippable** → a **non-release-triggering** type that matches the change,
|
|
269
|
+
never `feat`/`fix`: `docs:`, `chore:`, `ci:`, `refactor:`, `test:`, `build:`,
|
|
270
|
+
`style:`, `perf:`. Pick by the dominant changed area / first commit's type.
|
|
271
|
+
|
|
272
|
+
> ⚠️ **The PR title is the version.** A mistyped prefix silently ships the wrong
|
|
273
|
+
> semver — a `feat:` on a docs PR cuts a needless release; a `chore:` on a real
|
|
274
|
+
> fix ships nothing. There is no changeset file to cross-check against: the title
|
|
275
|
+
> **is** the declaration. Match the type to the shippability decision exactly.
|
|
276
|
+
|
|
277
|
+
When non-shippable, note `no release (developer-tooling/docs only)` in the PR body
|
|
278
|
+
so reviewers can confirm the non-release type was intentional.
|
|
279
|
+
|
|
280
|
+
### Step 7: Author or update the dated changelog entry — delegate to the `changelog` skill
|
|
281
|
+
|
|
282
|
+
> **Gated on shippability.** Author a `changelog/` entry **only when the change is
|
|
283
|
+
> shippable** (you composed a release-triggering `feat`/`fix`/breaking title). Skip
|
|
284
|
+
> it for non-shippable changes — the dated changelog mirrors the published-change
|
|
285
|
+
> surface, not every PR.
|
|
286
|
+
|
|
287
|
+
Follow the [`changelog`](../changelog/SKILL.md) skill to author or update the entry:
|
|
288
|
+
|
|
289
|
+
1. Detect an existing entry for this branch (by the `branch` frontmatter field) →
|
|
290
|
+
update vs create. On update, preserve the filename and `created_at`.
|
|
291
|
+
2. Write/refresh `changelog/<YYYYMMDD-HHMMSS>-<slug>.md` (the `<slug>` from Step 6),
|
|
292
|
+
deriving `title`/`release_note`/`category`/`breaking`/`issues` from the branch.
|
|
293
|
+
`category` follows the bump (`feat`→`feature`, `fix`→`fix`, etc.); `breaking:
|
|
294
|
+
true` iff the bump is `major`. Leave the post-merge fields (`merged_at`,
|
|
295
|
+
`commit`, `pr`, `merge_strategy`, `stats`) and `version` as blank placeholders —
|
|
296
|
+
the release step finalises them.
|
|
297
|
+
3. Run the enrichment scripts: `node skills/changelog/scripts/set-affected-packages.mjs`
|
|
298
|
+
then `node skills/changelog/scripts/add-links.mjs`.
|
|
299
|
+
4. **Validate:** `node skills/changelog/scripts/validate-changelog.mjs`. It must pass
|
|
300
|
+
before committing — if it fails, surface the error and abort; don't auto-fix.
|
|
301
|
+
|
|
302
|
+
### Step 8: Commit the changelog entry and push
|
|
303
|
+
|
|
304
|
+
If a `changelog/` entry was written (shippable), commit only that file:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
git add changelog/<YYYYMMDD-HHMMSS>-<slug>.md
|
|
308
|
+
git commit -m "docs(changelog): <one-line summary>"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Then push the branch:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
git push -u origin <branch>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Step 9: Create or update the PR
|
|
318
|
+
|
|
319
|
+
`<title>` is the Conventional Commits PR title from Step 6 — release-please reads it
|
|
320
|
+
as the squash subject, so set it on **both** create and update (re-derive it every
|
|
321
|
+
run so it stays in sync with the branch's commits).
|
|
322
|
+
|
|
323
|
+
1. Check for an existing PR: `gh pr view --json number,url 2>/dev/null`.
|
|
324
|
+
2. **If creating:** `gh pr create --base <base> --draft --title "<title>" --body
|
|
325
|
+
"<body>"`. Use `--ready` (the flag) instead of `--draft` if the user passed
|
|
326
|
+
`--ready`.
|
|
327
|
+
3. **If updating:** `gh pr edit <number> --title "<title>" --body "<body>"`.
|
|
328
|
+
4. **If `--merge-when-ready` was passed:** after create/update, run `gh pr merge
|
|
329
|
+
--auto --squash <number>` to enable auto-merge once requirements are met.
|
|
330
|
+
5. Return the PR URL via `gh pr view --json url -q '.url'`.
|
|
331
|
+
|
|
332
|
+
**PR body template:**
|
|
333
|
+
|
|
334
|
+
```markdown
|
|
335
|
+
## Summary
|
|
336
|
+
|
|
337
|
+
- Comprehensive summary of all changes on this branch
|
|
338
|
+
- What changed and why
|
|
339
|
+
|
|
340
|
+
## Related Issues
|
|
341
|
+
|
|
342
|
+
<!-- Linear identifiers extracted from the branch and commits -->
|
|
343
|
+
- <ISSUE-ID>
|
|
344
|
+
|
|
345
|
+
## Test Plan
|
|
346
|
+
|
|
347
|
+
- [ ] <test>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Drop the `## Related Issues` section if no issues were found.
|
|
351
|
+
|
|
352
|
+
### Step 10: Transition linked Linear issues to In Review — delegate to the `linear-sync` skill
|
|
353
|
+
|
|
354
|
+
Follow the [`linear-sync`](../linear-sync/SKILL.md) skill with target state **In
|
|
355
|
+
Review**: read its `config.json` for `linearTeamName` and `issueKeys`, extract issue
|
|
356
|
+
IDs from the branch and commits, resolve the live state ID by team **name** (once),
|
|
357
|
+
and apply the transition idempotently (skip any issue already at or past In Review).
|
|
358
|
+
Skip silently if `linear-sync` or the Linear MCP server is unavailable.
|
|
359
|
+
|
|
360
|
+
## Flags
|
|
361
|
+
|
|
362
|
+
- `--dry-run` — print what would be written/submitted (changelog preview, branch,
|
|
363
|
+
conventional PR title), make no commits, no push, no `gh` calls. Exit 0.
|
|
364
|
+
- `--branch=<name>` — override the auto-derived branch name when running on the base
|
|
365
|
+
branch with uncommitted changes.
|
|
366
|
+
- `--issue=<ID>` — prefix the auto-derived slug with a Linear issue ID (e.g.
|
|
367
|
+
`--issue=ASW-7` → `asw-7-<slug>`, lower-cased). Ignored if `--branch` is given.
|
|
368
|
+
- `--ready` — open the PR ready-for-review instead of draft (default is draft).
|
|
369
|
+
- `--merge-when-ready` — after create/update, enable `gh pr merge --auto --squash`.
|
|
370
|
+
- `--worktree=<branch-or-path>` — `cd` into a worktree before running (Step 0).
|
|
371
|
+
|
|
372
|
+
## Notes
|
|
373
|
+
|
|
374
|
+
- **Trunk-based:** PRs target the base branch (`config.json` `baseBranch`).
|
|
375
|
+
- **Idempotent:** re-running send-it updates the existing PR title and changelog
|
|
376
|
+
entry; the Linear writeback skips issues already In Review or beyond.
|
|
377
|
+
- **send-it does not bump versions or write any `CHANGELOG.md`.** release-please
|
|
378
|
+
reads the merged Conventional-Commit PR title, bumps the manifest in the release
|
|
379
|
+
PR, and the release workflow publishes + tags. send-it only writes the dated
|
|
380
|
+
`changelog/<ts>-<slug>.md` entry (Step 7), finalised at release.
|
|
381
|
+
|
|
382
|
+
## Error Handling
|
|
383
|
+
|
|
384
|
+
- **`gh auth status` fails** — run `gh auth login` first; abort until authenticated.
|
|
385
|
+
- **changelog validation fails** — surface the error; don't auto-fix. The user
|
|
386
|
+
resolves the entry and re-runs.
|
|
387
|
+
- **No commits ahead of the base** — exit "No commits ahead of the base branch.
|
|
388
|
+
Nothing to ship."
|
|
389
|
+
- **Branch push fails** — verify push access; ensure the remote is configured.
|
|
390
|
+
- **PR create/update fails** — verify the PR isn't closed; verify the branch is
|
|
391
|
+
pushed.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme-skunkworks/skill-send-it",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Agent skill: 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, push, open or update a PR, and transition linked Linear issues to In Review.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-skill",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"cursor",
|
|
10
|
+
"git",
|
|
11
|
+
"github",
|
|
12
|
+
"release-please",
|
|
13
|
+
"conventional-commits",
|
|
14
|
+
"changelog"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/acme-skunkworks/agent-skills/tree/main/skills/send-it#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/acme-skunkworks/agent-skills/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/acme-skunkworks/agent-skills.git",
|
|
23
|
+
"directory": "skills/send-it"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": {
|
|
27
|
+
"name": "Rob Easthope",
|
|
28
|
+
"url": "https://github.com/RobEasthope"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Derives the deterministic bits the send-it ship flow needs from the branch
|
|
2
|
+
// commits. Zero dependencies — Node built-ins only, no build step, no tsx.
|
|
3
|
+
// Run: node skills/send-it/scripts/derive-bump.mjs
|
|
4
|
+
//
|
|
5
|
+
// Under release-please (Conventional Commits) there is no changeset file: the
|
|
6
|
+
// release signal is the Conventional Commits PR title. send-it uses these
|
|
7
|
+
// derived bits to name the dated changelog/ entry and to compose that title
|
|
8
|
+
// (the bump signal release-please reads).
|
|
9
|
+
//
|
|
10
|
+
// Fields printed as JSON to stdout:
|
|
11
|
+
// slug : branch-name-derived slug (changelog/<ts>-<slug>.md filename)
|
|
12
|
+
// bump : major | minor | patch (drives the PR-title prefix: feat!/feat/fix)
|
|
13
|
+
// body : a one-line draft summary (the ship flow may rewrite this)
|
|
14
|
+
//
|
|
15
|
+
// Reads from git via `git branch --show-current` and `git log <base>..HEAD`.
|
|
16
|
+
// The base ref is `origin/main` (falling back to `main`), overridable via the
|
|
17
|
+
// BASE_REF env var. The pure functions are exported for vitest.
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
|
|
21
|
+
const SLUG_MAX = 60;
|
|
22
|
+
|
|
23
|
+
// Git --format field/record separators (%x1f unit, %x1e record).
|
|
24
|
+
const UNIT_SEP = "";
|
|
25
|
+
const RECORD_SEP = "";
|
|
26
|
+
|
|
27
|
+
export function deriveSlug(branch) {
|
|
28
|
+
const cleaned = branch
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replaceAll(/[^a-z0-9]+/g, "-")
|
|
31
|
+
.replaceAll(/^-+|-+$/g, "");
|
|
32
|
+
if (cleaned.length <= SLUG_MAX) {
|
|
33
|
+
return cleaned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const truncated = cleaned.slice(0, SLUG_MAX);
|
|
37
|
+
const lastHyphen = truncated.lastIndexOf("-");
|
|
38
|
+
return lastHyphen > 0 ? truncated.slice(0, lastHyphen) : truncated;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const BREAKING_SUBJECT = /^[a-z]+(\([^)]+\))?!:/;
|
|
42
|
+
const FEAT_SUBJECT = /^feat(\([^)]+\))?:/;
|
|
43
|
+
|
|
44
|
+
export function deriveBump(commits) {
|
|
45
|
+
if (commits.length === 0) {
|
|
46
|
+
return "patch";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const anyBreaking = commits.some(
|
|
50
|
+
(commit) =>
|
|
51
|
+
BREAKING_SUBJECT.test(commit.subject) ||
|
|
52
|
+
/BREAKING CHANGE:/.test(commit.body),
|
|
53
|
+
);
|
|
54
|
+
if (anyBreaking) {
|
|
55
|
+
return "major";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (FEAT_SUBJECT.test(commits[0].subject)) {
|
|
59
|
+
return "minor";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return "patch";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function deriveBody(commits) {
|
|
66
|
+
if (commits.length === 0) {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const subject = commits[0].subject;
|
|
71
|
+
return subject.replace(/^[a-z]+(\([^)]+\))?!?:\s*/, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveBaseRef() {
|
|
75
|
+
// BASE_REF (if set) is tried first, then the defaults — so an unresolvable
|
|
76
|
+
// override still falls back rather than silently yielding zero commits.
|
|
77
|
+
const candidates = [process.env.BASE_REF, "origin/main", "main"].filter(
|
|
78
|
+
Boolean,
|
|
79
|
+
);
|
|
80
|
+
for (const ref of candidates) {
|
|
81
|
+
try {
|
|
82
|
+
// execFileSync (no shell) — ref never reaches a shell, so a hostile
|
|
83
|
+
// BASE_REF can't inject.
|
|
84
|
+
execFileSync("git", ["rev-parse", "--verify", ref], { stdio: "ignore" });
|
|
85
|
+
return ref;
|
|
86
|
+
} catch {
|
|
87
|
+
// ref doesn't exist; try next
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readGitCommits() {
|
|
95
|
+
const base = resolveBaseRef();
|
|
96
|
+
if (!base) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const out = execFileSync(
|
|
101
|
+
"git",
|
|
102
|
+
["log", `${base}..HEAD`, "--format=%H%x1f%s%x1f%b%x1e"],
|
|
103
|
+
{ encoding: "utf8" },
|
|
104
|
+
);
|
|
105
|
+
return out
|
|
106
|
+
.split(RECORD_SEP)
|
|
107
|
+
.map((segment) => segment.trim())
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.map((entry) => {
|
|
110
|
+
const [hash, subject, body] = entry.split(UNIT_SEP);
|
|
111
|
+
return { body: body ?? "", hash, subject: subject ?? "" };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readGitBranch() {
|
|
116
|
+
return execFileSync("git", ["branch", "--show-current"], {
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
}).trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function main() {
|
|
122
|
+
const branch = readGitBranch();
|
|
123
|
+
const commits = readGitCommits();
|
|
124
|
+
console.log(
|
|
125
|
+
JSON.stringify(
|
|
126
|
+
{
|
|
127
|
+
body: deriveBody(commits),
|
|
128
|
+
bump: deriveBump(commits),
|
|
129
|
+
slug: deriveSlug(branch),
|
|
130
|
+
},
|
|
131
|
+
null,
|
|
132
|
+
2,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
138
|
+
main();
|
|
139
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# triage-pr
|
|
2
|
+
|
|
3
|
+
Take a pull request from **draft + failing CI** to **merge-ready**: fix in-scope
|
|
4
|
+
CI failures while the PR is a draft, then — once a human marks it ready — fetch
|
|
5
|
+
the unresolved AI review feedback, validate each finding, fix the valid ones and
|
|
6
|
+
decline the invalid ones with reasoning, and re-watch CI until green.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
From any consumer repo:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx skills add https://github.com/acme-skunkworks/agent-skills --skill triage-pr --agent claude-code --agent cursor --copy
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`--copy` writes real files so the bundle is portable. Don't use `-g` / `--global`
|
|
17
|
+
— the install should live in the consumer repo.
|
|
18
|
+
|
|
19
|
+
## Configure
|
|
20
|
+
|
|
21
|
+
Edit [`config.json`](config.json) in your installed copy:
|
|
22
|
+
|
|
23
|
+
| Key | Meaning | Default |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `reviewBots` | GitHub login names whose comments and threads are treated as first-class AI review feedback (matched on `author.login`; the `[bot]` suffix is normalised, so `claude` and `claude[bot]` both match). Edit to match your install — review-bot logins vary per repo. `github-actions` is excluded by default (it posts CI/release comments, not code review); add it only if your install posts review-type comments via the Actions bot. | `["claude", "cursor", "coderabbitai"]` |
|
|
26
|
+
| `maxCiRounds` | Maximum Phase-A re-watch iterations before stopping and reporting blockers — bounds the fix-and-watch loop. | `5` |
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- `gh` CLI, authenticated (`gh auth status` must pass) — used for checks, logs,
|
|
31
|
+
review threads, and thread resolution.
|
|
32
|
+
- `git`.
|
|
33
|
+
- Node.js >=22 (ES-module support), for the bundled review-thread fetcher.
|
|
34
|
+
|
|
35
|
+
## What it does
|
|
36
|
+
|
|
37
|
+
Two phases, chosen from the PR's draft state:
|
|
38
|
+
|
|
39
|
+
1. **Phase A — while the PR is a draft.** Inspect failing checks with `gh`, pull
|
|
40
|
+
the failing GitHub Actions logs, and fix failures **in PR scope only** — never
|
|
41
|
+
weakening CI config to greenwash. Rebase/merge the base branch when failures
|
|
42
|
+
are upstream drift. Loop until CI is green (then stop) or report blockers.
|
|
43
|
+
2. **Phase B — after the PR is ready-for-review.** AI review is gated on
|
|
44
|
+
`draft == false`, so it only runs after a human flips the PR. Fetch the
|
|
45
|
+
**unresolved** review threads (bundled `scripts/review-threads.mjs` returns
|
|
46
|
+
minimal JSON), validate each finding against the codebase before changing
|
|
47
|
+
anything, fix the valid ones, decline the invalid ones with technical
|
|
48
|
+
reasoning, then loop back through Phase A.
|
|
49
|
+
|
|
50
|
+
The skill **never flips the PR from draft to ready** — that is the human's call,
|
|
51
|
+
and the gate that turns AI review on. It actions only the configured `reviewBots`;
|
|
52
|
+
human review comments are surfaced in the report but left for the human.
|
|
53
|
+
|
|
54
|
+
The review-discipline rules folded into Phase B (verify before implementing, no
|
|
55
|
+
sycophancy, evidence before claims) live in
|
|
56
|
+
[`references/review-discipline.md`](references/review-discipline.md).
|