@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
package/README.md
CHANGED
|
@@ -16,7 +16,6 @@ npx skills add https://github.com/acme-skunkworks/agent-skills --skill <name> --
|
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
.
|
|
19
|
-
├── .changeset/ # pending changesets + config
|
|
20
19
|
├── .claude/commands/
|
|
21
20
|
│ └── send-it.md # all-in-one finisher (stopgap until the send-it skill ships)
|
|
22
21
|
├── .github/
|
|
@@ -24,10 +23,12 @@ npx skills add https://github.com/acme-skunkworks/agent-skills --skill <name> --
|
|
|
24
23
|
│ │ └── load-repo-config/ # infrastructure/repo-config.yaml → step outputs
|
|
25
24
|
│ └── workflows/
|
|
26
25
|
│ ├── release.yml # publish-only: build → npm (OIDC) → GitHub Packages (dormant)
|
|
27
|
-
│ └── validate.yml # PR gate: build & lint, changelog validation, infra tests
|
|
26
|
+
│ └── validate.yml # PR gate: build & lint, changelog validation, PR-title lint, infra tests
|
|
28
27
|
├── .husky/ # git hooks (block main pushes; lint-staged; strip Claude trailer)
|
|
29
28
|
├── architecture/ # ADRs (sequentially numbered, immutable)
|
|
30
|
-
├── changelog/ # dated per-change release-note entries (
|
|
29
|
+
├── changelog/ # dated per-change release-note entries (the repo's only changelog)
|
|
30
|
+
├── release-please-config.json # release-please packages config (single root package)
|
|
31
|
+
├── .release-please-manifest.json # release-please version manifest
|
|
31
32
|
├── infrastructure/
|
|
32
33
|
│ ├── repo-config.yaml # non-secret CI/release knobs
|
|
33
34
|
│ ├── scripts/ # changelog .ts helpers + ensure-*.sh tool bootstraps
|
|
@@ -49,4 +50,4 @@ ADRs land under `architecture/` as `NNNN-<slug>.md`. ADR-0001 — the foundation
|
|
|
49
50
|
|
|
50
51
|
## Contributing
|
|
51
52
|
|
|
52
|
-
See [CLAUDE.md](./CLAUDE.md) for the conventions (Conventional Commits, draft PRs,
|
|
53
|
+
See [CLAUDE.md](./CLAUDE.md) for the conventions (Conventional Commits, draft PRs, release-please versioning driven by the PR title).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acme-skunkworks/agent-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared agent skills (Claude Code, Cursor) distributed as skills.sh-compatible bundles.",
|
|
6
6
|
"keywords": [
|
|
@@ -28,20 +28,17 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"changelog:finalise": "tsx infrastructure/scripts/finalise-changelog.ts",
|
|
31
|
-
"changeset": "changeset",
|
|
32
|
-
"changeset:version": "changeset version && pnpm changelog:finalise",
|
|
33
31
|
"lint:sh": "bash -c 'if command -v shellcheck >/dev/null 2>&1; then shellcheck scripts/*.sh infrastructure/scripts/*.sh .husky/pre-commit .husky/pre-push .husky/commit-msg; elif [ \"$(uname -s)\" = \"Darwin\" ]; then echo \"⚠️ shellcheck not installed — skipping. Install: brew install shellcheck\"; else echo \"⚠️ shellcheck not installed — skipping. Install: apt-get install shellcheck\"; fi'",
|
|
34
32
|
"lint:workflows": "actionlint",
|
|
35
33
|
"lint:yaml": "yamllint .",
|
|
36
34
|
"prepare": "husky",
|
|
37
|
-
"release": "changeset publish",
|
|
38
35
|
"release:manual": "npm publish --access public --provenance=false",
|
|
39
36
|
"release:manual:dry": "npm publish --access public --provenance=false --dry-run",
|
|
40
37
|
"test": "vitest run",
|
|
41
38
|
"test:sh": "bash -c 'if command -v bats >/dev/null 2>&1; then bats infrastructure/tests/*.bats; elif [ \"$(uname -s)\" = \"Darwin\" ]; then echo \"⚠️ bats not installed — skipping. Install: brew install bats-core\"; else echo \"⚠️ bats not installed — skipping. Install: apt-get install bats\"; fi'",
|
|
42
39
|
"test:watch": "vitest",
|
|
43
40
|
"validate:changelog": "tsx infrastructure/scripts/validate-changelog.ts",
|
|
44
|
-
"
|
|
41
|
+
"validate:skills": "tsx infrastructure/scripts/validate-skills.ts"
|
|
45
42
|
},
|
|
46
43
|
"lint-staged": {
|
|
47
44
|
"**/*.sh": [
|
|
@@ -58,7 +55,6 @@
|
|
|
58
55
|
]
|
|
59
56
|
},
|
|
60
57
|
"devDependencies": {
|
|
61
|
-
"@changesets/cli": "^2.31.0",
|
|
62
58
|
"@types/node": "^25.6.0",
|
|
63
59
|
"gray-matter": "^4.0.3",
|
|
64
60
|
"husky": "^9.1.7",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# changelog
|
|
2
|
+
|
|
3
|
+
Author, refresh, or repair the changelog entry for the current branch under
|
|
4
|
+
`changelog/YYYYMMDD-HHMMSS-<slug>.md` — derive metadata, write the frontmatter and
|
|
5
|
+
grouped body, run the deterministic enrichment scripts, and validate against the
|
|
6
|
+
changelog contract.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
From any consumer repo:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx skills add https://github.com/acme-skunkworks/agent-skills --skill changelog --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 installation should live in the consumer repo.
|
|
18
|
+
|
|
19
|
+
## Configure
|
|
20
|
+
|
|
21
|
+
The shipped [`config.json`](config.json) carries **ACME Skunkworks defaults** —
|
|
22
|
+
update them for your organisation on install, or issue-ID detection and Linear
|
|
23
|
+
links will be wrong. A neutral [`config.example.json`](config.example.json) ships
|
|
24
|
+
alongside it as a template — copy it over `config.json` or edit `config.json`
|
|
25
|
+
directly.
|
|
26
|
+
|
|
27
|
+
| Key | Meaning | Default |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `issueKeys` | Team-key prefixes used to recognise issue IDs in the branch and body. Keep legacy keys so old branches still match. | `["ASW", "AKW", "SKW"]` |
|
|
30
|
+
| `linearWorkspaceSlug` | Linear workspace slug for issue links (`https://linear.app/<slug>/issue/<id>`). | `"goose-and-hobbes"` |
|
|
31
|
+
| `baseBranch` | Trunk the branch diff is taken against (`origin/<baseBranch>`); `BASE_REF` env overrides per-run. | `"main"` |
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- **Node.js ≥22** for the bundled scripts. They use **only Node built-ins** — no
|
|
36
|
+
`npm install`, no build step.
|
|
37
|
+
- The **`git` CLI** for branch and diff analysis.
|
|
38
|
+
- **pnpm** *only* for the optional `preflight-changelog-ci.mjs` step (Node/lockfile
|
|
39
|
+
CI-parity). Skip that step if your repo doesn't use pnpm.
|
|
40
|
+
|
|
41
|
+
## What it does
|
|
42
|
+
|
|
43
|
+
Detects the branch's existing entry (idempotent update-vs-create), derives the
|
|
44
|
+
metadata from git and the diff, writes the frontmatter + grouped/categorised body,
|
|
45
|
+
runs the enrichment scripts (`set-affected-packages.mjs`, `add-links.mjs`), and
|
|
46
|
+
validates with `validate-changelog.mjs`. `created_at` is set once and never
|
|
47
|
+
overwritten; `stats` and the post-merge fields are left blank for the release step.
|
|
48
|
+
|
|
49
|
+
Run standalone via `/changelog` (writes/validates, leaves the entry **uncommitted**)
|
|
50
|
+
or as the changelog step inside a ship flow. See [`SKILL.md`](SKILL.md) for the
|
|
51
|
+
six-step process and [`references/changelog-contract.md`](references/changelog-contract.md)
|
|
52
|
+
for the full frontmatter schema and field-ownership rules.
|
|
53
|
+
|
|
54
|
+
## Scripts and tests
|
|
55
|
+
|
|
56
|
+
The bundled scripts are the **zero-dependency `.mjs`** set (Node built-ins only),
|
|
57
|
+
deliberately chosen so the bundle is drop-in with no tooling. Their **unit tests
|
|
58
|
+
are maintained in the [`agent-skills`](https://github.com/acme-skunkworks/agent-skills)
|
|
59
|
+
repo**, not bundled into the skill — see that repo's test suite for coverage.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: changelog
|
|
3
|
+
description: >-
|
|
4
|
+
Author, refresh, or repair the changelog entry for the current branch — derive
|
|
5
|
+
metadata, write the frontmatter and grouped body, run the deterministic
|
|
6
|
+
enrichment scripts, and validate against the changelog contract. Use when asked
|
|
7
|
+
to write or update a changelog entry, refresh an entry after new commits, or as
|
|
8
|
+
the changelog step inside a ship/PR flow. Detects an existing entry for the
|
|
9
|
+
branch (idempotent update-vs-create), keeps `created_at` sacred, leaves
|
|
10
|
+
post-merge fields to the release step, and validates with a zero-dependency
|
|
11
|
+
Node script.
|
|
12
|
+
license: MIT
|
|
13
|
+
compatibility: >-
|
|
14
|
+
Requires Node.js ≥22 for the bundled scripts (no npm dependencies — Node
|
|
15
|
+
built-ins only) and the `git` CLI for branch/diff analysis. The optional
|
|
16
|
+
`preflight-changelog-ci.mjs` step assumes the consumer repo uses pnpm with a
|
|
17
|
+
committed lockfile; skip it if yours does not.
|
|
18
|
+
metadata:
|
|
19
|
+
version: 0.1.1
|
|
20
|
+
allowed-tools: Write, Read, Edit, Glob, Grep, Bash(git:*), Bash(node:*), Bash(pnpm:*)
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# changelog
|
|
24
|
+
|
|
25
|
+
Generate or update the changelog entry for the current branch under
|
|
26
|
+
`changelog/YYYYMMDD-HHMMSS-<slug>.md`: derive its metadata from git and the diff,
|
|
27
|
+
write the frontmatter and a grouped, categorised body, run the deterministic
|
|
28
|
+
enrichment scripts, then validate the result.
|
|
29
|
+
|
|
30
|
+
This skill is the single source of truth for **what a valid changelog entry is**
|
|
31
|
+
— the frontmatter schema, the field-ownership boundaries, idempotent
|
|
32
|
+
update-vs-create, and the validation gate. The same contract is enforced
|
|
33
|
+
downstream by a consumer repo's CI and relied on by a release-orchestrator that
|
|
34
|
+
finalises the post-merge fields, so the authoring rules live here once.
|
|
35
|
+
|
|
36
|
+
It is invoked two ways:
|
|
37
|
+
|
|
38
|
+
- **Standalone** (`/changelog`) — author, refresh, or repair this branch's entry
|
|
39
|
+
and leave it **uncommitted** in the working tree for review. No commit, push,
|
|
40
|
+
or PR.
|
|
41
|
+
- **Inside a ship flow** (e.g. a `/send-it`) — the changelog step that runs
|
|
42
|
+
before push; the ship flow **commits** the entry, pushes, and opens the PR.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Three knobs live in [`config.json`](config.json) beside this file; the bundled
|
|
47
|
+
scripts read it automatically. Edit your copied `config.json` to match the
|
|
48
|
+
consuming repo (a neutral [`config.example.json`](config.example.json) ships as a
|
|
49
|
+
template):
|
|
50
|
+
|
|
51
|
+
| Key | Meaning | Default |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `issueKeys` | Team-key prefixes used to recognise issue IDs in the branch and body. The issue-ID regex is built from these; keep legacy keys so old branches still match. | `["ASW", "AKW", "SKW"]` |
|
|
54
|
+
| `linearWorkspaceSlug` | Linear workspace slug used to build issue links (`https://linear.app/<slug>/issue/<id>`). | `"goose-and-hobbes"` |
|
|
55
|
+
| `baseBranch` | The trunk the branch diff is taken against (`origin/<baseBranch>`). Overridable per-run via the `BASE_REF` env var. | `"main"` |
|
|
56
|
+
|
|
57
|
+
All bundled scripts use only Node built-ins — no `npm install`, no build step.
|
|
58
|
+
They operate on the **consumer repo's root `changelog/` directory** (run them
|
|
59
|
+
from the repo root).
|
|
60
|
+
|
|
61
|
+
## Running it
|
|
62
|
+
|
|
63
|
+
### Step 1 — Detect an existing entry (idempotency)
|
|
64
|
+
|
|
65
|
+
Grep `changelog/` for a file whose frontmatter contains `branch: "<current-branch>"`.
|
|
66
|
+
If exactly one matches, you are in **update mode**: preserve its `created_at` and
|
|
67
|
+
filename, rewrite the rest. Otherwise you are in **create mode**.
|
|
68
|
+
|
|
69
|
+
### Step 2 — Analyse the branch
|
|
70
|
+
|
|
71
|
+
- `git log origin/<base>..HEAD --pretty=full` — full commit list including bodies
|
|
72
|
+
and trailers.
|
|
73
|
+
- `git diff origin/<base>...HEAD --name-only` — changed files, for grouping the
|
|
74
|
+
body by package.
|
|
75
|
+
|
|
76
|
+
`<base>` is `config.json`'s `baseBranch` (default `main`). Fetch it first
|
|
77
|
+
(`git fetch origin <base>`) so the diff is accurate — skip the fetch if the
|
|
78
|
+
caller already did it (e.g. a ship flow fetches in its preflight step).
|
|
79
|
+
|
|
80
|
+
### Step 3 — Derive metadata
|
|
81
|
+
|
|
82
|
+
| Field | How to derive |
|
|
83
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
84
|
+
| `issues` | Match the issue-ID regex (built from `issueKeys`) against the branch name (upper-cased) and against commit subjects/bodies. Deduplicate. |
|
|
85
|
+
| `author` | `git config user.email`. |
|
|
86
|
+
| `co_authors` | Parse `Co-authored-by: Name <email>` trailers across all branch commits. Store the email or `Name <email>` form. Empty array if none. |
|
|
87
|
+
| `category` | Infer from commit subjects and diff: `feature`, `fix`, `chore`, `docs`, `refactor`, `perf`. If ambiguous, ask the user to confirm. |
|
|
88
|
+
| `breaking` | Infer from `BREAKING CHANGE:` trailers, `!` in conventional-commit subjects, or removal of public surfaces. If unclear, ask the user. Default `false`. |
|
|
89
|
+
| `release_note` | One-sentence user-facing summary distinct from `title`. Optional — leave blank if the change has no public-facing impact (chore, internal refactor). |
|
|
90
|
+
|
|
91
|
+
**Field ownership** — what this skill authors vs. what it must leave alone is the
|
|
92
|
+
crux of the contract; see [`references/changelog-contract.md`](references/changelog-contract.md)
|
|
93
|
+
for the full rules. In short:
|
|
94
|
+
|
|
95
|
+
- **Authored here:** `title`, `release_note`, `category`, `breaking`, `issues`,
|
|
96
|
+
`co_authors`, `author`, and `affected_packages` (written by the enrichment
|
|
97
|
+
script in Step 5, not hand-edited).
|
|
98
|
+
- **`created_at` is sacred** — set once on create (UTC time of first run); on
|
|
99
|
+
update, preserve it verbatim.
|
|
100
|
+
- **Never authored here:** `stats` (`files_changed`, `loc_added`, `loc_removed`)
|
|
101
|
+
and the post-merge fields `merged_at` / `commit` / `merge_strategy`. A release
|
|
102
|
+
step finalises them from canonical GitHub PR data after merge. Emit them as
|
|
103
|
+
blank placeholders on create; leave existing values untouched on update.
|
|
104
|
+
- `pr` is back-filled by the ship flow once the PR exists (not here when
|
|
105
|
+
standalone).
|
|
106
|
+
|
|
107
|
+
The skill **emits the derived `issues` array** as a handoff — a ship flow reuses
|
|
108
|
+
it for the PR body and any Linear writeback (e.g. via a `linear-sync` skill).
|
|
109
|
+
|
|
110
|
+
### Step 4 — Generate the body
|
|
111
|
+
|
|
112
|
+
Group bullets by package, categorised under `## Added` / `## Changed` / `## Fixed`.
|
|
113
|
+
Only include headings that have entries. For multi-package changes use
|
|
114
|
+
`**<pkg-name>:**` subheaders.
|
|
115
|
+
|
|
116
|
+
If `breaking: true`, the body MUST start with a `## Breaking` section describing
|
|
117
|
+
the change and the migration path.
|
|
118
|
+
|
|
119
|
+
### Step 5 — Write or update the file
|
|
120
|
+
|
|
121
|
+
**Filename:** `changelog/YYYYMMDD-HHMMSS-<slug>.md`, where the timestamp is
|
|
122
|
+
`created_at` (UTC time of first run) and the slug derives from `title` (lowercase,
|
|
123
|
+
non-alphanumerics → `-`, collapse repeats, ~60-char cap on a word boundary).
|
|
124
|
+
|
|
125
|
+
**Always quote timestamp strings** in YAML (`created_at: "2026-04-26T13:24:00Z"`).
|
|
126
|
+
Unquoted ISO timestamps parse as Date objects and gain `.000Z` millis on the
|
|
127
|
+
enrichment round-trip; quoting keeps them lossless.
|
|
128
|
+
|
|
129
|
+
**On update:** preserve `created_at` and the filename; rewrite `title`,
|
|
130
|
+
`release_note`, `category`, `breaking`, `co_authors`, `issues`, and the body;
|
|
131
|
+
leave `merged_at` / `commit` / `merge_strategy` / `stats` alone; update `pr` only
|
|
132
|
+
if it was blank and a PR now exists.
|
|
133
|
+
|
|
134
|
+
Use the frontmatter field order shown in
|
|
135
|
+
[`references/changelog-contract.md`](references/changelog-contract.md), emitting
|
|
136
|
+
`affected_packages: []` as a placeholder — the script fills it in place.
|
|
137
|
+
|
|
138
|
+
Then run the two deterministic enrichment scripts from the consumer repo root
|
|
139
|
+
(both idempotent; they match the entry by its `branch:` frontmatter and leave the
|
|
140
|
+
post-merge fields blank):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
node skills/changelog/scripts/set-affected-packages.mjs # writes affected_packages from the branch diff
|
|
144
|
+
node skills/changelog/scripts/add-links.mjs # rewrites bare issue IDs in the body to Linear URLs
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Adjust the path prefix if you installed the skill to a different location.
|
|
148
|
+
|
|
149
|
+
### Step 6 — Validate against the contract
|
|
150
|
+
|
|
151
|
+
This is the gate:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
node scripts/preflight-changelog-ci.mjs # optional: checks Node vs engines/.nvmrc, then pnpm install --frozen-lockfile
|
|
155
|
+
node scripts/validate-changelog.mjs # validates frontmatter schema, filename format, field types, ISO timestamps, Breaking section, issue IDs
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`preflight-changelog-ci.mjs` is optional and pnpm-specific — skip it if the
|
|
159
|
+
consumer repo doesn't use pnpm. On failure, stop and fix the entry before
|
|
160
|
+
continuing — do not hand a malformed entry to the ship flow.
|
|
161
|
+
|
|
162
|
+
## Standalone vs inside a ship flow
|
|
163
|
+
|
|
164
|
+
- **Standalone (`/changelog`)** runs Steps 1–6 and then **reports**, leaving the
|
|
165
|
+
entry **uncommitted** in the working tree for the user to review and commit. It
|
|
166
|
+
never pushes or opens a PR.
|
|
167
|
+
- **Inside a ship flow** the same steps run before push; the ship flow then
|
|
168
|
+
commits the entry (`docs(changelog): <title>`), pushes, and opens or updates the
|
|
169
|
+
PR, back-filling `pr:` once the PR number is known.
|
|
170
|
+
|
|
171
|
+
## Implementation
|
|
172
|
+
|
|
173
|
+
The enrichment and validation scripts live under [`scripts/`](scripts/) in this
|
|
174
|
+
bundle and run on plain Node (no npm dependencies, no build step):
|
|
175
|
+
|
|
176
|
+
- `scripts/set-affected-packages.mjs` — writes `affected_packages` from the branch diff.
|
|
177
|
+
- `scripts/add-links.mjs` — rewrites bare issue IDs in the body to Linear URLs.
|
|
178
|
+
- `scripts/preflight-changelog-ci.mjs` — optional Node/lockfile CI-parity check (pnpm).
|
|
179
|
+
- `scripts/validate-changelog.mjs` — validates the entry against the contract.
|
|
180
|
+
|
|
181
|
+
They share helpers under `scripts/lib/` (`changelog.mjs`, `derive-packages.mjs`,
|
|
182
|
+
`frontmatter.mjs`, `config.mjs`). The post-merge finalisation of `stats` /
|
|
183
|
+
`merged_at` / `commit` / `merge_strategy` is owned by the release-orchestrator and
|
|
184
|
+
is **not** invoked here.
|
|
185
|
+
|
|
186
|
+
> **Note for adopters:** unit tests for these scripts are maintained in the
|
|
187
|
+
> `agent-skills` repo (not bundled into the skill). See the skill's README.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme-skunkworks/skill-changelog",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Agent skill: author, refresh, or repair the per-branch changelog entry — derive metadata, write the frontmatter and grouped body, run the deterministic enrichment scripts, and validate against the changelog contract.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-skill",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"cursor",
|
|
10
|
+
"changelog",
|
|
11
|
+
"release-notes",
|
|
12
|
+
"changesets"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/acme-skunkworks/agent-skills/tree/main/skills/changelog#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/acme-skunkworks/agent-skills/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/acme-skunkworks/agent-skills.git",
|
|
21
|
+
"directory": "skills/changelog"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "Rob Easthope",
|
|
26
|
+
"url": "https://github.com/RobEasthope"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Changelog entry contract
|
|
2
|
+
|
|
3
|
+
The full frontmatter schema and field-ownership rules the `changelog` skill
|
|
4
|
+
enforces. The bundled `scripts/validate-changelog.mjs` is the executable form of
|
|
5
|
+
this contract.
|
|
6
|
+
|
|
7
|
+
## Frontmatter schema
|
|
8
|
+
|
|
9
|
+
Preserve this field order. Emit `affected_packages: []` as a placeholder — the
|
|
10
|
+
enrichment script fills it in place.
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
---
|
|
14
|
+
title: "Concise summary"
|
|
15
|
+
release_note: "One-sentence user-facing summary"
|
|
16
|
+
created_at: "2026-04-26T13:24:00Z"
|
|
17
|
+
merged_at:
|
|
18
|
+
branch: "<current-branch>"
|
|
19
|
+
pr:
|
|
20
|
+
commit:
|
|
21
|
+
merge_strategy:
|
|
22
|
+
author: "you@example.com"
|
|
23
|
+
co_authors: []
|
|
24
|
+
category: feature
|
|
25
|
+
breaking: false
|
|
26
|
+
issues: ["ASW-123"]
|
|
27
|
+
affected_packages: []
|
|
28
|
+
stats:
|
|
29
|
+
files_changed:
|
|
30
|
+
loc_added:
|
|
31
|
+
loc_removed:
|
|
32
|
+
---
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Required fields
|
|
36
|
+
|
|
37
|
+
`validate-changelog.mjs` requires: `title`, `created_at`, `branch`, `author`,
|
|
38
|
+
`category`, `breaking`, `co_authors`, and a `stats` object. Other fields are
|
|
39
|
+
validated **by type when present** but may be blank placeholders until enrichment.
|
|
40
|
+
|
|
41
|
+
## Field types and rules
|
|
42
|
+
|
|
43
|
+
| Field | Rule |
|
|
44
|
+
| ----- | ---- |
|
|
45
|
+
| `title` | Non-empty string. |
|
|
46
|
+
| `release_note` | String or `null`/blank when present. |
|
|
47
|
+
| `created_at` | ISO 8601 UTC with `Z` suffix, quoted. **Set once; never overwritten.** |
|
|
48
|
+
| `merged_at` | ISO 8601 UTC with `Z` suffix when set; blank until release. |
|
|
49
|
+
| `branch` | Non-empty string — the stable lookup key for enrichment. |
|
|
50
|
+
| `pr` | Integer when set; blank until the PR exists. |
|
|
51
|
+
| `commit` | 7-char hex SHA when set; blank until merge. |
|
|
52
|
+
| `merge_strategy` | One of `merge`, `rebase`, `squash`; blank until merge. |
|
|
53
|
+
| `author` | Non-empty string (an email). |
|
|
54
|
+
| `co_authors` | Array of strings (`[]` when none). |
|
|
55
|
+
| `category` | One of `feature`, `fix`, `chore`, `docs`, `refactor`, `perf`. |
|
|
56
|
+
| `breaking` | Boolean. If `true`, the body MUST contain a `## Breaking` section. |
|
|
57
|
+
| `issues` | Array of strings, each matching `[A-Z]{2,}-\d+`. |
|
|
58
|
+
| `affected_packages` | Array of strings (`[]` when unpopulated). |
|
|
59
|
+
| `stats.{files_changed,loc_added,loc_removed}` | Non-negative integers when set; blank until release. |
|
|
60
|
+
|
|
61
|
+
The filename must match `YYYYMMDD-HHMMSS-<slug>.md` (slug `[a-z0-9-]+`), and the
|
|
62
|
+
body must contain at least one of `## Breaking` / `## Added` / `## Changed` /
|
|
63
|
+
`## Fixed`.
|
|
64
|
+
|
|
65
|
+
## Field ownership boundaries
|
|
66
|
+
|
|
67
|
+
Four owners, never overlapping:
|
|
68
|
+
|
|
69
|
+
1. **The author (this skill).** `title`, `release_note`, `category`, `breaking`,
|
|
70
|
+
`issues`, `co_authors`, `author`. Re-derived on every run.
|
|
71
|
+
2. **The enrichment scripts (deterministic, pre-merge).** `affected_packages` —
|
|
72
|
+
`set-affected-packages.mjs` always overwrites it from the latest branch diff,
|
|
73
|
+
so it tracks added commits. `add-links.mjs` rewrites bare issue IDs in the body
|
|
74
|
+
to Linear links.
|
|
75
|
+
3. **The release-orchestrator (post-merge, privileged).** `merged_at`, `commit`,
|
|
76
|
+
`merge_strategy`, and authoritative `stats`, plus the published `version` where
|
|
77
|
+
a consumer adds one. Emit these as blank placeholders; never hand-edit them —
|
|
78
|
+
so an in-flight PR never shows numbers that drift as commits land.
|
|
79
|
+
4. **The ship flow (`/send-it`).** `pr` — back-filled when the PR is opened; left
|
|
80
|
+
blank by the author and untouched by enrichment until then.
|
|
81
|
+
|
|
82
|
+
`branch` is set by the author at create time and is the stable lookup key for
|
|
83
|
+
enrichment.
|
|
84
|
+
|
|
85
|
+
`created_at` is **sacred**: set once at create time, preserved verbatim on every
|
|
86
|
+
update. The release step refuses to finalise an entry without it.
|
|
87
|
+
|
|
88
|
+
## Body structure
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
## Breaking <!-- only when breaking: true -->
|
|
92
|
+
|
|
93
|
+
- Description and migration steps
|
|
94
|
+
|
|
95
|
+
## Added
|
|
96
|
+
|
|
97
|
+
- Description
|
|
98
|
+
|
|
99
|
+
## Changed
|
|
100
|
+
|
|
101
|
+
- ...
|
|
102
|
+
|
|
103
|
+
## Fixed
|
|
104
|
+
|
|
105
|
+
- ...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Only include `Added` / `Changed` / `Fixed` headings that have entries.
|
|
109
|
+
|
|
110
|
+
## Notes for adopters
|
|
111
|
+
|
|
112
|
+
- **Single-package repos** can drop `affected_packages` from the schema (there is
|
|
113
|
+
only one package) and skip `set-affected-packages.mjs`. The validator treats
|
|
114
|
+
`affected_packages` as optional-when-present, so leaving it out is fine — but
|
|
115
|
+
if you keep `validate-changelog.mjs` as shipped, it still requires `branch`,
|
|
116
|
+
`author`, `co_authors`, and `stats`.
|
|
117
|
+
- **Adding a `version` field.** A single-versioned package may add `version` to
|
|
118
|
+
record the release each entry shipped in; that is owned by the release step,
|
|
119
|
+
alongside the other post-merge fields.
|
|
120
|
+
- The `issueKeys`, `linearWorkspaceSlug`, and `baseBranch` knobs in `config.json`
|
|
121
|
+
are the only repo-specific inputs; everything else in the contract is generic.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadConfig } from "./lib/config.mjs";
|
|
3
|
+
import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const CHANGELOG_DIR = "changelog";
|
|
7
|
+
const { linearWorkspaceSlug: WORKSPACE, issueKeys: TEAM_KEYS } = loadConfig();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Escape regex metacharacters so a configured key such as `C++` or `MY.KEY`
|
|
11
|
+
* can't throw at construction or silently widen the match.
|
|
12
|
+
* @param {string} s
|
|
13
|
+
*/
|
|
14
|
+
function escapeRegex(s) {
|
|
15
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// `null` when no issue keys are configured: an empty alternation would match the
|
|
19
|
+
// empty string before every `-<digits>` and inject bogus links (e.g. `-2`).
|
|
20
|
+
const ISSUE_RE =
|
|
21
|
+
TEAM_KEYS.length > 0
|
|
22
|
+
? new RegExp(`\\b(?:${TEAM_KEYS.map(escapeRegex).join("|")})-\\d+\\b`, "g")
|
|
23
|
+
: null;
|
|
24
|
+
const FENCE_RE = /```[\s\S]*?```/g;
|
|
25
|
+
const INLINE_CODE_RE = /`[^`]*`/g;
|
|
26
|
+
const ALREADY_LINKED_RE = /\[[^\]]*\]\([^)]*\)/g;
|
|
27
|
+
|
|
28
|
+
function buildUrl(id) {
|
|
29
|
+
return `https://linear.app/${WORKSPACE}/issue/${id}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rewriteBody(body) {
|
|
33
|
+
if (!ISSUE_RE) {
|
|
34
|
+
return body;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Mask fenced/inline code and existing links so issue-like text inside them
|
|
38
|
+
// isn't linkified. Tokens are delimited with NUL bytes, which cannot occur in
|
|
39
|
+
// a UTF-8 text file, so a token can never collide with real prose on restore
|
|
40
|
+
// (the previous bare `FENCE0`/`INLINE1`/`LINK2` tokens could).
|
|
41
|
+
const masks = [];
|
|
42
|
+
const mask = (m) => {
|
|
43
|
+
masks.push(m);
|
|
44
|
+
return `\x00CR_MASK_${masks.length - 1}\x00`;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const masked = body
|
|
48
|
+
.replace(FENCE_RE, mask)
|
|
49
|
+
.replace(INLINE_CODE_RE, mask)
|
|
50
|
+
.replace(ALREADY_LINKED_RE, mask)
|
|
51
|
+
.replace(ISSUE_RE, (id) => `[${id}](${buildUrl(id)})`);
|
|
52
|
+
|
|
53
|
+
return masked.replace(/\x00CR_MASK_(\d+)\x00/g, (_, i) => masks[Number(i)]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function splitFrontmatter(raw) {
|
|
57
|
+
// Match the opening/closing `---` fences with either LF or CRLF endings so a
|
|
58
|
+
// file authored on Windows isn't treated as having no frontmatter (which
|
|
59
|
+
// would let `rewriteBody` rewrite the frontmatter region too).
|
|
60
|
+
const match = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
|
|
61
|
+
if (!match) {
|
|
62
|
+
return { fm: "", body: raw };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { fm: match[0], body: raw.slice(match[0].length) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let stat;
|
|
69
|
+
try {
|
|
70
|
+
stat = statSync(CHANGELOG_DIR);
|
|
71
|
+
} catch {
|
|
72
|
+
console.error(`changelog directory not found: ${CHANGELOG_DIR}`);
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!stat.isDirectory()) {
|
|
77
|
+
console.error(`${CHANGELOG_DIR} is not a directory`);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const files = readdirSync(CHANGELOG_DIR)
|
|
82
|
+
.filter((n) => n.endsWith(".md") && n !== "README.md")
|
|
83
|
+
.map((n) => join(CHANGELOG_DIR, n));
|
|
84
|
+
|
|
85
|
+
let touched = 0;
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const raw = readFileSync(file, "utf8");
|
|
88
|
+
const { fm, body } = splitFrontmatter(raw);
|
|
89
|
+
const next = rewriteBody(body);
|
|
90
|
+
if (next !== body) {
|
|
91
|
+
writeFileSync(file, fm + next);
|
|
92
|
+
touched++;
|
|
93
|
+
console.log(`rewrote: ${file}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`Linear link rewriting complete. ${touched} file(s) updated.`);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Shared helpers for locating changelog entries on disk.
|
|
2
|
+
//
|
|
3
|
+
// `findEntryByBranch` is the one entry-lookup rule the enrichment scripts share,
|
|
4
|
+
// so the rule can't drift between callers — the same reasoning that produced
|
|
5
|
+
// derive-packages.mjs.
|
|
6
|
+
|
|
7
|
+
import { parseFrontmatter } from "./frontmatter.mjs";
|
|
8
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CHANGELOG_DIR = "changelog";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find the changelog entry whose frontmatter `branch:` equals `branch`.
|
|
15
|
+
* @param {string} branch the branch name to match against the `branch:` field
|
|
16
|
+
* @param {string} [changelogDir] directory to scan (default: "changelog")
|
|
17
|
+
* @returns {string|null} the matching entry's path, or null if none matches
|
|
18
|
+
*/
|
|
19
|
+
export function findEntryByBranch(
|
|
20
|
+
branch,
|
|
21
|
+
changelogDir = DEFAULT_CHANGELOG_DIR,
|
|
22
|
+
) {
|
|
23
|
+
let names;
|
|
24
|
+
try {
|
|
25
|
+
names = readdirSync(changelogDir);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// A repo with no `changelog/` directory yet means "no entry found", not a
|
|
28
|
+
// crash — callers (e.g. set-affected-packages.mjs) already handle null.
|
|
29
|
+
if (err.code === "ENOENT") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const files = names
|
|
36
|
+
.filter((n) => n.endsWith(".md") && n !== "README.md")
|
|
37
|
+
.map((n) => join(changelogDir, n));
|
|
38
|
+
for (const entryPath of files) {
|
|
39
|
+
const { data } = parseFrontmatter(readFileSync(entryPath, "utf8"));
|
|
40
|
+
if (data?.branch === branch) {
|
|
41
|
+
return entryPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|