great-cto 3.75.4 → 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.
@@ -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",
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: haiku
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 — like a senior engineer who will have to maintain this code.
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
- When you reviewed this branch before (a fix after your BLOCKED is the usual case),
44
- do not re-read the whole feature — that is how six of twenty-eight runs ended at the
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` → nothing changed since your verdict; say so and re-emit it.
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
- A change can pass every other check here — tests green, scope respected, security
69
- clean — and still be too large to actually review. Past ~400 hand-written lines,
70
- review quality collapses and the change gets **approved rather than read**; the
71
- team then owns code no one holds a model of. Generated files (lockfiles, dist,
72
- snapshots, binaries) are excluded, because their size is not review effort.
73
-
74
- - `large` / `huge` → **say so as the first line of the review**, name the heaviest
75
- files, and recommend slicing. Do not pretend to have reviewed 2,000 lines with
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 the behaviour → no finding. New public surface with no
138
- documentation at all is an Observation.
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-<feature-slug>.md` — `**Date:**`,
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` — "Every open question carries
203
- options and a pick".
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
- Why this and not a reviewer reading your report: a reviewer judges whether the
248
- finding reads plausibly, and a confident wrong finding is exactly what passes
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.
@@ -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" 2>/dev/null | head -10
484
- # Private keys in history
485
- git log --all -S "BEGIN RSA PRIVATE\|BEGIN EC PRIVATE\|PRIVATE KEY" --oneline 2>/dev/null | head -10
486
- # High-entropy strings in recent commits (last 50)
487
- git log --oneline -50 --all 2>/dev/null | awk '{print $1}' | \
488
- xargs -I{} git show {}:. 2>/dev/null | \
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
- return { head, dirty, base: ref, base_from: baseFrom, files, ...(truncated ? { truncated: true } : {}) };
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
- return treeReceipt(cwd, { base: base || null });
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 approval = latestApproval(cwd);
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
- console.log('receipt: no approving verdict carries a receipt — nothing was verified');
522
- process.exit(0);
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
- if (cmp.state !== 'differs') process.exit(0);
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 acc = readAcceptance(cwd, hash);
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
- if (cmp.state !== 'differs') {
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
- writeAcceptance(cwd, { hash, why: `drift accepted over ${cmp.changed.length} changed file(s)` });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.75.4",
3
+ "version": "3.75.5",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",