create-agent-rig 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -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 +11 -3
- package/templates/agent-os/init/CLAUDE.md +11 -3
- 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 +298 -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 +8 -1
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +8 -1
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +28 -3
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +127 -0
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +23 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +8 -0
- package/templates/agent-os/universal/.claude/rules/invariants.md +33 -3
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +19 -1
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +351 -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 +28 -0
- 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 +138 -15
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +394 -46
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +68 -5
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +316 -0
- package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +180 -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 +239 -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 +298 -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 +309 -49
- package/templates/release-ledger.json +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,118 @@ 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.0
|
|
15
|
+
|
|
16
|
+
**The loop now checks its premises against the tracker at three points, and
|
|
17
|
+
the rulebook cannot be edited from an unattended run.** Everything below is what
|
|
18
|
+
a newly scaffolded or `init`ed project receives.
|
|
19
|
+
|
|
20
|
+
**The released-hash table no longer depends on tags** (AR-35). It is built
|
|
21
|
+
from `templates/release-ledger.json` — the commit each version was published
|
|
22
|
+
from — so it now carries 0.5.0's bytes, 0.4.0's real bytes (the stale `v0.4.0`
|
|
23
|
+
tag is reported and ignored) and a 0.2.0 row; the consequence 0.5.0's notes
|
|
24
|
+
state for a rig upgraded without a readable manifest is closed for every
|
|
25
|
+
release the ledger records. This release itself is excluded, as every release
|
|
26
|
+
being prepared is: commit `.claude/.rig-manifest.json`.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Revalidation at SELECT, BEFORE_PR and BEFORE_CLOSE** (AR-133, AR-134,
|
|
31
|
+
AR-135, AR-136). `.claude/scripts/revalidate.mjs` compares the item the run
|
|
32
|
+
took against the tracker's current state — at selection against this run's
|
|
33
|
+
take-up, before a PR against the item and the default branch, and before a
|
|
34
|
+
close against the item's fields and its dependants — and every point records
|
|
35
|
+
one evidence shape in the run directory; `revalidation-report.mjs` reads them
|
|
36
|
+
back. A close now proves it transitioned rather than reporting the write. The
|
|
37
|
+
points themselves have one spelling, `.claude/scripts/lib/revalidation-points.mjs`,
|
|
38
|
+
and the `loop` and `pr-ship` skills are checked against it in both directions
|
|
39
|
+
(AR-137).
|
|
40
|
+
- **`guard-rulebook`** (AR-51): a `PreToolUse` hook that refuses an edit to the
|
|
41
|
+
hooks, their wiring, `.claude/queue.json`, the queue adapters, the router, the
|
|
42
|
+
gate sweep, the rules or `CLAUDE.md` while the unattended flag the `loop` skill
|
|
43
|
+
writes at claim time is on disk (`.claude/scripts/unattended-flag.mjs`), unless
|
|
44
|
+
the item's allow-list names the path. Attended sessions are untouched. Its
|
|
45
|
+
header states its limits, each one under test in the generator.
|
|
46
|
+
- **`doctor`** (AR-5): `node .claude/scripts/doctor.mjs` reads
|
|
47
|
+
`.claude/.rig-manifest.json` and reports every hook the project owns — bytes
|
|
48
|
+
that differ from what the generator installed, or no manifest entry — that has
|
|
49
|
+
no `<hook>.test.mjs` beside it. Exemptions are an explicit list with reasons in
|
|
50
|
+
`.claude/doctor-exemptions.json`, a file the project writes (none ships); a
|
|
51
|
+
doctor that looked nowhere never says GO.
|
|
52
|
+
- **Fan-out coverage is checked, not just recorded** (AR-79, AR-118): `pr-ship`
|
|
53
|
+
compares the reviewers that answered against the route the router gave the
|
|
54
|
+
head, bound to that head; `docs/decisions/gate-coverage.md` records the shape
|
|
55
|
+
and the unreadable states.
|
|
56
|
+
- **Queue items carry more of the tracker's meaning into selection:**
|
|
57
|
+
- an item marked for another repository (`owner-<name>`; `[owner:<name>]` in
|
|
58
|
+
`PLAN.md`) is held, never taken — a checkout names itself in
|
|
59
|
+
`options.owner` (AR-132);
|
|
60
|
+
- the lifecycle vocabulary `keep-core` / `re-scope` / `obsolete` and the
|
|
61
|
+
`parked` pile are read above the adapter seam, and the loop infers none of
|
|
62
|
+
it (AR-144);
|
|
63
|
+
- a proposal the loop files records the commit it was measured against
|
|
64
|
+
(`asOf`), and `hygiene` reports the one git has overtaken (AR-116); it also
|
|
65
|
+
names what it measured and what it inferred, and an inference past the
|
|
66
|
+
measurement is refused at filing (AR-142);
|
|
67
|
+
- the take-up baseline reaches into earlier runs, so a marker the adapter's
|
|
68
|
+
own write produced is not read back as a catch (AR-138, AR-140);
|
|
69
|
+
- `gate-round` refuses to count a round on a checkout that cannot ship, and
|
|
70
|
+
states the cap as a spent count rather than a convergence verdict (AR-141,
|
|
71
|
+
AR-115).
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- **The Jira adapter is harder to knock over** (AR-54): a request timeout that
|
|
76
|
+
stays armed through the body read, transient retry honouring `Retry-After`
|
|
77
|
+
(capped at 60 s), cursor pagination with a stated page cap, a priority-id
|
|
78
|
+
fallback, and a JQL that is always project-qualified — an explicit
|
|
79
|
+
`options.jql` must begin with `project = <KEY>`.
|
|
80
|
+
- **The adapter contract gained `find` and `listProposals`** (AR-135, AR-116)
|
|
81
|
+
and the ticket shape gained `updatedAt`, `owner`, `lifecycle` and `parked`.
|
|
82
|
+
On `jira`, `limit` is now the **page** size, not a result cap.
|
|
83
|
+
- **A close is a close only when the tracker says so** (AR-135): all three
|
|
84
|
+
adapters read the item back and return `transitioned` from what they read,
|
|
85
|
+
instead of from the argument they were given or a `gh` exit code.
|
|
86
|
+
- **`gate-stop-dod` measures the project the hook belongs to, not the cwd**, and
|
|
87
|
+
names the tree in its refusal (AR-119).
|
|
88
|
+
- **`.claude/rules/node-ts.md` names the third state of a PR head** — one that
|
|
89
|
+
gets no workflow run at all — and says it is retriggered per required check,
|
|
90
|
+
never merged on an older head's green (AR-149).
|
|
91
|
+
- **The autonomy and invariants rules state the enforcement they have exactly**:
|
|
92
|
+
`guard-secret-file`'s four blind spots, the unattended flag as what arms
|
|
93
|
+
`guard-rulebook`, and the unbacked-claim rule with its two exits (delete, or
|
|
94
|
+
point at the test).
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **`MultiEdit` and `NotebookEdit` reached every content guard and produced no
|
|
99
|
+
fragment**, so an impure edit to the core through either passed unchecked.
|
|
100
|
+
`hooks/lib/edit-input.mjs` now yields one fragment per edit for both (AR-51).
|
|
101
|
+
- **`manifest.version` is held to the same value check its siblings get**, and
|
|
102
|
+
the comment no longer claims a prerelease the rig never wrote (AR-128).
|
|
103
|
+
- **Hooks resolve the project root inside `main()`**, so a throw there announces
|
|
104
|
+
itself instead of failing open silently (AR-119).
|
|
105
|
+
|
|
106
|
+
- **The upgrade plan's header told you your rig was old when it could not know
|
|
107
|
+
that.** It greeted every rig it could not read a manifest for with "no manifest
|
|
108
|
+
here (a pre-0.4.0 rig)". There are three ways to reach that branch and the
|
|
109
|
+
claim is false for two of them: a manifest you deleted, and one that is on disk
|
|
110
|
+
and voided by its own reader — the case 0.5.0's notes below single out. The
|
|
111
|
+
header now states the condition the code actually has, **no readable manifest
|
|
112
|
+
here**, and offers the three causes without asserting any of them.
|
|
113
|
+
- **`--no-color` is accepted by `upgrade` and `init`**, not only by the
|
|
114
|
+
scaffolder. It was advertised under Options without being scoped to one
|
|
115
|
+
command, and the other two exited 1 with "Unknown option". **Nothing about
|
|
116
|
+
their output changes** — the CLI builds its palette on the `create` path alone,
|
|
117
|
+
so neither command had colour to switch off, and neither read `NO_COLOR`
|
|
118
|
+
either. What changes is that a flag the help offers is no longer a refusal.
|
|
119
|
+
- **The plan's summary now accounts for every action, not four kinds out of
|
|
120
|
+
six.** It counted files to replace, new files, yours-kept and already-current,
|
|
121
|
+
while the plan above it also prints a line for a hook wiring hand-over and for
|
|
122
|
+
a file you removed — so the four buckets could not add up to what was printed.
|
|
123
|
+
Both are counted now, in the order the plan lists them, and they appear only
|
|
124
|
+
when they occurred: a plan without them reads exactly as it did.
|
|
125
|
+
|
|
14
126
|
## 0.5.0
|
|
15
127
|
|
|
16
128
|
**Codex is a harness of this rig now, not a thing you adapt it to.** A generated
|
|
@@ -506,13 +618,27 @@ sometimes earlier (step 6). Everything before that is mechanical:
|
|
|
506
618
|
tree, are in the tarball. This is where scaffolders break, and the git path
|
|
507
619
|
cannot catch it.
|
|
508
620
|
3. Version in `package.json` (and the private inner package, kept in step).
|
|
509
|
-
4.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
621
|
+
4. **Record where the previous release was published from, then regenerate
|
|
622
|
+
the released-hash table.** `templates/release-ledger.json` maps each released
|
|
623
|
+
version to the commit it was published from; the entry for the release
|
|
624
|
+
_before_ this one is written now, because a commit cannot carry its own sha:
|
|
625
|
+
|
|
626
|
+
```sh
|
|
627
|
+
npm view create-agent-rig@<previous> gitHead # → the sha for the ledger
|
|
628
|
+
node scripts/build-hash-history.mjs # rebuilds the table from it
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
The builder reads every `## X.Y.Z` this file lists below the version in
|
|
632
|
+
`package.json` and **refuses, naming the version and that command**, when
|
|
633
|
+
the ledger has no entry for one — it never drops a release silently, since a
|
|
634
|
+
dropped release is one `upgrade` can no longer recognise. A value of `null`
|
|
635
|
+
is the one other answer: the published bytes are not recoverable from git
|
|
636
|
+
(0.1.0 was published from a commit whose `package.json` already read
|
|
637
|
+
0.2.0), so that version deliberately gets no row. Pinned in
|
|
638
|
+
`test/template/hash-history.test.ts` › "throws for a released version the
|
|
639
|
+
ledger does not mention, naming the version and the npm command" and ›
|
|
640
|
+
"points at a commit whose package.json carries that version".
|
|
641
|
+
|
|
516
642
|
5. This file, and `PLAN.md` if the plan's claims changed.
|
|
517
643
|
6. **`pnpm test` again — this run, not step 1, is the one that can catch a
|
|
518
644
|
stale hash table.** The check compares the table against the versions this
|
|
@@ -521,33 +647,13 @@ sometimes earlier (step 6). Everything before that is mechanical:
|
|
|
521
647
|
guard that can only fire after the thing it guards has changed has to be run
|
|
522
648
|
after it.
|
|
523
649
|
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.
|
|
650
|
+
decision, recorded at 0.5.0: the owner publishes by hand and does not tag.
|
|
651
|
+
Since 0.6.0 (AR-35) that costs nothing: the table is built from the ledger in
|
|
652
|
+
step 4, not from tags, and a `v*` tag is neither required nor trusted. One
|
|
653
|
+
that exists and points elsewhere than the ledger — `v0.4.0` does, at 0.3.2's
|
|
654
|
+
content — is printed as a warning by the builder and changes nothing:
|
|
655
|
+
`test/template/hash-history.test.ts` › "builds the table from the ledger
|
|
656
|
+
alone — tags are a warning source, never an input".
|
|
551
657
|
|
|
552
658
|
8. **Owner:** `npm publish`.
|
|
553
659
|
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`, 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
|
|
@@ -130,7 +130,6 @@ All three are one-liners, and all three are inert until you do them.
|
|
|
130
130
|
# the run journal's per-run trace
|
|
131
131
|
.claude/runs/
|
|
132
132
|
```
|
|
133
|
-
|
|
134
133
|
Each comment is on its own line, and that is not formatting: git treats `#`
|
|
135
134
|
as a comment **only at line start**, so a trailing `# …` becomes part of the
|
|
136
135
|
pattern and the line then ignores nothing. It fails silently — you find out
|
|
@@ -145,6 +144,15 @@ All three are one-liners, and all three are inert until you do them.
|
|
|
145
144
|
edited on purpose. `.claude/queue.json` is the opposite: that one is
|
|
146
145
|
configuration and belongs in the repository.
|
|
147
146
|
|
|
147
|
+
4. **`doctor` reads two files this install does not ship.**
|
|
148
|
+
`node .claude/scripts/doctor.mjs` decides who owns each hook from
|
|
149
|
+
`.claude/.rig-manifest.json` — which `init` wrote next to the files it
|
|
150
|
+
installed, so commit it — and reads exemptions from
|
|
151
|
+
`.claude/doctor-exemptions.json`, a file you author (`{ "<path>": "<reason>" }`)
|
|
152
|
+
only when a hook you own is deliberately left without a test neighbour.
|
|
153
|
+
Without the manifest every hook that has no test neighbour reports `unknown`,
|
|
154
|
+
which is not a pass.
|
|
155
|
+
|
|
148
156
|
## The elevated paths of this project
|
|
149
157
|
|
|
150
158
|
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
|
|
@@ -130,7 +130,6 @@ All three are one-liners, and all three are inert until you do them.
|
|
|
130
130
|
# the run journal's per-run trace
|
|
131
131
|
.claude/runs/
|
|
132
132
|
```
|
|
133
|
-
|
|
134
133
|
Each comment is on its own line, and that is not formatting: git treats `#`
|
|
135
134
|
as a comment **only at line start**, so a trailing `# …` becomes part of the
|
|
136
135
|
pattern and the line then ignores nothing. It fails silently — you find out
|
|
@@ -145,6 +144,15 @@ All three are one-liners, and all three are inert until you do them.
|
|
|
145
144
|
edited on purpose. `.claude/queue.json` is the opposite: that one is
|
|
146
145
|
configuration and belongs in the repository.
|
|
147
146
|
|
|
147
|
+
4. **`doctor` reads two files this install does not ship.**
|
|
148
|
+
`node .claude/scripts/doctor.mjs` decides who owns each hook from
|
|
149
|
+
`.claude/.rig-manifest.json` — which `init` wrote next to the files it
|
|
150
|
+
installed, so commit it — and reads exemptions from
|
|
151
|
+
`.claude/doctor-exemptions.json`, a file you author (`{ "<path>": "<reason>" }`)
|
|
152
|
+
only when a hook you own is deliberately left without a test neighbour.
|
|
153
|
+
Without the manifest every hook that has no test neighbour reports `unknown`,
|
|
154
|
+
which is not a pass.
|
|
155
|
+
|
|
148
156
|
## The elevated paths of this project
|
|
149
157
|
|
|
150
158
|
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."
|
|
@@ -55,3 +55,32 @@ gh api "repos/{owner}/{repo}/commits/$SHA/check-runs" \
|
|
|
55
55
|
# must print: success (a result set containing only a scanner is NOT done)
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
**A head that gets no run at all is a third state, not a slow one.** A
|
|
59
|
+
`pull_request` push can register no workflow run and emit no failure signal —
|
|
60
|
+
the head sits with a scanner only, and the poll above waits forever while the
|
|
61
|
+
previous head's green sits one line up in the same PR (AR-149: `69b5d65` on
|
|
62
|
+
#130 got no `ci` run; `8ca26e7` on #134 got `ci` and no `e2e`). Tell "not
|
|
63
|
+
registered" apart from "pending" by asking for runs by head, not by PR:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
gh api "repos/{owner}/{repo}/actions/runs?head_sha=$SHA" \
|
|
67
|
+
-q '.workflow_runs[] | "\(.name) \(.event) \(.status) \(.conclusion)"'
|
|
68
|
+
# one line per workflow that ran for THIS sha; a required workflow missing
|
|
69
|
+
# here after a bounded wait (a few minutes) is not registered, not pending
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
⚠ The names differ in case between the two queries: `workflow_runs[].name`
|
|
73
|
+
is the **workflow** name (`CI`, `E2E`), the check-runs filter above reads the
|
|
74
|
+
**job** name (`ci`, `e2e`). Searching the runs list for `ci` finds nothing.
|
|
75
|
+
|
|
76
|
+
Then the rule, **per required check by name**, never per head: a check with
|
|
77
|
+
no run after the wait is **retriggered**, and the PR is **never merged on an
|
|
78
|
+
older head's green**. The retrigger depends on the workflow — `ci.yml` has no
|
|
79
|
+
`workflow_dispatch`, so an empty commit (`git commit --allow-empty`) is the
|
|
80
|
+
simplest trigger it has; `e2e.yml` has one, so
|
|
81
|
+
`gh workflow run e2e.yml --ref <branch>` re-runs it on the same head. Pinned
|
|
82
|
+
in the generator's `test/template/pr-flow.test.ts` — absent in a generated
|
|
83
|
+
rig — › "node-ts names the head that gets no run at all, and says it is
|
|
84
|
+
retriggered rather than waited on" and › "the no-run branch is stated per
|
|
85
|
+
required check, not per head".
|
|
86
|
+
|
|
@@ -173,7 +173,8 @@ inside the block; both forms are the contract.
|
|
|
173
173
|
}
|
|
174
174
|
],
|
|
175
175
|
"advisories": [],
|
|
176
|
-
"evidence": ["read the validator and both call sites"]
|
|
176
|
+
"evidence": ["read the validator and both call sites"],
|
|
177
|
+
"headSha": "9c1f0a7d4b3e2c5a8f6d0b9e7c4a1f2d3e5b6c70"
|
|
177
178
|
}
|
|
178
179
|
```
|
|
179
180
|
|
|
@@ -187,6 +188,8 @@ inside the block; both forms are the contract.
|
|
|
187
188
|
and the caller is what checks: the `loop` skill runs
|
|
188
189
|
`node .claude/scripts/verdict.mjs check <report> check-premises` on what you
|
|
189
190
|
return. Nothing in this skill runs it — this skill writes nothing at all.
|
|
191
|
+
- **`headSha` is the commit you read** — `git rev-parse HEAD` in the checkout
|
|
192
|
+
you examined. A premise check answers about a tree, and the tree moves.
|
|
190
193
|
|
|
191
194
|
## Examples — the three shapes this actually catches
|
|
192
195
|
|