create-agent-rig 0.5.0 → 0.6.1

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 (66) hide show
  1. package/CHANGELOG.md +170 -34
  2. package/README.md +12 -6
  3. package/package.json +1 -1
  4. package/packages/cli/dist/commands/init.js +6 -3
  5. package/packages/cli/dist/commands/upgrade.js +2 -2
  6. package/packages/cli/dist/index.js +46 -8
  7. package/packages/cli/dist/lib/manifest.js +10 -0
  8. package/scripts/prepare.mjs +1 -1
  9. package/templates/agent-os/init/AGENTS.md +15 -5
  10. package/templates/agent-os/init/CLAUDE.md +15 -5
  11. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +8 -1
  12. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +8 -1
  13. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +8 -1
  14. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +1 -1
  15. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +29 -0
  16. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +4 -1
  17. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +330 -16
  18. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +64 -6
  19. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -1
  20. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +18 -6
  21. package/templates/agent-os/universal/.claude/agents/security-scanner.md +8 -1
  22. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +42 -17
  23. package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +2 -1
  24. package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +187 -0
  25. package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +72 -65
  26. package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +33 -0
  27. package/templates/agent-os/universal/.claude/rules/autonomy.md +13 -3
  28. package/templates/agent-os/universal/.claude/rules/invariants.md +45 -17
  29. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +20 -1
  30. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +354 -0
  31. package/templates/agent-os/universal/.claude/scripts/git-env.mjs +1 -0
  32. package/templates/agent-os/universal/.claude/scripts/lib/gate-coverage.mjs +306 -0
  33. package/templates/agent-os/universal/.claude/scripts/lib/revalidation-points.mjs +29 -0
  34. package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +4 -1
  35. package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +37 -8
  36. package/templates/agent-os/universal/.claude/scripts/preflight.mjs +27 -1
  37. package/templates/agent-os/universal/.claude/scripts/queue/as-of.mjs +51 -0
  38. package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +62 -2
  39. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +479 -9
  40. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +89 -15
  41. package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +282 -19
  42. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +395 -46
  43. package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +68 -5
  44. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +317 -0
  45. package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +181 -0
  46. package/templates/agent-os/universal/.claude/scripts/run-state.mjs +101 -3
  47. package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +15 -8
  48. package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +436 -0
  49. package/templates/agent-os/universal/.claude/scripts/verdict.mjs +101 -4
  50. package/templates/agent-os/universal/.claude/settings.json +5 -1
  51. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +4 -1
  52. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +330 -16
  53. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +64 -6
  54. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +1 -1
  55. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +1 -1
  56. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  57. package/templates/agent-os/universal/.codex/hooks.json +6 -1
  58. package/templates/agent-os/universal/AGENTS.md +3 -1
  59. package/templates/agent-os/universal/CLAUDE.md +3 -1
  60. package/templates/agent-os/universal/docs/decisions/gate-coverage.md +83 -0
  61. package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +18 -6
  62. package/templates/agent-os/universal/layers.json +9 -0
  63. package/templates/hash-history.json +404 -51
  64. package/templates/release-ledger.json +10 -0
  65. package/templates/skeleton/aws-serverless/gitignore +2 -0
  66. package/templates/skeleton/node-service/gitignore +2 -0
package/CHANGELOG.md CHANGED
@@ -11,6 +11,148 @@ Numbering is ordinary semver — **additive is a minor, a fix is a patch** — s
11
11
  that "I only take minors" remains a usable policy; 0.3.2 shipped additive
12
12
  content as a patch by the owner's call and stays recorded as one.
13
13
 
14
+ ## 0.6.1
15
+
16
+ **Security and upgrade hardening for the Agent OS shipped by 0.6.0.** This patch
17
+ closes the rulebook, unattended-run and queue-board gaps found while upgrading a
18
+ live generated repository; it adds no dependency and changes no public CLI
19
+ command.
20
+
21
+ ### Security
22
+
23
+ - **`guard-rulebook` now covers the whole shared rulebook and symlink aliases on
24
+ either side of the comparison.** `AGENTS.md` and `.codex/hooks.json` are
25
+ protected alongside the Claude files. Checkout roots and payload paths are
26
+ judged in both their selected and canonical spellings, including a
27
+ payload-only alias.
28
+ - **Queue board names containing terminal control characters are rejected before
29
+ selection, diagnostics or selector writes.** Ordinary names, including names
30
+ with spaces, remain valid; ANSI, OSC, C1 and DEL bytes from repository-owned
31
+ `queue.json` keys can no longer repaint terminal output.
32
+ - **Unattended authorization is checkout-scoped.** Concurrent worktrees derive
33
+ distinct flag paths from canonical checkout identity, legacy machine-wide
34
+ state fails closed, and migration or cleanup refuses when any armed flag cannot
35
+ be removed instead of reporting a partial disarm as success.
36
+
37
+ ### Fixed
38
+
39
+ - The generated prose-reviewer exception for upstream-only tests now applies
40
+ only to manifest-proven generator snapshots and expires on local drift. This
41
+ keeps intentional generator test references available without turning a dead
42
+ reference in an edited downstream rulebook into a pass.
43
+
44
+ ## 0.6.0
45
+
46
+ **The loop now checks its premises against the tracker at three points, and
47
+ the rulebook cannot be edited from an unattended run.** Everything below is what
48
+ a newly scaffolded or `init`ed project receives.
49
+
50
+ **The released-hash table no longer depends on tags** (AR-35). It is built
51
+ from `templates/release-ledger.json` — the commit each version was published
52
+ from — so it now carries 0.5.0's bytes, 0.4.0's real bytes (the stale `v0.4.0`
53
+ tag is reported and ignored) and a 0.2.0 row; the consequence 0.5.0's notes
54
+ state for a rig upgraded without a readable manifest is closed for every
55
+ release the ledger records. This release itself is excluded, as every release
56
+ being prepared is: commit `.claude/.rig-manifest.json`.
57
+
58
+ ### Added
59
+
60
+ - **Revalidation at SELECT, BEFORE_PR and BEFORE_CLOSE** (AR-133, AR-134,
61
+ AR-135, AR-136). `.claude/scripts/revalidate.mjs` compares the item the run
62
+ took against the tracker's current state — at selection against this run's
63
+ take-up, before a PR against the item and the default branch, and before a
64
+ close against the item's fields and its dependants — and every point records
65
+ one evidence shape in the run directory; `revalidation-report.mjs` reads them
66
+ back. A close now proves it transitioned rather than reporting the write. The
67
+ points themselves have one spelling, `.claude/scripts/lib/revalidation-points.mjs`,
68
+ and the `loop` and `pr-ship` skills are checked against it in both directions
69
+ (AR-137).
70
+ - **`guard-rulebook`** (AR-51): a `PreToolUse` hook that refuses an edit to the
71
+ hooks, their wiring, `.claude/queue.json`, the queue adapters, the router, the
72
+ gate sweep, the rules or `CLAUDE.md` while the unattended flag the `loop` skill
73
+ writes at claim time is on disk (`.claude/scripts/unattended-flag.mjs`), unless
74
+ the item's allow-list names the path. Attended sessions are untouched. Its
75
+ header states its limits, each one under test in the generator.
76
+ - **`doctor`** (AR-5): `node .claude/scripts/doctor.mjs` reads
77
+ `.claude/.rig-manifest.json` and reports every hook the project owns — bytes
78
+ that differ from what the generator installed, or no manifest entry — that has
79
+ no `<hook>.test.mjs` beside it. Exemptions are an explicit list with reasons in
80
+ `.claude/doctor-exemptions.json`, a file the project writes (none ships); a
81
+ doctor that looked nowhere never says GO.
82
+ - **Fan-out coverage is checked, not just recorded** (AR-79, AR-118): `pr-ship`
83
+ compares the reviewers that answered against the route the router gave the
84
+ head, bound to that head; `docs/decisions/gate-coverage.md` records the shape
85
+ and the unreadable states.
86
+ - **Queue items carry more of the tracker's meaning into selection:**
87
+ - an item marked for another repository (`owner-<name>`; `[owner:<name>]` in
88
+ `PLAN.md`) is held, never taken — a checkout names itself in
89
+ `options.owner` (AR-132);
90
+ - the lifecycle vocabulary `keep-core` / `re-scope` / `obsolete` and the
91
+ `parked` pile are read above the adapter seam, and the loop infers none of
92
+ it (AR-144);
93
+ - a proposal the loop files records the commit it was measured against
94
+ (`asOf`), and `hygiene` reports the one git has overtaken (AR-116); it also
95
+ names what it measured and what it inferred, and an inference past the
96
+ measurement is refused at filing (AR-142);
97
+ - the take-up baseline reaches into earlier runs, so a marker the adapter's
98
+ own write produced is not read back as a catch (AR-138, AR-140);
99
+ - `gate-round` refuses to count a round on a checkout that cannot ship, and
100
+ states the cap as a spent count rather than a convergence verdict (AR-141,
101
+ AR-115).
102
+
103
+ ### Changed
104
+
105
+ - **The Jira adapter is harder to knock over** (AR-54): a request timeout that
106
+ stays armed through the body read, transient retry honouring `Retry-After`
107
+ (capped at 60 s), cursor pagination with a stated page cap, a priority-id
108
+ fallback, and a JQL that is always project-qualified — an explicit
109
+ `options.jql` must begin with `project = <KEY>`.
110
+ - **The adapter contract gained `find` and `listProposals`** (AR-135, AR-116)
111
+ and the ticket shape gained `updatedAt`, `owner`, `lifecycle` and `parked`.
112
+ On `jira`, `limit` is now the **page** size, not a result cap.
113
+ - **A close is a close only when the tracker says so** (AR-135): all three
114
+ adapters read the item back and return `transitioned` from what they read,
115
+ instead of from the argument they were given or a `gh` exit code.
116
+ - **`gate-stop-dod` measures the project the hook belongs to, not the cwd**, and
117
+ names the tree in its refusal (AR-119).
118
+ - **`.claude/rules/node-ts.md` names the third state of a PR head** — one that
119
+ gets no workflow run at all — and says it is retriggered per required check,
120
+ never merged on an older head's green (AR-149).
121
+ - **The autonomy and invariants rules state the enforcement they have exactly**:
122
+ `guard-secret-file`'s four blind spots, the unattended flag as what arms
123
+ `guard-rulebook`, and the unbacked-claim rule with its two exits (delete, or
124
+ point at the test).
125
+
126
+ ### Fixed
127
+
128
+ - **`MultiEdit` and `NotebookEdit` reached every content guard and produced no
129
+ fragment**, so an impure edit to the core through either passed unchecked.
130
+ `hooks/lib/edit-input.mjs` now yields one fragment per edit for both (AR-51).
131
+ - **`manifest.version` is held to the same value check its siblings get**, and
132
+ the comment no longer claims a prerelease the rig never wrote (AR-128).
133
+ - **Hooks resolve the project root inside `main()`**, so a throw there announces
134
+ itself instead of failing open silently (AR-119).
135
+
136
+ - **The upgrade plan's header told you your rig was old when it could not know
137
+ that.** It greeted every rig it could not read a manifest for with "no manifest
138
+ here (a pre-0.4.0 rig)". There are three ways to reach that branch and the
139
+ claim is false for two of them: a manifest you deleted, and one that is on disk
140
+ and voided by its own reader — the case 0.5.0's notes below single out. The
141
+ header now states the condition the code actually has, **no readable manifest
142
+ here**, and offers the three causes without asserting any of them.
143
+ - **`--no-color` is accepted by `upgrade` and `init`**, not only by the
144
+ scaffolder. It was advertised under Options without being scoped to one
145
+ command, and the other two exited 1 with "Unknown option". **Nothing about
146
+ their output changes** — the CLI builds its palette on the `create` path alone,
147
+ so neither command had colour to switch off, and neither read `NO_COLOR`
148
+ either. What changes is that a flag the help offers is no longer a refusal.
149
+ - **The plan's summary now accounts for every action, not four kinds out of
150
+ six.** It counted files to replace, new files, yours-kept and already-current,
151
+ while the plan above it also prints a line for a hook wiring hand-over and for
152
+ a file you removed — so the four buckets could not add up to what was printed.
153
+ Both are counted now, in the order the plan lists them, and they appear only
154
+ when they occurred: a plan without them reads exactly as it did.
155
+
14
156
  ## 0.5.0
15
157
 
16
158
  **Codex is a harness of this rig now, not a thing you adapt it to.** A generated
@@ -506,13 +648,27 @@ sometimes earlier (step 6). Everything before that is mechanical:
506
648
  tree, are in the tarball. This is where scaffolders break, and the git path
507
649
  cannot catch it.
508
650
  3. Version in `package.json` (and the private inner package, kept in step).
509
- 4. `node scripts/build-hash-history.mjs` regenerate the released-hash table
510
- from the tags **after** the version bump, so the version now shipping is the
511
- first one it excludes. Forgetting it would leave `upgrade` unable to
512
- recognise the previous release. With no tag cut (step 7), what excludes the
513
- shipping version is the absent tag rather than that filter — and the same
514
- absence keeps it out of every later table too, which is the cost step 7
515
- states.
651
+ 4. **Record where the previous release was published from, then regenerate
652
+ the released-hash table.** `templates/release-ledger.json` maps each released
653
+ version to the commit it was published from; the entry for the release
654
+ _before_ this one is written now, because a commit cannot carry its own sha:
655
+
656
+ ```sh
657
+ npm view create-agent-rig@<previous> gitHead # → the sha for the ledger
658
+ node scripts/build-hash-history.mjs # rebuilds the table from it
659
+ ```
660
+
661
+ The builder reads every `## X.Y.Z` this file lists below the version in
662
+ `package.json` and **refuses, naming the version and that command**, when
663
+ the ledger has no entry for one — it never drops a release silently, since a
664
+ dropped release is one `upgrade` can no longer recognise. A value of `null`
665
+ is the one other answer: the published bytes are not recoverable from git
666
+ (0.1.0 was published from a commit whose `package.json` already read
667
+ 0.2.0), so that version deliberately gets no row. Pinned in
668
+ `test/template/hash-history.test.ts` › "throws for a released version the
669
+ ledger does not mention, naming the version and the npm command" and ›
670
+ "points at a commit whose package.json carries that version".
671
+
516
672
  5. This file, and `PLAN.md` if the plan's claims changed.
517
673
  6. **`pnpm test` again — this run, not step 1, is the one that can catch a
518
674
  stale hash table.** The check compares the table against the versions this
@@ -521,33 +677,13 @@ sometimes earlier (step 6). Everything before that is mechanical:
521
677
  guard that can only fire after the thing it guards has changed has to be run
522
678
  after it.
523
679
  7. **Tagging is not part of this project's release process** — standing owner
524
- decision, recorded at 0.5.0: the owner publishes by hand and does not tag. So
525
- this step is deliberately _not_ performed, and the cost is stated here rather
526
- than discovered later.
527
-
528
- Step 4 builds the table from `v*` tags, so an untagged release never enters
529
- it. That is free for the release being prepared and **not free for the one
530
- after it**: step 6 fails as soon as the CHANGELOG lists an untagged release
531
- _below_ the version being prepared, and its message says "stale table", which
532
- running the builder cannot satisfy. Measured against this repository at
533
- 0.5.0 preparation: with `0.5.0` untagged, a `0.5.0` bump passes and an
534
- `0.6.0` bump fails. The check is
535
- `test/template/hash-history.test.ts` › "covers every released version below
536
- the one being prepared". **AR-35 carries the fix**, and which shape it takes
537
- is that item's to decide, not this note's.
538
-
539
- **What a releaser may do when step 6 fails, stated so it is not inferred:**
540
- stop, and land AR-35 first. Cutting the missing tag after the fact is _not_
541
- the sanctioned way out — it would put the table's honesty back on a step this
542
- process does not perform, which is the whole reason the decision is recorded
543
- here.
544
-
545
- If a tag is ever cut anyway, the older warning still applies: check first
546
- that it does not exist (`git ls-remote --tags origin`), because a leftover
547
- from an abandoned attempt is a published ref, deleting or moving one is an
548
- **owner** action, and a tag on the wrong commit makes the next table **name a
549
- version whose bytes it does not carry**. `v0.4.0` is in exactly that state —
550
- it points at 0.3.2's content.
680
+ decision, recorded at 0.5.0: the owner publishes by hand and does not tag.
681
+ Since 0.6.0 (AR-35) that costs nothing: the table is built from the ledger in
682
+ step 4, not from tags, and a `v*` tag is neither required nor trusted. One
683
+ that exists and points elsewhere than the ledger — `v0.4.0` does, at 0.3.2's
684
+ content is printed as a warning by the builder and changes nothing:
685
+ `test/template/hash-history.test.ts` "builds the table from the ledger
686
+ alone tags are a warning source, never an input".
551
687
 
552
688
  8. **Owner:** `npm publish`.
553
689
  9. **Owner:** smoke the published artifact — `npx create-agent-rig@<version>` in
package/README.md CHANGED
@@ -68,12 +68,13 @@ How it knows: `create` and `init` write `.claude/.rig-manifest.json` — the rig
68
68
  version plus a hash per installed file. **Commit it**; without it in the
69
69
  repository the command is blind on CI and on a colleague's machine. Rigs
70
70
  installed before 0.4.0 have no manifest, so the package also carries the hashes
71
- of every **tagged** release (0.3.0 onward — 0.1.0 and 0.2.0 shipped untagged,
72
- and a rig from those reports every file as yours) and recognises a file matching
73
- one of them. Releases from 0.5.0 on ship untagged as well, so the table does not
74
- carry them either: on a rig with no manifest most of their files are reported as yours
75
- rather than refreshed. This is why committing the manifest is the sentence in
76
- bold above and not an aside.
71
+ of every release whose published commit is on record (0.2.0 onward — 0.1.0's
72
+ published bytes are not recoverable, and a rig from it reports every file as
73
+ yours) and recognises a file matching one of them. The record is
74
+ `templates/release-ledger.json`, written at the release _after_ the one it
75
+ describes, so the newest release is never in the table a rig installed from it
76
+ carries one more reason committing the manifest is the sentence in bold above
77
+ and not an aside.
77
78
 
78
79
  `.claude/settings.json` is replaced only when the manifest's recorded hash
79
80
  proves the rig wrote those exact bytes and you have not touched them — the case
@@ -105,6 +106,11 @@ wired by both `.claude/settings.json` and `.codex/hooks.json`:
105
106
  environment access, or a non-allowlisted import into the pure domain core;
106
107
  - **`guard-web-boundary`** — refuses `db`/service imports from the frontend;
107
108
  the web talks to the backend over HTTP only;
109
+ - **`guard-rulebook`** — in an unattended run (a flag file the `loop` skill
110
+ writes at claim time), refuses an edit to the rulebook — hooks, wiring,
111
+ `queue.json` and its board selector, the queue adapters, the router, the gate sweep, the rules,
112
+ `CLAUDE.md` — outside the current item's allow-list; does nothing in an
113
+ attended session.
108
114
  - **`guard-secret-file`** — refuses an edit that writes a credential: either the
109
115
  path names one (`jira.env`, `id_rsa`, anything under `secrets/`) or the text
110
116
  carries a credential VALUE. Both arms read one vocabulary,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agent-rig",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Scaffold a new project with an agent operating system (rules, gates, hooks) and a runnable code skeleton",
5
5
  "keywords": [
6
6
  "create",
@@ -179,9 +179,12 @@ export async function initProject(repoDir, options) {
179
179
  * it wrote; it does not get to re-describe how the rig was installed.
180
180
  *
181
181
  * ⚠ **The limit, stated because the fix reads as wider than it is:** this
182
- * preserves a manifest, so a rig that has none anything from before 0.4.0 —
183
- * still gets `kind: 'init'`, no stacks and an empty region, and the advisory in
184
- * `runInit` stays silent for the same reason. `upgrade`'s `detectInstall`
182
+ * preserves a manifest, so a rig without a READABLE one still gets
183
+ * `kind: 'init'`, no stacks and an empty region, and the advisory in `runInit`
184
+ * stays silent for the same reason. That is three populations, not one: a rig
185
+ * from before 0.4.0 never had a manifest, a deleted manifest is a documented
186
+ * recovery step, and one on disk that `parseManifest` voids reads as absent to
187
+ * `readManifest` alike. `upgrade`'s `detectInstall`
185
188
  * recovers all three from the files on disk, so those values are not
186
189
  * unavailable, only unavailable *here*: reaching for it would point
187
190
  * `commands/init` at `commands/upgrade`, which already imports this module.
@@ -189,8 +189,8 @@ export async function planUpgrade(repoDir, options = {}) {
189
189
  // The one case where the raw name cannot be kept is a directory the manifest
190
190
  // reader would refuse — `My App` produced `{"name":"My App"}`, which
191
191
  // `parseManifest` voids, so the manifest this command exists to write was
192
- // written and immediately unreadable and every later run reported "no
193
- // manifest here (a pre-0.4.0 rig)". The condition is that reader's own
192
+ // written and immediately unreadable, and every later run fell back to
193
+ // matching against released versions. The condition is that reader's own
194
194
  // exported predicate, not a second copy of its rule.
195
195
  //
196
196
  // 🔴 All three branches were bought by a defect, and two of those defects
@@ -38,7 +38,13 @@ async function runInit(rawArgs) {
38
38
  try {
39
39
  ({ values } = parseArgs({
40
40
  args: rawArgs,
41
- options: { 'dry-run': { type: 'boolean' }, force: { type: 'boolean' } },
41
+ // `--no-color` for the same reason it is accepted on `upgrade`: USAGE
42
+ // offers it without scoping it to one command.
43
+ options: {
44
+ 'dry-run': { type: 'boolean' },
45
+ force: { type: 'boolean' },
46
+ 'no-color': { type: 'boolean' },
47
+ },
42
48
  allowPositionals: false,
43
49
  }));
44
50
  }
@@ -54,9 +60,11 @@ async function runInit(rawArgs) {
54
60
  // overlays. Say so before anything is written, so it is visible on --dry-run
55
61
  // too.
56
62
  //
57
- // It is a manifest read, so a pre-0.4.0 rig with no manifest gets no advisory
58
- // even when it came from `create` — the same limit `recordInstall` carries,
59
- // and stated in both places because either one alone reads as wider.
63
+ // It is a manifest read, so any rig without a READABLE manifest gets no
64
+ // advisory even when it came from `create` — one never written, one deleted,
65
+ // and one present but voided by `parseManifest` all reach here alike. The
66
+ // same limit `recordInstall` carries, stated in both places because either
67
+ // one alone reads as wider.
60
68
  const existing = await readManifest(cwd);
61
69
  const plan = await planInit(cwd);
62
70
  process.stdout.write(`agent-rig init — process layer into ${cwd}\n\n` +
@@ -106,7 +114,7 @@ function renderUpgradePlan(repoDir, plan) {
106
114
  const lines = [
107
115
  `agent-rig upgrade — ${plan.kind} rig in ${repoDir}`,
108
116
  plan.bootstrapped
109
- ? ` no manifest here (a pre-0.4.0 rig) — matching files against released versions`
117
+ ? ` no readable manifest here (deleted, never written, or unparseable) — matching files against released versions`
110
118
  : ` installed by ${plan.fromVersion}`,
111
119
  ` upgrading to ${plan.toVersion}`,
112
120
  '',
@@ -120,9 +128,29 @@ function renderUpgradePlan(repoDir, plan) {
120
128
  }
121
129
  }
122
130
  }
123
- const unchanged = of('unchanged').length;
131
+ // Every one of `UpgradeVerdict`'s six members is accounted for here.
132
+ // `wiring` and `deleted` each print their own line and were in none of the
133
+ // buckets, so a reader counted lines and was told a smaller number.
134
+ // (`unchanged` is counted and prints nothing — the sum is over actions, not
135
+ // over printed lines.) The two appear only when they occurred, so a plan
136
+ // without them renders exactly as it always has. Pinned by, in cli-report.test.ts,
137
+ // "renders a plan with no wiring action exactly as it does today".
138
+ // `deleted` before `wiring`, the relative order the plan prints them in.
139
+ // ⚠ Only their order relative to EACH OTHER matches: the plan prints
140
+ // `deleted` before `conflict` and the summary prints it after, so this is not
141
+ // a plan-ordered line. Pinned by, in cli-report.test.ts,
142
+ // "lists the two occasional buckets in the order the plan prints them".
143
+ const occasional = [
144
+ ['deleted', (n) => `${n} you removed (left removed)`],
145
+ ['wiring', (n) => `${n} wiring handed over`],
146
+ ];
147
+ const extra = occasional
148
+ .map(([verdict, phrase]) => [of(verdict).length, phrase])
149
+ .filter(([count]) => count > 0)
150
+ .map(([count, phrase]) => phrase(count));
124
151
  lines.push('', ` ${of('update').length} to replace, ${of('new').length} new, ` +
125
- `${of('conflict').length} yours (kept), ${unchanged} already current`);
152
+ `${of('conflict').length} yours (kept), ` +
153
+ [...extra, `${of('unchanged').length} already current`].join(', '));
126
154
  return `${lines.join('\n')}\n`;
127
155
  }
128
156
  async function runUpgrade(rawArgs) {
@@ -130,7 +158,17 @@ async function runUpgrade(rawArgs) {
130
158
  try {
131
159
  ({ values } = parseArgs({
132
160
  args: rawArgs,
133
- options: { 'dry-run': { type: 'boolean' }, yes: { type: 'boolean' } },
161
+ // `--no-color` is advertised in USAGE without scoping it to one command, so
162
+ // every command accepts it. Refusing a flag the help offers costs the
163
+ // reader more than honouring it costs us — and honouring it is only a
164
+ // parse here, because the sole palette lives on the `create` path below.
165
+ // Pinned by, in cli-report.test.ts,
166
+ // "upgrade accepts --no-color and prints plain output".
167
+ options: {
168
+ 'dry-run': { type: 'boolean' },
169
+ yes: { type: 'boolean' },
170
+ 'no-color': { type: 'boolean' },
171
+ },
134
172
  allowPositionals: false,
135
173
  }));
136
174
  }
@@ -44,6 +44,16 @@ export function parseManifest(raw) {
44
44
  const m = parsed;
45
45
  if (typeof m.version !== 'string')
46
46
  return null;
47
+ // The same value check its siblings get (AR-128). `version` is printed raw
48
+ // in the upgrade plan header — `installed by ${plan.fromVersion}`, the screen
49
+ // read immediately before `--yes` — so a version carrying a newline or an
50
+ // ANSI escape could forge plan lines the CLI never composed. A version this
51
+ // rig writes is the package's own (`0.5.0` today; a prerelease such as
52
+ // `0.6.0-rc.1` would also pass), which the substitution whitelist admits;
53
+ // semver build metadata (`+`) is not, and a manifest carrying one is voided
54
+ // rather than printed — no released version has carried one.
55
+ if (!isSafeSubstitutionValue(m.version))
56
+ return null;
47
57
  if (m.kind !== 'create' && m.kind !== 'init')
48
58
  return null;
49
59
  const project = m.project;
@@ -36,7 +36,7 @@ export const gitConfigEnv = (env = process.env) => {
36
36
 
37
37
  function main() {
38
38
  // 1. Wire up the pre-commit hook when working inside the git checkout.
39
- if (existsSync(path.join(root, '.git'))) {
39
+ if (!process.env.CI && existsSync(path.join(root, '.git'))) {
40
40
  spawnSync('git', ['config', 'core.hooksPath', '.husky'], {
41
41
  cwd: root,
42
42
  env: gitConfigEnv(),
@@ -105,9 +105,9 @@ it a hook via the `new-invariant` skill.
105
105
  this repository once it has a remote. An empty queue **ends the session**; it is
106
106
  never a cue to invent work, and the agent never files its own work items.
107
107
 
108
- ## Three things this install left for you to finish
108
+ ## Four things this install left for you to finish
109
109
 
110
- All three are one-liners, and all three are inert until you do them.
110
+ All four are one-liners, and all four are inert until you do them.
111
111
 
112
112
  1. **The Definition-of-Done gate has nothing to run.** `gate-stop-dod` executes
113
113
  the commands listed in `.claude/hooks/dod-checks.json`, and `init` ships no
@@ -116,13 +116,15 @@ All three are one-liners, and all three are inert until you do them.
116
116
  no-op, and the Definition of Done is back to being a wish.
117
117
  2. **The elevated-path list below is a seed, not a survey.** It names only what
118
118
  every repo has. Everything else is yours to add.
119
- 3. **Four runtime paths need a `.gitignore` line each**, and `init` cannot add
119
+ 3. **Five runtime paths need a `.gitignore` line each**, and `init` cannot add
120
120
  them — it installs into your repository and does not edit files it did not
121
- bring. Add all four:
121
+ bring. If any are missing, add only the missing entries:
122
122
 
123
123
  ```
124
124
  # the tier the last close recorded
125
125
  .claude/queue.state.json
126
+ # the board this checkout runs on, when the config declares several
127
+ .claude/queue.board
126
128
  # gate rounds, one count per branch
127
129
  .claude/gate-rounds.json
128
130
  # task worktrees
@@ -130,7 +132,6 @@ All three are one-liners, and all three are inert until you do them.
130
132
  # the run journal's per-run trace
131
133
  .claude/runs/
132
134
  ```
133
-
134
135
  Each comment is on its own line, and that is not formatting: git treats `#`
135
136
  as a comment **only at line start**, so a trailing `# …` becomes part of the
136
137
  pattern and the line then ignores nothing. It fails silently — you find out
@@ -145,6 +146,15 @@ All three are one-liners, and all three are inert until you do them.
145
146
  edited on purpose. `.claude/queue.json` is the opposite: that one is
146
147
  configuration and belongs in the repository.
147
148
 
149
+ 4. **`doctor` reads two files this install does not ship.**
150
+ `node .claude/scripts/doctor.mjs` decides who owns each hook from
151
+ `.claude/.rig-manifest.json` — which `init` wrote next to the files it
152
+ installed, so commit it — and reads exemptions from
153
+ `.claude/doctor-exemptions.json`, a file you author (`{ "<path>": "<reason>" }`)
154
+ only when a hook you own is deliberately left without a test neighbour.
155
+ Without the manifest every hook that has no test neighbour reports `unknown`,
156
+ which is not a pass.
157
+
148
158
  ## The elevated paths of this project
149
159
 
150
160
  Tier 2 in `.claude/rules/autonomy.md` names *kinds* of change. This block names
@@ -105,9 +105,9 @@ it a hook via the `new-invariant` skill.
105
105
  this repository once it has a remote. An empty queue **ends the session**; it is
106
106
  never a cue to invent work, and the agent never files its own work items.
107
107
 
108
- ## Three things this install left for you to finish
108
+ ## Four things this install left for you to finish
109
109
 
110
- All three are one-liners, and all three are inert until you do them.
110
+ All four are one-liners, and all four are inert until you do them.
111
111
 
112
112
  1. **The Definition-of-Done gate has nothing to run.** `gate-stop-dod` executes
113
113
  the commands listed in `.claude/hooks/dod-checks.json`, and `init` ships no
@@ -116,13 +116,15 @@ All three are one-liners, and all three are inert until you do them.
116
116
  no-op, and the Definition of Done is back to being a wish.
117
117
  2. **The elevated-path list below is a seed, not a survey.** It names only what
118
118
  every repo has. Everything else is yours to add.
119
- 3. **Four runtime paths need a `.gitignore` line each**, and `init` cannot add
119
+ 3. **Five runtime paths need a `.gitignore` line each**, and `init` cannot add
120
120
  them — it installs into your repository and does not edit files it did not
121
- bring. Add all four:
121
+ bring. If any are missing, add only the missing entries:
122
122
 
123
123
  ```
124
124
  # the tier the last close recorded
125
125
  .claude/queue.state.json
126
+ # the board this checkout runs on, when the config declares several
127
+ .claude/queue.board
126
128
  # gate rounds, one count per branch
127
129
  .claude/gate-rounds.json
128
130
  # task worktrees
@@ -130,7 +132,6 @@ All three are one-liners, and all three are inert until you do them.
130
132
  # the run journal's per-run trace
131
133
  .claude/runs/
132
134
  ```
133
-
134
135
  Each comment is on its own line, and that is not formatting: git treats `#`
135
136
  as a comment **only at line start**, so a trailing `# …` becomes part of the
136
137
  pattern and the line then ignores nothing. It fails silently — you find out
@@ -145,6 +146,15 @@ All three are one-liners, and all three are inert until you do them.
145
146
  edited on purpose. `.claude/queue.json` is the opposite: that one is
146
147
  configuration and belongs in the repository.
147
148
 
149
+ 4. **`doctor` reads two files this install does not ship.**
150
+ `node .claude/scripts/doctor.mjs` decides who owns each hook from
151
+ `.claude/.rig-manifest.json` — which `init` wrote next to the files it
152
+ installed, so commit it — and reads exemptions from
153
+ `.claude/doctor-exemptions.json`, a file you author (`{ "<path>": "<reason>" }`)
154
+ only when a hook you own is deliberately left without a test neighbour.
155
+ Without the manifest every hook that has no test neighbour reports `unknown`,
156
+ which is not a pass.
157
+
148
158
  ## The elevated paths of this project
149
159
 
150
160
  Tier 2 in `.claude/rules/autonomy.md` names *kinds* of change. This block names
@@ -74,7 +74,8 @@ the next selection reads the verdict.
74
74
  }
75
75
  ],
76
76
  "advisories": [],
77
- "evidence": ["stack LastUpdatedTime is this deploy", "DLQ depth 0"]
77
+ "evidence": ["stack LastUpdatedTime is this deploy", "DLQ depth 0"],
78
+ "headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
78
79
  }
79
80
  ```
80
81
 
@@ -88,6 +89,12 @@ the next selection reads the verdict.
88
89
  deploy runs `node .claude/scripts/verdict.mjs check <report>
89
90
  post-deploy-verify` before it retypes the word into `run-state.mjs`. This
90
91
  skill runs nothing — it is read-only by construction.
92
+ - **`headSha` is the commit that was deployed** — `git rev-parse HEAD` in the
93
+ checkout the deploy went out from. ⚠ **Nothing reads it yet**: `run-state.mjs`
94
+ stores the word alone, so the commit lives in this block and nowhere else.
95
+ Write it anyway — the pairing is what a later reader needs to tell a stale
96
+ `HEALTHY` from a current one, and `HEALTHY` is the only thing that clears the
97
+ `REGRESSION` latch the next selection reads.
91
98
 
92
99
  ## Boundaries
93
100
 
@@ -67,7 +67,8 @@ nothing after it. It is what the calling gate reads.
67
67
  }
68
68
  ],
69
69
  "advisories": [],
70
- "evidence": ["cdk diff against the deployed stage"]
70
+ "evidence": ["cdk diff against the deployed stage"],
71
+ "headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
71
72
  }
72
73
  ```
73
74
 
@@ -78,3 +79,9 @@ nothing after it. It is what the calling gate reads.
78
79
  - A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:
79
80
  `node .claude/scripts/verdict.mjs check <report> cdk-diff-reviewer` is what
80
81
  refuses them.
82
+ - **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the
83
+ checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage
84
+ <commit>` tell "this gate answered for the commit being merged" from "it
85
+ answered two pushes ago". A verdict naming no commit is counted as neither
86
+ covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook
87
+ runs that check, so a session that skips the gate skips this with it.
@@ -74,7 +74,8 @@ the next selection reads the verdict.
74
74
  }
75
75
  ],
76
76
  "advisories": [],
77
- "evidence": ["stack LastUpdatedTime is this deploy", "DLQ depth 0"]
77
+ "evidence": ["stack LastUpdatedTime is this deploy", "DLQ depth 0"],
78
+ "headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
78
79
  }
79
80
  ```
80
81
 
@@ -88,6 +89,12 @@ the next selection reads the verdict.
88
89
  deploy runs `node .claude/scripts/verdict.mjs check <report>
89
90
  post-deploy-verify` before it retypes the word into `run-state.mjs`. This
90
91
  skill runs nothing — it is read-only by construction.
92
+ - **`headSha` is the commit that was deployed** — `git rev-parse HEAD` in the
93
+ checkout the deploy went out from. ⚠ **Nothing reads it yet**: `run-state.mjs`
94
+ stores the word alone, so the commit lives in this block and nowhere else.
95
+ Write it anyway — the pairing is what a later reader needs to tell a stale
96
+ `HEALTHY` from a current one, and `HEALTHY` is the only thing that clears the
97
+ `REGRESSION` latch the next selection reads.
91
98
 
92
99
  ## Boundaries
93
100
 
@@ -1,4 +1,4 @@
1
1
  name = "cdk-diff-reviewer"
2
2
  description = "Reviews an infrastructure change via `cdk diff` BEFORE any deploy. MUST run on every change under infra/ — a deploy without this review is a Never-tier action. Read-only; findings gate the deploy."
3
3
  sandbox_mode = "read-only"
4
- developer_instructions = "You review what a deploy would actually do to running infrastructure. Your\ninput is the change under `infra/` **and** the synthesized diff (`cdk diff`,\nrun it yourself); your output is a verdict. You never fix and never deploy.\n\n## How you work\n\n1. Run `cdk diff` (and read the changed `infra/` sources for intent). The diff\n is the truth: review what CloudFormation will do, not what the TypeScript\n looks like it does.\n2. Walk every resource change and flag it **by named rule** (below). Findings\n come as **BLOCKERS first, then nits** — one list each, with the resource\n and the rule it violates.\n3. Your message IS the review, not a summary of it: every finding carries the\n resource, the change, the rule, and the smallest fix. End with the verdict\n block below — `SHIP` where `DEPLOY: OK` used to be, `HOLD` where\n `DEPLOY: BLOCKED` did.\n\n## Named rules — blockers\n\n- **IAM broadening.** Any policy gaining actions, resources widening to `*`,\n or a grant that outruns what a usecase does today. Least privilege is added\n in the same PR as the need, never \"for later\".\n- **Data loss paths.** A stateful resource (table, bucket, queue) being\n replaced, deleted, or flipping its RemovalPolicy toward DESTROY.\n Logical-id renames on stateful resources are replacements in disguise.\n- **Safety-net removal.** A DLQ detached, an alarm deleted or loosened, a\n retry budget widened to infinity, a dead-letter retention shortened.\n- **Blast-radius growth.** New public surface (endpoints, permissions to\n external principals), broadened network access, cross-stack exports that\n make future changes harder to reverse.\n- **Cost-relevant flips.** On-demand → provisioned capacity, log retention to\n \"forever\", memory/timeout jumps with no stated reason.\n\n## Nits (report, do not block)\n\nNaming drift, missing descriptions, constructs that could use the narrower\ngrant helper, duplication between stacks.\n\n## Boundaries\n\n- Read-only: you run `cdk diff` and read code; you never run `cdk deploy`,\n never edit files, never mutate AWS state.\n- An empty diff is a real finding too — say \"no infrastructure change\" and\n return `SHIP`, so the gate leaves a trace either way.\n\n## The verdict block\n\nEnd your report with **exactly one** fenced `json` block of this shape, and\nnothing after it. It is what the calling gate reads.\n\n```json\n{\n \"gate\": \"cdk-diff-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"infra/lib/api-stack.ts\",\n \"line\": 88,\n \"rule\": \"data loss\",\n \"note\": \"the table's RemovalPolicy went to DESTROY — replacement drops it\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"cdk diff against the deployed stage\"]\n}\n```\n\n- `verdict` is `SHIP` (nothing blocking, including an empty diff), `HOLD`, or\n `NOT_APPLICABLE` when the change touches no infrastructure at all.\n- Every blocker names the `rule` it violates, with `file` and `line` when it has\n a location and neither when it does not.\n- A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:\n `node .claude/scripts/verdict.mjs check <report> cdk-diff-reviewer` is what\n refuses them."
4
+ developer_instructions = "You review what a deploy would actually do to running infrastructure. Your\ninput is the change under `infra/` **and** the synthesized diff (`cdk diff`,\nrun it yourself); your output is a verdict. You never fix and never deploy.\n\n## How you work\n\n1. Run `cdk diff` (and read the changed `infra/` sources for intent). The diff\n is the truth: review what CloudFormation will do, not what the TypeScript\n looks like it does.\n2. Walk every resource change and flag it **by named rule** (below). Findings\n come as **BLOCKERS first, then nits** — one list each, with the resource\n and the rule it violates.\n3. Your message IS the review, not a summary of it: every finding carries the\n resource, the change, the rule, and the smallest fix. End with the verdict\n block below — `SHIP` where `DEPLOY: OK` used to be, `HOLD` where\n `DEPLOY: BLOCKED` did.\n\n## Named rules — blockers\n\n- **IAM broadening.** Any policy gaining actions, resources widening to `*`,\n or a grant that outruns what a usecase does today. Least privilege is added\n in the same PR as the need, never \"for later\".\n- **Data loss paths.** A stateful resource (table, bucket, queue) being\n replaced, deleted, or flipping its RemovalPolicy toward DESTROY.\n Logical-id renames on stateful resources are replacements in disguise.\n- **Safety-net removal.** A DLQ detached, an alarm deleted or loosened, a\n retry budget widened to infinity, a dead-letter retention shortened.\n- **Blast-radius growth.** New public surface (endpoints, permissions to\n external principals), broadened network access, cross-stack exports that\n make future changes harder to reverse.\n- **Cost-relevant flips.** On-demand → provisioned capacity, log retention to\n \"forever\", memory/timeout jumps with no stated reason.\n\n## Nits (report, do not block)\n\nNaming drift, missing descriptions, constructs that could use the narrower\ngrant helper, duplication between stacks.\n\n## Boundaries\n\n- Read-only: you run `cdk diff` and read code; you never run `cdk deploy`,\n never edit files, never mutate AWS state.\n- An empty diff is a real finding too — say \"no infrastructure change\" and\n return `SHIP`, so the gate leaves a trace either way.\n\n## The verdict block\n\nEnd your report with **exactly one** fenced `json` block of this shape, and\nnothing after it. It is what the calling gate reads.\n\n```json\n{\n \"gate\": \"cdk-diff-reviewer\",\n \"verdict\": \"HOLD\",\n \"blockers\": [\n {\n \"file\": \"infra/lib/api-stack.ts\",\n \"line\": 88,\n \"rule\": \"data loss\",\n \"note\": \"the table's RemovalPolicy went to DESTROY — replacement drops it\"\n }\n ],\n \"advisories\": [],\n \"evidence\": [\"cdk diff against the deployed stage\"],\n \"headSha\": \"9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70\"\n}\n```\n\n- `verdict` is `SHIP` (nothing blocking, including an empty diff), `HOLD`, or\n `NOT_APPLICABLE` when the change touches no infrastructure at all.\n- Every blocker names the `rule` it violates, with `file` and `line` when it has\n a location and neither when it does not.\n- A `HOLD` naming no blocker is **refused**, and so is a `SHIP` carrying one:\n `node .claude/scripts/verdict.mjs check <report> cdk-diff-reviewer` is what\n refuses them.\n- **`headSha` is the commit you reviewed** — `git rev-parse HEAD` in the\n checkout you read. It is what lets `node .claude/scripts/verdict.mjs coverage\n <commit>` tell \"this gate answered for the commit being merged\" from \"it\n answered two pushes ago\". A verdict naming no commit is counted as neither\n covered nor missing, so `pr-ship` holds on it — and only `pr-ship`: no hook\n runs that check, so a session that skips the gate skips this with it."