@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.
Files changed (43) hide show
  1. package/README.md +5 -4
  2. package/package.json +2 -6
  3. package/skills/changelog/README.md +59 -0
  4. package/skills/changelog/SKILL.md +187 -0
  5. package/skills/changelog/config.example.json +5 -0
  6. package/skills/changelog/config.json +5 -0
  7. package/skills/changelog/package.json +31 -0
  8. package/skills/changelog/references/changelog-contract.md +121 -0
  9. package/skills/changelog/scripts/add-links.mjs +97 -0
  10. package/skills/changelog/scripts/lib/changelog.mjs +46 -0
  11. package/skills/changelog/scripts/lib/config.mjs +53 -0
  12. package/skills/changelog/scripts/lib/derive-packages.mjs +39 -0
  13. package/skills/changelog/scripts/lib/frontmatter.mjs +369 -0
  14. package/skills/changelog/scripts/preflight-changelog-ci.mjs +152 -0
  15. package/skills/changelog/scripts/set-affected-packages.mjs +99 -0
  16. package/skills/changelog/scripts/validate-changelog.mjs +264 -0
  17. package/skills/linear-sync/README.md +47 -0
  18. package/skills/linear-sync/SKILL.md +115 -0
  19. package/skills/linear-sync/config.example.json +4 -0
  20. package/skills/linear-sync/config.json +4 -0
  21. package/skills/linear-sync/package.json +31 -0
  22. package/skills/preflight/README.md +70 -0
  23. package/skills/preflight/SKILL.md +148 -0
  24. package/skills/preflight/config.example.json +6 -0
  25. package/skills/preflight/package.json +33 -0
  26. package/skills/preflight/scripts/classify-lint.mjs +176 -0
  27. package/skills/preflight/scripts/lib/diff-lines.mjs +83 -0
  28. package/skills/preflight/scripts/lib/paths.mjs +26 -0
  29. package/skills/preflight/scripts/lib/scope.mjs +530 -0
  30. package/skills/preflight/scripts/lint-fix.mjs +78 -0
  31. package/skills/preflight/scripts/preflight.mjs +416 -0
  32. package/skills/send-it/README.md +75 -0
  33. package/skills/send-it/SKILL.md +391 -0
  34. package/skills/send-it/config.example.json +5 -0
  35. package/skills/send-it/config.json +5 -0
  36. package/skills/send-it/package.json +33 -0
  37. package/skills/send-it/scripts/derive-bump.mjs +139 -0
  38. package/skills/triage-pr/README.md +56 -0
  39. package/skills/triage-pr/SKILL.md +291 -0
  40. package/skills/triage-pr/config.json +4 -0
  41. package/skills/triage-pr/package.json +32 -0
  42. package/skills/triage-pr/references/review-discipline.md +73 -0
  43. 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,5 @@
1
+ {
2
+ "baseBranch": "main",
3
+ "shippablePaths": ["dist/", "src/"],
4
+ "shippableManifestKeys": ["name", "version", "files", "publishConfig"]
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "baseBranch": "main",
3
+ "shippablePaths": ["skills/"],
4
+ "shippableManifestKeys": ["name", "version", "files", "publishConfig"]
5
+ }
@@ -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).