@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
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 (companion to CHANGELOG.md)
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, Changesets per behavioural change).
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.0.0",
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
- "version": "changeset version"
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,5 @@
1
+ {
2
+ "issueKeys": ["ABC", "XYZ"],
3
+ "linearWorkspaceSlug": "your-workspace-slug",
4
+ "baseBranch": "main"
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "issueKeys": ["ASW", "AKW", "SKW", "SK"],
3
+ "linearWorkspaceSlug": "goose-and-hobbes",
4
+ "baseBranch": "main"
5
+ }
@@ -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
+ }