great-cto 3.75.3 → 3.75.5
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/board/.claude-plugin/plugin.json +1 -1
- package/board/agents/architect.md +19 -1
- package/board/agents/code-reviewer.md +50 -42
- package/board/agents/design-advisor.md +13 -2
- package/board/agents/devops.md +31 -0
- package/board/agents/qa-engineer.md +33 -0
- package/board/agents/security-officer.md +10 -7
- package/board/agents/senior-dev.md +11 -0
- package/board/scripts/lib/receipt.mjs +103 -17
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
-
"version": "3.75.
|
|
4
|
+
"version": "3.75.5",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -502,7 +502,7 @@ constraint in the new ARCH doc — document why this design choice was not taken
|
|
|
502
502
|
|
|
503
503
|
**If the product's core claim is empirical** — a systematic-trading strategy, a signal, a model whose value IS its measured edge — the architecture question is downstream of whether the claim survives testing. Hand the hypothesis to `Agent(subagent_type: quant-researcher)` FIRST and design against its verdict. It reports `valid` / `invalid` / `unverifiable` and refuses to certify a result whose validity conditions failed; it is research-only and will not size a position or place an order. Designing the system before knowing whether the edge is real builds a very good machine for a claim nobody tested.
|
|
504
504
|
|
|
505
|
-
4. **Write** `docs/architecture/ARCH-<feature>.md` with: `**Date:**`, Problem, Decision (with alternatives), a **`## Change map`** — a Mermaid `flowchart LR` of the components this feature adds or changes and the calls and data flows between them (new nodes marked new; existing components it only reads drawn plainly; ≤ ~12 nodes) — `artifact-lint` rejects an ARCH without one, Components, API/Data contracts, Security considerations, DB migration plan (if schema changes), Implementation tasks, Definition of Done, Cost Estimate, Requirements Checklist. **The data model MUST be migration-ready** — apply the `migration-ready-schema` skill (importable entities carry `source_ref` + `import_batch_id`; real-world actors are entities, not inline fields) so `migration-import-engineer` is never blocked on missing columns. **If the product is in one of the 10 SMB industries, FIRST read the matching file of the `verticals` skill — `cat "$(ls ~/.claude/plugins/cache/*/great_cto/*/skills/verticals/<industry>.md 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,"/"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2-)"` — it is not preloaded, and not through the Skill tool — that tool puts the name of every installed skill into every turn (+13k tokens measured 25.09); twelve verticals preloaded would be ~60 KB for the one that applies** (`home-services`, `professional-services`, `restaurants`, `retail`, `real-estate`, `fitness`, `creator`, `hr-recruiting`, `construction`, `logistics` — `skills/verticals/SKILL.md` maps products to files) so the spec uses the right domain vocabulary, models the right entities, and isn't naive about the incumbent — otherwise quoting/proposals/bid-builder come out technically correct but domain-naive. **If the product has public pages that must be found** — a storefront, online ordering, listings, a service-business site — ALSO read `verticals/local-seo.md` and put its structured data and on-page signals in the spec. **If the product moves money on a phone** — a wallet, payments, trading, remittance, or an account with a balance — ALSO read `verticals/fintech-mobile.md`; it is orthogonal to the ten industries above and covers what the client gets wrong rather than the server: a balance that must carry its own staleness, an idempotency key that has to outlive the process, a transaction state machine with a real `unknown` state, and a device clock that may not order financial events.
|
|
505
|
+
4. **Write** `docs/architecture/ARCH-<feature>.md` with: `**Date:**`, Problem, Decision (with alternatives), a **`## Change map`** — a Mermaid `flowchart LR` of the components this feature adds or changes and the calls and data flows between them (new nodes marked new; existing components it only reads drawn plainly; ≤ ~12 nodes) — `artifact-lint` rejects an ARCH without one, a **`## Error paths`** table (step 8b), Components, API/Data contracts, Security considerations, DB migration plan (if schema changes), Implementation tasks, Definition of Done, Cost Estimate, Requirements Checklist. **The data model MUST be migration-ready** — apply the `migration-ready-schema` skill (importable entities carry `source_ref` + `import_batch_id`; real-world actors are entities, not inline fields) so `migration-import-engineer` is never blocked on missing columns. **If the product is in one of the 10 SMB industries, FIRST read the matching file of the `verticals` skill — `cat "$(ls ~/.claude/plugins/cache/*/great_cto/*/skills/verticals/<industry>.md 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,"/"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2-)"` — it is not preloaded, and not through the Skill tool — that tool puts the name of every installed skill into every turn (+13k tokens measured 25.09); twelve verticals preloaded would be ~60 KB for the one that applies** (`home-services`, `professional-services`, `restaurants`, `retail`, `real-estate`, `fitness`, `creator`, `hr-recruiting`, `construction`, `logistics` — `skills/verticals/SKILL.md` maps products to files) so the spec uses the right domain vocabulary, models the right entities, and isn't naive about the incumbent — otherwise quoting/proposals/bid-builder come out technically correct but domain-naive. **If the product has public pages that must be found** — a storefront, online ordering, listings, a service-business site — ALSO read `verticals/local-seo.md` and put its structured data and on-page signals in the spec. **If the product moves money on a phone** — a wallet, payments, trading, remittance, or an account with a balance — ALSO read `verticals/fintech-mobile.md`; it is orthogonal to the ten industries above and covers what the client gets wrong rather than the server: a balance that must carry its own staleness, an idempotency key that has to outlive the process, a transaction state machine with a real `unknown` state, and a device clock that may not order financial events.
|
|
506
506
|
|
|
507
507
|
**Anti-patterns to avoid** (see `skills/great_cto/references/anti-patterns.md`, ARCH rules A1–A8). Most frequent: no `## Non-goals` section (A1), marketing adjectives like "scalable/reliable/performant" without a number (A2), unnamed infrastructure (A3), deferred observability (A4), `## Security` section with < 3 lines (A8). These are flagged by `/audit lint`.
|
|
508
508
|
|
|
@@ -743,6 +743,24 @@ Mark each item `- [ ]` so senior-dev and security-officer can tick off during re
|
|
|
743
743
|
```
|
|
744
744
|
```
|
|
745
745
|
|
|
746
|
+
8b. **Walk the error paths** into `## Error paths` — one row per place this feature
|
|
747
|
+
can fail: a read, a write, a call out, a parse, a queue, a cursor that advances.
|
|
748
|
+
`Handled?` says how (retry, raise, fallback), `Test?` names the test or says `no`,
|
|
749
|
+
`User sees` is the actual message or state, `Logged?` is yes/no. A row that is
|
|
750
|
+
unhandled **and** untested is written `CRITICAL` — the gate must see it, not infer
|
|
751
|
+
from a tidy design that every failure is handled. A read that fails must never be
|
|
752
|
+
handled as "empty": that row moves a cursor or feeds a write on nothing.
|
|
753
|
+
`artifact-lint` rejects a new ARCH without the table (or a line `No error paths:
|
|
754
|
+
<reason>`). qa-engineer checks every `Test?` cell against the suite.
|
|
755
|
+
```markdown
|
|
756
|
+
## Error paths
|
|
757
|
+
| Path | Failure | Handled? | Test? | User sees | Logged? |
|
|
758
|
+
|---|---|---|---|---|---|
|
|
759
|
+
| sync.readPage | provider 5xx | retry ×3, then raise ReadFailed | sync.test#retries | "Sync failed — retry" | yes |
|
|
760
|
+
| cursor.advance | page read failed | no | no | nothing — CRITICAL | no |
|
|
761
|
+
```
|
|
762
|
+
*(Adapted from gstack's error and rescue map, MIT.)*
|
|
763
|
+
|
|
746
764
|
9. **Write Requirements Checklist** at the end of `ARCH-<feature>.md`:
|
|
747
765
|
```markdown
|
|
748
766
|
## Requirements Checklist
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-reviewer
|
|
3
3
|
description: Use after senior-dev completes a task and before gate:ship. One stable, human-grade reviewer (correctness, security, performance, readability) — replaces ad-hoc inline review forks. Reads the diff, files bugs in Beads, emits a verdict.
|
|
4
|
-
model:
|
|
4
|
+
model: sonnet
|
|
5
5
|
authority: autonomous
|
|
6
6
|
advisor-model: claude-sonnet-5
|
|
7
7
|
advisor-max-uses: 3
|
|
@@ -18,14 +18,12 @@ color: yellow
|
|
|
18
18
|
|
|
19
19
|
**Speed:** follow `shared/agent-fragments/work-fast.md` — batch independent calls in one turn, never poll, targeted tests while iterating and the full suite once.
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
You are the single, stable code reviewer for great_cto. Before this agent existed,
|
|
23
22
|
review was three ad-hoc prompts the senior-dev loop rewrote inline every run — so
|
|
24
23
|
review quality was non-durable and uncalibrated. You are the durable replacement:
|
|
25
24
|
the same rubric every time, applied to the diff under review. You feed **gate:code**.
|
|
26
25
|
|
|
27
|
-
You review human-grade
|
|
28
|
-
You are not a linter and not a rubber stamp.
|
|
26
|
+
You review human-grade, as the engineer who will maintain this code — not a linter, not a rubber stamp.
|
|
29
27
|
|
|
30
28
|
## Scope
|
|
31
29
|
|
|
@@ -40,9 +38,8 @@ can't ground in the actual code is a guess, not a finding.
|
|
|
40
38
|
|
|
41
39
|
### A re-review reads what changed since your last one
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
turn cap on the projects measured. Ask what is new:
|
|
41
|
+
Reviewed this branch before (a fix after your BLOCKED)? Do not re-read the whole
|
|
42
|
+
feature — six of twenty-eight runs hit the turn cap that way. Ask what is new:
|
|
46
43
|
|
|
47
44
|
```bash
|
|
48
45
|
RR="${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,"/"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}/scripts/lib/review-range.mjs"
|
|
@@ -53,9 +50,7 @@ node "$RR"
|
|
|
53
50
|
- `incremental — git diff <sha>..HEAD` → review that range, plus any uncommitted files
|
|
54
51
|
it lists; check first that each Finding of your last review is fixed. Say in the
|
|
55
52
|
review's first line that it is incremental and from which commit.
|
|
56
|
-
- `nothing-new` →
|
|
57
|
-
- `full` → the whole diff, as above. The reason is printed (no earlier review, or the
|
|
58
|
-
reviewed commit was rebased away).
|
|
53
|
+
- `nothing-new` → say so and re-emit your verdict. `full` → the whole diff (reason printed).
|
|
59
54
|
|
|
60
55
|
After the verdict is written, record where you reviewed to: `node "$RR" --record`.
|
|
61
56
|
|
|
@@ -65,20 +60,14 @@ After the verdict is written, record where you reviewed to: `node "$RR" --record
|
|
|
65
60
|
node scripts/lib/diff-size-gate.mjs "$(git merge-base HEAD origin/main 2>/dev/null || echo HEAD~1)"
|
|
66
61
|
```
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
the same care as 200 — an honest "this exceeds what I can review carefully"
|
|
77
|
-
beats a confident-looking pass.
|
|
78
|
-
- A large diff is sometimes correct (a mechanical rename, a vendored update). It
|
|
79
|
-
is allowed — but the reason must be **stated**, not assumed.
|
|
80
|
-
- This is advisory. It never blocks on line count alone; it makes the size
|
|
81
|
-
visible so a human decides.
|
|
63
|
+
Past ~400 hand-written lines (generated files excluded) a change gets **approved
|
|
64
|
+
rather than read**, however green everything else is.
|
|
65
|
+
|
|
66
|
+
- `large` / `huge` → **say so in the review's first line**, name the heaviest files
|
|
67
|
+
and recommend slicing; an honest "this exceeds what I can review carefully" beats
|
|
68
|
+
a confident-looking pass.
|
|
69
|
+
- A large diff is sometimes right (a mechanical rename, a vendored update); the
|
|
70
|
+
reason must be **stated**. Advisory only — it never blocks on size alone.
|
|
82
71
|
|
|
83
72
|
## Rubric — review along four dimensions, in priority order
|
|
84
73
|
|
|
@@ -106,6 +95,35 @@ snapshots, binaries) are excluded, because their size is not review effort.
|
|
|
106
95
|
and name the positive assertion that is missing (the call that must happen, the
|
|
107
96
|
value that must come back). Judge the test, not your guess about the implementation.
|
|
108
97
|
|
|
98
|
+
## Contracts beyond the diff — read what the change calls, and what calls it
|
|
99
|
+
|
|
100
|
+
The 22.09 production outage had green tests: an internal function gained a
|
|
101
|
+
parameter and a background job still called the wrapper without it. No diff line
|
|
102
|
+
was wrong on its own. So read outside the diff:
|
|
103
|
+
|
|
104
|
+
- **A call the diff adds or changes** — open the callee's real declaration and check
|
|
105
|
+
count, order, types and defaults. A mock that accepts anything (`**kwargs`,
|
|
106
|
+
`...args`, `vi.fn()`) hides exactly this.
|
|
107
|
+
- **A signature the diff changes** — grep every caller (wrappers, jobs, CLI entry
|
|
108
|
+
points, other packages); name the ones you checked.
|
|
109
|
+
- **A new enum, union or status value, or a widened validator** — check every
|
|
110
|
+
consumer that branches on it handles it rather than falling to a default.
|
|
111
|
+
- **Deleted code** — say what contract it held and where that contract lives now.
|
|
112
|
+
- **Commit and PR claims** — after tracing, try to prove each claim wrong ("no
|
|
113
|
+
behaviour change"); one the code does not bear out is a Finding.
|
|
114
|
+
|
|
115
|
+
A mismatch is a **Finding** citing the caller's `file:line` and the callee's
|
|
116
|
+
declaration. When consistent, list the call sites you read in one line.
|
|
117
|
+
|
|
118
|
+
**Gate integrity** — `node scripts/lib/gate-integrity.mjs` (plugin root as for
|
|
119
|
+
`review-range.mjs`) lists diff lines that pass a check without improving the code
|
|
120
|
+
(skipped/removed/loosened test, suppression, test-only branch, CI allowed to fail).
|
|
121
|
+
Each is a **question**, answered by ID under `## Gate integrity` in the PR or hand-off;
|
|
122
|
+
unanswered at gate:code, a Finding at the printed severity (P1 for a test switched
|
|
123
|
+
off, removed or loosened; else P2). Exit 2 = `gate integrity: not checked`.
|
|
124
|
+
*(Adapted from BMAD-METHOD's edge-case and deletion checks and gstack's enum
|
|
125
|
+
completeness check, both MIT.)*
|
|
126
|
+
|
|
109
127
|
## Calibration — apply `shared/agent-fragments/argument-quality.md`
|
|
110
128
|
|
|
111
129
|
Every finding carries **severity + concrete evidence (file:line or a metric)**.
|
|
@@ -134,10 +152,8 @@ grep -rn "<old-name-or-value>" README* docs/ commands/ skills/ 2>/dev/null | hea
|
|
|
134
152
|
two citations: the doc's file:line and the diff line that changed the
|
|
135
153
|
behaviour. It ships two sources that disagree, and the reader cannot tell which
|
|
136
154
|
one is current.
|
|
137
|
-
- No document describes
|
|
138
|
-
|
|
139
|
-
- If you did not search (the diff is too large, the old name is too common to
|
|
140
|
-
grep), write `docs not checked` and why. Silence reads as a clean search.
|
|
155
|
+
- No document describes it → no finding; undocumented new public surface is an Observation.
|
|
156
|
+
- Not searched (too large, too common)? Write `docs not checked` and why.
|
|
141
157
|
|
|
142
158
|
## Fresh-context, cross-model pass (architect-loop R3)
|
|
143
159
|
|
|
@@ -176,7 +192,7 @@ sets the verdict, and the human at the gate sees both.
|
|
|
176
192
|
|
|
177
193
|
1. For each P0/P1 Finding, file a Beads bug:
|
|
178
194
|
`bd create "<finding> (<file:line>)" --type bug --priority <0-2>`
|
|
179
|
-
2. Write the review to `docs/reviews/REVIEW
|
|
195
|
+
2. Write the review to `docs/reviews/REVIEW-{slug}.md` (the feature slug) — `**Date:**`,
|
|
180
196
|
Findings table (severity · file:line · evidence · fix), Observations, what the
|
|
181
197
|
change does well, and a **`## Change map`**: a Mermaid `flowchart LR` of what
|
|
182
198
|
the diff touches, drawn from the diff itself — one node per changed file or
|
|
@@ -199,14 +215,9 @@ your verdict.
|
|
|
199
215
|
|
|
200
216
|
## Open questions carry options and a pick
|
|
201
217
|
|
|
202
|
-
Canonical rule: `shared/agent-fragments/handoff-format.md
|
|
203
|
-
options and
|
|
204
|
-
|
|
205
|
-
A review that names a problem and stops has moved the work to the CTO's desk
|
|
206
|
-
unchanged. Each finding that needs a decision gets two or three real options with
|
|
207
|
-
what each costs, the one you would take and why, and what would make your pick
|
|
208
|
-
wrong. If you cannot choose, say what evidence would decide it — "it depends"
|
|
209
|
-
alone is not a finding.
|
|
218
|
+
Canonical rule: `shared/agent-fragments/handoff-format.md`. A finding that needs a
|
|
219
|
+
decision gets two or three options with their cost, your pick and what would make
|
|
220
|
+
it wrong; if you cannot choose, the evidence that would decide it.
|
|
210
221
|
|
|
211
222
|
## Evidence discipline — check it before you claim you are done
|
|
212
223
|
|
|
@@ -244,8 +255,5 @@ not report done over it.
|
|
|
244
255
|
`node "${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,"/"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}/scripts/lib/quote-verify.mjs" --file <file> --quote "<passage>"`;
|
|
245
256
|
a quote that does not verify is removed, or rewritten as a paraphrase marked `(paraphrase)`.
|
|
246
257
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
that test. "The secret is not set" written because it looks true is
|
|
250
|
-
indistinguishable, as prose, from the same sentence written after running
|
|
251
|
-
`grep`. The only thing that separates them is the output.
|
|
258
|
+
A reviewer judging your report sees only whether a finding reads plausibly; the
|
|
259
|
+
pasted output is what separates a checked claim from a confident guess.
|
|
@@ -245,11 +245,22 @@ component the implementer touches), `status: draft`, `author: design-advisor v2.
|
|
|
245
245
|
For React Native pull the Critical/High rules from `app-interface.csv`.
|
|
246
246
|
5. **Responsive contract** — breakpoints and what reflows. For RN: device classes,
|
|
247
247
|
safe-area, orientation.
|
|
248
|
-
6. **Motion contract** — transitions, durations, reduced-motion fallback.
|
|
248
|
+
6. **Motion contract** — transitions, durations, reduced-motion fallback. Every
|
|
249
|
+
motion names its duration and curve as numbers. Defaults, changed only with a reason:
|
|
250
|
+
feedback (hover, press, toggle) 120–180 ms; a panel, sheet or dialog 200–300 ms;
|
|
251
|
+
entering uses ease-out `cubic-bezier(0.23, 1, 0.32, 1)`, moving on screen uses
|
|
252
|
+
ease-in-out `cubic-bezier(0.77, 0, 0.175, 1)`, a drawer `cubic-bezier(0.32, 0.72, 0, 1)`;
|
|
253
|
+
nothing user-facing over 400 ms. "Smooth" or "subtle" without a number is not a
|
|
254
|
+
contract.
|
|
249
255
|
7. **6.5 Platform integration contract** — for mobile/extension: native APIs,
|
|
250
256
|
permissions, deep links, RN-specific component substitutions.
|
|
251
257
|
8. **Brand tokens** — the actual token table (CSS custom properties for web; a token
|
|
252
|
-
module for RN).
|
|
258
|
+
module for RN). **One value per token.** A component that departs from a global
|
|
259
|
+
token (a card at 8 px radius under a 0 px corner rule) gets its own row that says
|
|
260
|
+
`override` and why; `artifact-lint` rejects a token named twice with two values and
|
|
261
|
+
no `override`. Every style word the design uses ("editorial", "soft shadow",
|
|
262
|
+
"comfortable") resolves here to numbers — spacing scale, radius, shadow, row height —
|
|
263
|
+
because the implementer builds the numbers, not the word.
|
|
253
264
|
9. **Out of scope** — what this design deliberately does not cover.
|
|
254
265
|
10. **Open questions** — capped at 10; each with your recommended default so the
|
|
255
266
|
pipeline never blocks on you.
|
package/board/agents/devops.md
CHANGED
|
@@ -436,6 +436,37 @@ step for that condition before proceeding to Step 1 (gate:ship check).
|
|
|
436
436
|
```
|
|
437
437
|
If any required check fails → **stop**. Do not deploy. Tell CTO which artifact is missing.
|
|
438
438
|
|
|
439
|
+
**2b. The approvals are of THIS tree.** A `PASS` in the newest QA file says what QA
|
|
440
|
+
saw, not what you are about to ship: a commit after gate:ship, or a fix after the
|
|
441
|
+
review, ships unreviewed bytes under an old approval. Check each approving stage
|
|
442
|
+
against the working tree you deploy (the build uploads the directory, not HEAD):
|
|
443
|
+
```bash
|
|
444
|
+
RC="${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,"/"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}/scripts/lib/receipt.mjs"
|
|
445
|
+
[ -f "$RC" ] || RC="$(pwd)/scripts/lib/receipt.mjs"
|
|
446
|
+
STAGES=""
|
|
447
|
+
[ "$PROJECT_SIZE" != "nano" ] && STAGES="qa-engineer"
|
|
448
|
+
[ "$TIER_EFFECTIVE" != "baseline" ] && STAGES="$STAGES security-officer"
|
|
449
|
+
# The verdict logs live in the main checkout's .great_cto, also from a worktree.
|
|
450
|
+
VSTATE="$(node "$RC" --state-dir)"
|
|
451
|
+
[ -s "$VSTATE/verdicts/code-reviewer.log" ] && STAGES="$STAGES code-reviewer"
|
|
452
|
+
for S in $STAGES; do
|
|
453
|
+
node "$RC" --verify --agent "$S" --require; RC_EXIT=$?
|
|
454
|
+
case $RC_EXIT in
|
|
455
|
+
0) ;;
|
|
456
|
+
1) echo "BLOCKED: files changed or added after $S approved — re-run $S on this tree, or the operator accepts this exact state in a terminal: node $RC --accept --agent $S --require"; exit 1 ;;
|
|
457
|
+
3) echo "FRESHNESS NOT VERIFIED: no $S approval carries a receipt — say so in the deploy report; do not write 'verified'" ;;
|
|
458
|
+
*) echo "BLOCKED: could not compare $S's receipt with this tree (exit $RC_EXIT) — an unread comparison is not a match"; exit 1 ;;
|
|
459
|
+
esac
|
|
460
|
+
done
|
|
461
|
+
```
|
|
462
|
+
An acceptance is the operator's, from a controlling terminal; never run `--accept`
|
|
463
|
+
yourself. **What this step does not prove:** verdict logs and the acceptance file
|
|
464
|
+
are plain files under `.great_cto/`. The check catches an approval that went stale
|
|
465
|
+
in the normal flow; it cannot tell a real PASS from a line someone wrote. Never
|
|
466
|
+
write either yourself, and say so in the deploy report if you find one you did
|
|
467
|
+
not expect. *(Freshness on the deployed tree adapted from gstack's land-and-deploy
|
|
468
|
+
readiness gate, MIT.)*
|
|
469
|
+
|
|
439
470
|
3. **Read** `.great_cto/PROJECT.md` → get `archetype`, `type`, and deploy params:
|
|
440
471
|
```bash
|
|
441
472
|
ARCHETYPE=$(grep "^archetype:" .great_cto/PROJECT.md 2>/dev/null | awk '{print $2}'); ARCHETYPE=${ARCHETYPE:-web-service}
|
|
@@ -641,6 +641,8 @@ Rules:
|
|
|
641
641
|
a number. If none is configured, say so in the report (`mutation: not
|
|
642
642
|
configured`) and move on — an unmeasured signal reported honestly beats a <!-- slop-ok: "reported honestly" — honestly carries the meaning -->
|
|
643
643
|
fabricated one, and adding a heavyweight dev dependency is not QA's call.
|
|
644
|
+
"Not configured" ends the mutation run, not the question — do the
|
|
645
|
+
**verification-gap pass** below instead.
|
|
644
646
|
- **Report surviving mutants, not the score alone.** "73%" tells the developer
|
|
645
647
|
nothing; "the null-branch in `applyDiscount` survives — no test asserts what
|
|
646
648
|
happens at quantity 0" tells them exactly what to write.
|
|
@@ -649,6 +651,23 @@ Rules:
|
|
|
649
651
|
- **Timebox it.** If the run exceeds the step budget, report the partial result
|
|
650
652
|
with what was covered; do not let it eat the QA window.
|
|
651
653
|
|
|
654
|
+
**Verification-gap pass** — when no mutation framework is configured, and for small
|
|
655
|
+
tasks. Read the diff against the tests and name, with `file:line`, each of four gaps:
|
|
656
|
+
|
|
657
|
+
1. **A regression no test would catch** — a changed branch, boundary or error path
|
|
658
|
+
that no assertion pins. Name the input that would break it unnoticed.
|
|
659
|
+
2. **A call site left on the old behaviour** — the change is made in one place, a
|
|
660
|
+
sibling path still does the old thing, and no test reaches the sibling.
|
|
661
|
+
3. **A test that is skipped, flaky or too weak** to fail if the change were reverted.
|
|
662
|
+
4. **A test now wrong or pointless** — it pins behaviour the change removed, or passes
|
|
663
|
+
either way.
|
|
664
|
+
|
|
665
|
+
These do **not** count as verification: a snapshot alone, a check that a mock was
|
|
666
|
+
called, a check of the source text, an end-to-end test whose path passes through the
|
|
667
|
+
change without asserting its result. Each gap is a finding (P2; P1 when it sits on a
|
|
668
|
+
`CRITICAL` error path). None found → write `verification gaps: none found` and the
|
|
669
|
+
tests you read. *(Adapted from BMAD-METHOD's verification-gap review, MIT.)*
|
|
670
|
+
|
|
652
671
|
### Step 3c: Requirements Traceability
|
|
653
672
|
|
|
654
673
|
Read the Requirements Checklist from `docs/architecture/ARCH-<feature>.md` (bottom section).
|
|
@@ -686,6 +705,20 @@ After wiring, audit the chain: `/trace feature-$FEATURE_SLUG` — any remaining
|
|
|
686
705
|
never reaches a test, a UC with no task) is either a missing test to add or a hole to flag in
|
|
687
706
|
the QA report. **If bd unavailable**: skip silently — the QA report markdown already lists REQ → Evidence inline.
|
|
688
707
|
|
|
708
|
+
### Step 3c-2: Error paths — the architect's `Test?` column is a claim
|
|
709
|
+
|
|
710
|
+
Read `## Error paths` in `docs/architecture/ARCH-<feature>.md`. For each row:
|
|
711
|
+
|
|
712
|
+
- `Test?` names a test → open it. It must make that failure happen and assert what the
|
|
713
|
+
row says the user sees. A named test that does not exercise the failure → P2.
|
|
714
|
+
- `Test?` is `no` → a test gap: P2, or **P1 on a `CRITICAL` row**.
|
|
715
|
+
- A `CRITICAL` row still unhandled in the code → a blocking defect (P1); QA does not
|
|
716
|
+
PASS over it.
|
|
717
|
+
- A failing read handled as an empty result where the row says it raises → P1.
|
|
718
|
+
|
|
719
|
+
No table in a pre-2026-11-01 ARCH → `error paths: not in ARCH`. Missing in a newer
|
|
720
|
+
ARCH → `artifact-lint` should have stopped it; report it.
|
|
721
|
+
|
|
689
722
|
### Step 3d: Proof Loop — verify QA plan was fully executed
|
|
690
723
|
|
|
691
724
|
Before writing the report, confirm each item from the QA Plan (Step 2) was actually run:
|
|
@@ -480,14 +480,17 @@ Continues from the original Workflow below — produces `CSO-{slug}-{date}.md`,
|
|
|
480
480
|
**4b. Secrets in git history** (secrets deleted from code but still in git):
|
|
481
481
|
```bash
|
|
482
482
|
# .env files ever committed
|
|
483
|
-
git log --all --full-history -- "*.env" "**/.env"
|
|
484
|
-
#
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
grep -oE '[A-Za-z0-9+/]{40,}' | sort -u | head -20
|
|
483
|
+
git log --all --full-history --oneline -- "*.env" "**/.env" | head -10
|
|
484
|
+
# Every added line of the history, against the same patterns as the write hook.
|
|
485
|
+
# Reports commit · file · kind — never the value, so the scan does not leak what it finds.
|
|
486
|
+
HS="${CLAUDE_PLUGIN_ROOT:-$(ls -d ~/.claude/plugins/cache/*/great_cto/*/ 2>/dev/null | awk -F'/plugins/cache/' '{split($NF,p,"/"); print p[3], $0}' | sort -V | tail -1 | cut -d' ' -f2- | sed 's|/$||')}/scripts/lib/history-secret-scan.mjs"
|
|
487
|
+
[ -f "$HS" ] || HS="$(pwd)/scripts/lib/history-secret-scan.mjs"
|
|
488
|
+
node "$HS" --cwd .
|
|
490
489
|
```
|
|
490
|
+
Read the exit code: `0` clean, `1` findings, `2` **NOT SCANNED** ("history not
|
|
491
|
+
scanned: <reason>"), `3` **PARTIAL** — a capped walk or a shallow clone; report
|
|
492
|
+
"history scanned in part: <what it printed>". Neither 2 nor 3 is clean. With `gitleaks` installed, `gitleaks git --redact`
|
|
493
|
+
adds generic high-entropy detection. Never print a matched value yourself.
|
|
491
494
|
If any history findings: flag as P0 — secret must be rotated even if removed from code.
|
|
492
495
|
|
|
493
496
|
**4b′. CI workflows:** run the checker, don't read the YAML by eye —
|
|
@@ -49,12 +49,17 @@ that get pushed back on, because those are the ones nobody else will catch.
|
|
|
49
49
|
- committing secrets, `.env`, or machine-local config
|
|
50
50
|
- a sleep to make a test pass — the sleep is a symptom; find the condition
|
|
51
51
|
- `.skip` on a failing test, which is deletion with a comment
|
|
52
|
+
- production code that branches on running under test (`NODE_ENV === 'test'`, `CI`) —
|
|
53
|
+
it makes the test check a path production never takes
|
|
54
|
+
- loosening an assertion or raising a timeout or threshold until a test passes —
|
|
55
|
+
the test then no longer pins what it was written to pin
|
|
52
56
|
- speculative structure for a need nobody has yet
|
|
53
57
|
- widening the brief's file scope without the brief being reissued
|
|
54
58
|
|
|
55
59
|
**Ask before proceeding**, and do not start until answered:
|
|
56
60
|
|
|
57
61
|
- a lint or type rule you are being told to disable — what is it protecting?
|
|
62
|
+
(`gate-integrity.mjs` asks this of every suppression in your diff; answer each by ID under `## Gate integrity` in the hand-off)
|
|
58
63
|
- a pattern you are told to copy — is the repo moving toward it or away?
|
|
59
64
|
- an "unrelated" failing test — unrelated is a hypothesis, not an observation
|
|
60
65
|
- a field added to an API response — which consumers see it?
|
|
@@ -504,6 +509,12 @@ Schema: `skills/great_cto/references/knowledge-extraction.md`
|
|
|
504
509
|
of them asserts nothing, so add the test for the positive effect (the call made, the
|
|
505
510
|
value returned) until the stub fails on an assertion. Mutation testing is skipped for
|
|
506
511
|
small tasks; this check is not.
|
|
512
|
+
- **Say what green does not prove.** For each new test, one line in your reply:
|
|
513
|
+
`no_claim: <what passing does NOT show>` — which dependency is mocked and what it
|
|
514
|
+
stands in for, which path the test never reaches. A mock that accepts any arguments
|
|
515
|
+
standing in for a real function is how a signature mismatch shipped on 22.09: name
|
|
516
|
+
it, and when the real contract matters, add one test that calls the real function on
|
|
517
|
+
the same path. *(Adapted from gstack's test value card, MIT.)*
|
|
507
518
|
- **GREEN**: write minimal code to make test pass → re-run → confirm PASS
|
|
508
519
|
- **REFACTOR**: clean up → confirm still PASS, no coverage regression
|
|
509
520
|
- **Two fixes, one premise → stop.** When two fixes resting on the same assumption have
|
|
@@ -196,6 +196,12 @@ export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_F
|
|
|
196
196
|
}
|
|
197
197
|
const changedOutput = git(['diff', '--name-only', '--diff-filter=d', ref, '--', ':/', ...pathspecExclude], cwd);
|
|
198
198
|
if (changedOutput === null) return null;
|
|
199
|
+
// Deletions, kept apart: a file deleted after an approval changes what ships, and
|
|
200
|
+
// the file map — which holds content digests — cannot hold a file that is gone.
|
|
201
|
+
const deletedOutput = git(['diff', '--name-only', '--diff-filter=D', ref, '--', ':/', ...pathspecExclude], cwd);
|
|
202
|
+
if (deletedOutput === null) return null;
|
|
203
|
+
const allDeleted = deletedOutput.split('\n').map((x) => x.trim()).filter(Boolean).sort();
|
|
204
|
+
const deleted = allDeleted.slice(0, maxFiles);
|
|
199
205
|
const names = [
|
|
200
206
|
...changedOutput
|
|
201
207
|
.split('\n').map((s) => s.trim()).filter(Boolean),
|
|
@@ -213,7 +219,9 @@ export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_F
|
|
|
213
219
|
files[p] = blob;
|
|
214
220
|
}
|
|
215
221
|
|
|
216
|
-
|
|
222
|
+
// A cut deletion list is a partial receipt like a cut file map: say so.
|
|
223
|
+
if (allDeleted.length > maxFiles) truncated = true;
|
|
224
|
+
return { head, dirty, base: ref, base_from: baseFrom, files, deleted, ...(truncated ? { truncated: true } : {}) };
|
|
217
225
|
}
|
|
218
226
|
|
|
219
227
|
/**
|
|
@@ -289,6 +297,33 @@ export function mergeBase(cwd) {
|
|
|
289
297
|
* whose receipt matched, and collapsing them is the defect this whole ladder
|
|
290
298
|
* exists to remove.
|
|
291
299
|
*/
|
|
300
|
+
// Inside .great_cto/, only what DECIDES behaviour is compared: PROJECT.md (project_size
|
|
301
|
+
// and the tier, which decide the stages a deploy checks), exceptions/ (signed
|
|
302
|
+
// overrides), configuration files (*.toml, *.yml — security-allowlist.yml is one),
|
|
303
|
+
// security-signals.log (it raises the tier from baseline), slo-budget-current.md
|
|
304
|
+
// (an exhausted budget blocks a deploy) and env.sh (sourced into the deploy shell).
|
|
305
|
+
// Deleting or editing any of them after an approval is drift.
|
|
306
|
+
// Everything else there is what the pipeline writes while it runs — verdicts, logs,
|
|
307
|
+
// lessons, the review marker, handoffs, an acceptance — and in a project that does
|
|
308
|
+
// not gitignore .great_cto/ (init does not) each of those, written AFTER an approval,
|
|
309
|
+
// made the approval drift from itself, so a deploy could never pass.
|
|
310
|
+
//
|
|
311
|
+
// A list of what counts, not of what to ignore. The first fix listed the writers and
|
|
312
|
+
// missed three (review-marker.json, .last-auto-learn, the learner's file); the writers grow
|
|
313
|
+
// with every hook, the configuration does not.
|
|
314
|
+
const COUNTED_STATE = /^(PROJECT\.md|exceptions(\/|$)|security-signals\.log$|slo-budget-current\.md$|env\.sh$|[^/]+\.(toml|ya?ml)$)/;
|
|
315
|
+
export function isPipelineOutput(p) {
|
|
316
|
+
if (!p.startsWith('.great_cto/')) return false;
|
|
317
|
+
return !COUNTED_STATE.test(p.slice('.great_cto/'.length));
|
|
318
|
+
}
|
|
319
|
+
/** The top-level entries of .great_cto/ that are pipeline output, as paths to exclude. */
|
|
320
|
+
export function pipelineOutputPaths(cwd) {
|
|
321
|
+
let names;
|
|
322
|
+
try { names = requireFs().readdirSync(join(cwd, '.great_cto')); } catch { return []; }
|
|
323
|
+
return names.filter((n) => !n.includes(':') && isPipelineOutput(`.great_cto/${n}`)).map((n) => `.great_cto/${n}`);
|
|
324
|
+
}
|
|
325
|
+
const codeOnly = (files) => Object.fromEntries(Object.entries(files || {}).filter(([p]) => !isPipelineOutput(p)));
|
|
326
|
+
|
|
292
327
|
export function compareReceipts(recorded, current, { digest = null, cwd = process.cwd() } = {}) {
|
|
293
328
|
if (!recorded) return { state: 'no-receipt', why: 'the approving verdict carries no receipt — nothing to compare against' };
|
|
294
329
|
if (!current) return { state: 'unreadable', why: 'the current tree state could not be read' };
|
|
@@ -297,8 +332,8 @@ export function compareReceipts(recorded, current, { digest = null, cwd = proces
|
|
|
297
332
|
const added = [];
|
|
298
333
|
const removed = [];
|
|
299
334
|
const landed = [];
|
|
300
|
-
const before = recorded.files
|
|
301
|
-
const now = current.files
|
|
335
|
+
const before = codeOnly(recorded.files);
|
|
336
|
+
const now = codeOnly(current.files);
|
|
302
337
|
// Falling out of the CHANGE SET is not the same as being deleted.
|
|
303
338
|
//
|
|
304
339
|
// The file map is the diff against the merge-base, so the moment a reviewed
|
|
@@ -321,6 +356,12 @@ export function compareReceipts(recorded, current, { digest = null, cwd = proces
|
|
|
321
356
|
else changed.push(p); // still here, and edited
|
|
322
357
|
}
|
|
323
358
|
for (const p of Object.keys(now)) if (!(p in before)) added.push(p);
|
|
359
|
+
// Deleted since the approval. Only when the recorded receipt says what was deleted
|
|
360
|
+
// then; one from before this field cannot tell a new deletion from the change's own.
|
|
361
|
+
if (Array.isArray(recorded.deleted)) {
|
|
362
|
+
const was = new Set(recorded.deleted);
|
|
363
|
+
for (const p of current.deleted || []) if (!was.has(p) && !isPipelineOutput(p) && !removed.includes(p)) removed.push(p);
|
|
364
|
+
}
|
|
324
365
|
|
|
325
366
|
// A file the review covered, edited since. This is the finding; the rest is
|
|
326
367
|
// context. `added` in particular is usually ordinary work continuing, not a
|
|
@@ -426,11 +467,11 @@ export function receiptHash(receipt) {
|
|
|
426
467
|
* Single-use by design: an acceptance authorises ONE push. An acceptance that
|
|
427
468
|
* outlives its push is a standing permission, and nobody asked for one.
|
|
428
469
|
*/
|
|
429
|
-
export function writeAcceptance(cwd, { hash, why = '', at = Date.now() } = {}) {
|
|
470
|
+
export function writeAcceptance(cwd, { hash, why = '', at = Date.now(), agent = null } = {}) {
|
|
430
471
|
const { writeFileSync, mkdirSync } = requireFs();
|
|
431
472
|
mkdirSync(join(cwd, '.great_cto'), { recursive: true });
|
|
432
|
-
writeFileSync(join(cwd, ACCEPT_PATH), JSON.stringify({ hash, why, at }) + '\n');
|
|
433
|
-
return { hash, why, at };
|
|
473
|
+
writeFileSync(join(cwd, ACCEPT_PATH), JSON.stringify({ hash, why, at, ...(agent ? { agent } : {}) }) + '\n');
|
|
474
|
+
return { hash, why, at, agent };
|
|
434
475
|
}
|
|
435
476
|
|
|
436
477
|
/**
|
|
@@ -440,7 +481,7 @@ export function writeAcceptance(cwd, { hash, why = '', at = Date.now() } = {}) {
|
|
|
440
481
|
* state" and "valid" are different situations, and the third is the one worth
|
|
441
482
|
* naming out loud: it means the tree moved after a human looked at it.
|
|
442
483
|
*/
|
|
443
|
-
export function readAcceptance(cwd, currentHash) {
|
|
484
|
+
export function readAcceptance(cwd, currentHash, { agent = null } = {}) {
|
|
444
485
|
const { readFileSync } = requireFs();
|
|
445
486
|
let raw;
|
|
446
487
|
try { raw = readFileSync(join(cwd, ACCEPT_PATH), 'utf8'); }
|
|
@@ -455,6 +496,12 @@ export function readAcceptance(cwd, currentHash) {
|
|
|
455
496
|
return { valid: false, stale: true, rec,
|
|
456
497
|
why: 'the acceptance names a different state — the tree changed after it was accepted' };
|
|
457
498
|
}
|
|
499
|
+
// An acceptance given while looking at one stage's drift does not cover another's:
|
|
500
|
+
// the operator saw code-reviewer's files, not qa-engineer's.
|
|
501
|
+
if (agent && rec.agent !== agent) {
|
|
502
|
+
return { valid: false, stale: true, rec,
|
|
503
|
+
why: `the acceptance was given for ${rec.agent || 'the whole tree'}, not for ${agent} — accept ${agent}'s drift separately` };
|
|
504
|
+
}
|
|
458
505
|
return { valid: true, rec, why: `accepted at ${new Date(rec.at).toISOString()}` };
|
|
459
506
|
}
|
|
460
507
|
|
|
@@ -475,6 +522,12 @@ function requireFs() {
|
|
|
475
522
|
// `--emit` prints a receipt for the current tree (used by log-verdict.sh).
|
|
476
523
|
// `--check <file>` compares a recorded receipt held in a file.
|
|
477
524
|
// `--verify` compares the newest approving verdict's receipt against now.
|
|
525
|
+
// `--agent <name>` narrows it to one approving stage. The newest approval of
|
|
526
|
+
// any stage is the wrong question before a deploy: a review of today's tree
|
|
527
|
+
// must not stand in for a QA pass on yesterday's.
|
|
528
|
+
// `--require` is the deploy question: "no approval carries a receipt" exits 3,
|
|
529
|
+
// not 0, and files ADDED since the approval count as drift. Before a push they
|
|
530
|
+
// are work continuing; before a deploy they are code nobody reviewed, shipping.
|
|
478
531
|
|
|
479
532
|
function currentForReceipt(cwd, receipt) {
|
|
480
533
|
// Persisted evidence is data, not Git options or a new revision expression.
|
|
@@ -487,7 +540,10 @@ function currentForReceipt(cwd, receipt) {
|
|
|
487
540
|
base = receipt.head;
|
|
488
541
|
}
|
|
489
542
|
if (base != null && (typeof base !== 'string' || !/^[0-9a-f]{7,64}$/i.test(base))) return null;
|
|
490
|
-
|
|
543
|
+
// Without the pipeline's own state: an approval, an acceptance and a verdict all
|
|
544
|
+
// write under .great_cto/, and in a project that does not ignore it each write
|
|
545
|
+
// would change the state it is about (an acceptance voiding itself on write).
|
|
546
|
+
return treeReceipt(cwd, { base: base || null, exclude: pipelineOutputPaths(cwd) });
|
|
491
547
|
}
|
|
492
548
|
|
|
493
549
|
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
@@ -510,27 +566,48 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1
|
|
|
510
566
|
process.stdout.write(r ? JSON.stringify(r) : '');
|
|
511
567
|
process.exit(r ? 0 : 1);
|
|
512
568
|
}
|
|
569
|
+
const agentsArg = () => {
|
|
570
|
+
const i = argv.indexOf('--agent');
|
|
571
|
+
if (i < 0) return APPROVING_AGENTS;
|
|
572
|
+
const name = argv[i + 1];
|
|
573
|
+
if (!APPROVING_AGENTS.includes(name)) {
|
|
574
|
+
console.error(`receipt: --agent must be one of ${APPROVING_AGENTS.join(', ')}, got ${name}`);
|
|
575
|
+
process.exit(2);
|
|
576
|
+
}
|
|
577
|
+
return [name];
|
|
578
|
+
};
|
|
513
579
|
if (argv.includes('--verify')) {
|
|
514
580
|
const cwd = process.cwd();
|
|
515
|
-
const
|
|
581
|
+
const agents = agentsArg();
|
|
582
|
+
const approval = latestApproval(cwd, { agents });
|
|
516
583
|
const current = currentForReceipt(cwd, approval?.receipt);
|
|
517
584
|
const cmp = compareReceipts(approval?.receipt ?? null, current);
|
|
518
585
|
if (cmp.state === 'no-receipt') {
|
|
519
586
|
// Not silence. A push with no approval to compare against and a push whose
|
|
520
587
|
// receipt matched are different facts, and only one of them is evidence.
|
|
521
|
-
|
|
522
|
-
|
|
588
|
+
const whom = agents.length === 1 ? `${agents[0]} verdict` : 'verdict';
|
|
589
|
+
console.log(`receipt: no approving ${whom} carries a receipt — nothing was verified`);
|
|
590
|
+
process.exit(argv.includes('--require') ? 3 : 0);
|
|
523
591
|
}
|
|
524
592
|
const who = approval ? `${approval.agent} ${approval.verdict} at ${approval.ts}` : 'an approval';
|
|
525
593
|
console.log(`receipt: against ${who}`);
|
|
526
594
|
console.log(describeDrift(cmp).split('\n').map((l) => ` ${l}`).join('\n'));
|
|
527
595
|
|
|
528
596
|
if (cmp.state === 'unreadable') process.exit(2);
|
|
529
|
-
|
|
597
|
+
const requireMode = argv.includes('--require');
|
|
598
|
+
// A receipt capped at MAX_FILES lists only part of the change, so a file past
|
|
599
|
+
// the cap could change unseen. For the deploy question that is not a match.
|
|
600
|
+
if (requireMode && (approval?.receipt?.truncated || current?.truncated)) {
|
|
601
|
+
console.log(` the receipt lists only the first ${MAX_FILES} files of this change — a match cannot be shown; re-run the stage on a smaller change`);
|
|
602
|
+
process.exit(2);
|
|
603
|
+
}
|
|
604
|
+
const drifted = cmp.state === 'differs' || (cmp.state === 'extended' && requireMode);
|
|
605
|
+
if (!drifted) process.exit(0);
|
|
530
606
|
|
|
531
607
|
// Drift, but a human may already have looked at exactly this state.
|
|
532
608
|
const hash = receiptHash(current);
|
|
533
|
-
const
|
|
609
|
+
const narrowed = agents.length === 1 ? agents[0] : null;
|
|
610
|
+
const acc = readAcceptance(cwd, hash, { agent: narrowed });
|
|
534
611
|
if (acc.valid) {
|
|
535
612
|
console.log(` accepted by the operator for this exact state (${acc.why})`);
|
|
536
613
|
// Consumed here rather than by the caller: whoever asked the question is
|
|
@@ -540,7 +617,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1
|
|
|
540
617
|
process.exit(0);
|
|
541
618
|
}
|
|
542
619
|
if (acc.stale) console.log(` ${acc.why} — accept again if this state is fine`);
|
|
543
|
-
console.log(` to accept this state: node scripts/lib/receipt.mjs --accept`);
|
|
620
|
+
console.log(` to accept this state: node scripts/lib/receipt.mjs --accept${narrowed ? ` --agent ${narrowed}` : ''}${requireMode ? ' --require' : ''}`);
|
|
544
621
|
process.exit(1);
|
|
545
622
|
}
|
|
546
623
|
|
|
@@ -553,14 +630,21 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1
|
|
|
553
630
|
// behaviour — an agent asked to approve its own work will comply.
|
|
554
631
|
if (argv.includes('--accept')) {
|
|
555
632
|
const cwd = process.cwd();
|
|
556
|
-
const approval = latestApproval(cwd);
|
|
633
|
+
const approval = latestApproval(cwd, { agents: agentsArg() });
|
|
557
634
|
const current = currentForReceipt(cwd, approval?.receipt);
|
|
558
635
|
const cmp = compareReceipts(approval?.receipt ?? null, current);
|
|
559
636
|
if (cmp.state === 'unreadable') {
|
|
560
637
|
console.error(`receipt: cannot accept — ${cmp.why}`);
|
|
561
638
|
process.exit(2);
|
|
562
639
|
}
|
|
563
|
-
|
|
640
|
+
const requireMode = argv.includes('--require');
|
|
641
|
+
if (requireMode && (approval?.receipt?.truncated || current?.truncated)) {
|
|
642
|
+
console.error(`receipt: cannot accept — the receipt lists only the first ${MAX_FILES} files; what you would accept is not all on screen`);
|
|
643
|
+
process.exit(2);
|
|
644
|
+
}
|
|
645
|
+
// Under --require an added file is drift, so it must be acceptable too —
|
|
646
|
+
// otherwise verify says "accept it" and accept says "nothing to accept".
|
|
647
|
+
if (!(cmp.state === 'differs' || (cmp.state === 'extended' && requireMode))) {
|
|
564
648
|
console.log(`receipt: nothing to accept — ${cmp.why}`);
|
|
565
649
|
process.exit(0);
|
|
566
650
|
}
|
|
@@ -578,7 +662,9 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1
|
|
|
578
662
|
const answer = await new Promise((res) => rl.question('\nShip this state anyway? [y/N] ', (a) => { rl.close(); res(a); }));
|
|
579
663
|
if (!/^y(es)?$/i.test(answer.trim())) { console.log('not accepted — nothing recorded.'); process.exit(1); }
|
|
580
664
|
|
|
581
|
-
|
|
665
|
+
const stage = argv.includes('--agent') ? argv[argv.indexOf('--agent') + 1] : null;
|
|
666
|
+
writeAcceptance(cwd, { hash, agent: stage,
|
|
667
|
+
why: `drift accepted over ${cmp.changed.length} changed and ${cmp.added.length} added file(s)${stage ? ` for ${stage}` : ''}` });
|
|
582
668
|
console.log('accepted. This authorises ONE push of this exact state; any further edit voids it.');
|
|
583
669
|
process.exit(0);
|
|
584
670
|
}
|
package/package.json
CHANGED