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.
- package/CHANGELOG.md +170 -34
- package/README.md +12 -6
- package/package.json +1 -1
- package/packages/cli/dist/commands/init.js +6 -3
- package/packages/cli/dist/commands/upgrade.js +2 -2
- package/packages/cli/dist/index.js +46 -8
- package/packages/cli/dist/lib/manifest.js +10 -0
- package/scripts/prepare.mjs +1 -1
- package/templates/agent-os/init/AGENTS.md +15 -5
- package/templates/agent-os/init/CLAUDE.md +15 -5
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +8 -1
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +8 -1
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +8 -1
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +1 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +29 -0
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +4 -1
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +330 -16
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +64 -6
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -1
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +18 -6
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +8 -1
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +42 -17
- package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +2 -1
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +187 -0
- package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +72 -65
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +33 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +13 -3
- package/templates/agent-os/universal/.claude/rules/invariants.md +45 -17
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +20 -1
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +354 -0
- package/templates/agent-os/universal/.claude/scripts/git-env.mjs +1 -0
- package/templates/agent-os/universal/.claude/scripts/lib/gate-coverage.mjs +306 -0
- package/templates/agent-os/universal/.claude/scripts/lib/revalidation-points.mjs +29 -0
- package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +4 -1
- package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +37 -8
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +27 -1
- package/templates/agent-os/universal/.claude/scripts/queue/as-of.mjs +51 -0
- package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +62 -2
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +479 -9
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +89 -15
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +282 -19
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +395 -46
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +68 -5
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +317 -0
- package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +181 -0
- package/templates/agent-os/universal/.claude/scripts/run-state.mjs +101 -3
- package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +15 -8
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +436 -0
- package/templates/agent-os/universal/.claude/scripts/verdict.mjs +101 -4
- package/templates/agent-os/universal/.claude/settings.json +5 -1
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +4 -1
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +330 -16
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +64 -6
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +6 -1
- package/templates/agent-os/universal/AGENTS.md +3 -1
- package/templates/agent-os/universal/CLAUDE.md +3 -1
- package/templates/agent-os/universal/docs/decisions/gate-coverage.md +83 -0
- package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +18 -6
- package/templates/agent-os/universal/layers.json +9 -0
- package/templates/hash-history.json +404 -51
- package/templates/release-ledger.json +10 -0
- package/templates/skeleton/aws-serverless/gitignore +2 -0
- 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.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
|
72
|
-
and a rig from
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
@@ -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
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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
|
|
193
|
-
//
|
|
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
|
-
|
|
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
|
|
58
|
-
// even when it came from `create` —
|
|
59
|
-
// and
|
|
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 (
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
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;
|
package/scripts/prepare.mjs
CHANGED
|
@@ -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
|
-
##
|
|
108
|
+
## Four things this install left for you to finish
|
|
109
109
|
|
|
110
|
-
All
|
|
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. **
|
|
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.
|
|
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
|
-
##
|
|
108
|
+
## Four things this install left for you to finish
|
|
109
109
|
|
|
110
|
-
All
|
|
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. **
|
|
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.
|
|
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."
|