great-cto 3.75.4 → 3.75.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/assets/statusline.mjs +7 -4
  2. package/board/.claude-plugin/plugin.json +1 -1
  3. package/board/agents/architect.md +19 -1
  4. package/board/agents/code-reviewer.md +50 -42
  5. package/board/agents/design-advisor.md +13 -2
  6. package/board/agents/devops.md +40 -1
  7. package/board/agents/qa-engineer.md +33 -0
  8. package/board/agents/security-officer.md +15 -7
  9. package/board/agents/senior-dev.md +16 -0
  10. package/board/packages/board/lib/bd-read-worker.mjs +2 -1
  11. package/board/packages/board/lib/receipt-read-worker.mjs +2 -1
  12. package/board/scripts/hooks/auto-attach-reviewers.mjs +2 -2
  13. package/board/scripts/lib/affected-tests.mjs +2 -2
  14. package/board/scripts/lib/agent-events.mjs +2 -1
  15. package/board/scripts/lib/agent-usage.mjs +2 -1
  16. package/board/scripts/lib/approval-level.mjs +3 -1
  17. package/board/scripts/lib/board-watch.mjs +2 -2
  18. package/board/scripts/lib/change-tier.mjs +2 -1
  19. package/board/scripts/lib/cost-meter.mjs +2 -2
  20. package/board/scripts/lib/cross-model-review.mjs +2 -2
  21. package/board/scripts/lib/dag-metric.mjs +2 -1
  22. package/board/scripts/lib/gate-plan.mjs +2 -2
  23. package/board/scripts/lib/gate-tier.mjs +2 -1
  24. package/board/scripts/lib/is-main.mjs +24 -0
  25. package/board/scripts/lib/judge-model.mjs +2 -1
  26. package/board/scripts/lib/pipeline-health.mjs +2 -1
  27. package/board/scripts/lib/receipt.mjs +138 -19
  28. package/board/scripts/lib/second-opinion.mjs +2 -1
  29. package/board/scripts/lib/session-usage.mjs +2 -1
  30. package/board/scripts/lib/stack-capabilities.mjs +3 -1
  31. package/board/scripts/lib/stand-down.mjs +2 -1
  32. package/board/scripts/lib/system-map.mjs +2 -1
  33. package/board/scripts/lib/verdict-record.mjs +15 -3
  34. package/board/scripts/lib/verdict-seal.mjs +95 -0
  35. package/board/scripts/skill-lint.mjs +2 -1
  36. package/package.json +1 -1
  37. package/postinstall.mjs +7 -2
@@ -20,7 +20,6 @@ import { homedir } from 'node:os';
20
20
  import { basename, join } from 'node:path';
21
21
  import { spawnSync } from 'node:child_process';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { resolve } from 'node:path';
24
23
 
25
24
  const DIR = process.env.GREAT_CTO_HOME || join(homedir(), '.great_cto');
26
25
  const LOG = join(DIR, 'claude-limits.jsonl');
@@ -96,6 +95,10 @@ function main() {
96
95
  }
97
96
 
98
97
  // Real paths on both sides: the module URL is resolved through symlinks (macOS
99
- // /var → /private/var, a symlinked home), the command line is not.
100
- const real = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
101
- if (process.argv[1] && real(fileURLToPath(import.meta.url)) === real(process.argv[1])) main();
98
+ // /var → /private/var, a symlinked home), the command line is not. Same rule as
99
+ // scripts/lib/is-main.mjs, inlined because this file is copied alone into
100
+ // ~/.claude; a path that does not resolve is not this file.
101
+ const isMain = () => {
102
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; }
103
+ };
104
+ if (process.argv[1] && isMain()) main();
@@ -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.6",
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.
@@ -208,11 +208,18 @@ lapses, and an approval that does not name its target approves everything.
208
208
  **Outcome — after the step.** What happened, against which exact target, and the
209
209
  admission it acted under, cited by id. An outcome with no admission to cite is
210
210
  an **unapproved** change: report it as one, in those words, and do not let the
211
- deploy record read as though a human had seen it.
211
+ deploy record read as though a human had seen it. The record is the evidence:
212
+ "shipped to prod" with no approval cited is unapproved **as written** — say so now,
213
+ not "it would be unapproved if a lookup comes back empty". An approval found later
214
+ corrects the record; it does not make your sentence wrong.
212
215
 
213
216
  **Coverage — in every post-deploy verification.** What was checked, what was
214
217
  skipped, and where the check stopped. A verification that cannot say what it
215
218
  skipped is `partial`, never `ok` — and `partial` is reported, not rounded up.
219
+ After the step the words are `ok`, `partial` or `failed`: the deploy has happened,
220
+ so `BLOCKED` (a word for before the step) does not describe it. A skipped
221
+ migration check after a green smoke is `partial`, naming the skipped check and the
222
+ command that would run it.
216
223
 
217
224
  ## Upgrading a container deployment without losing the data
218
225
 
@@ -436,6 +443,38 @@ step for that condition before proceeding to Step 1 (gate:ship check).
436
443
  ```
437
444
  If any required check fails → **stop**. Do not deploy. Tell CTO which artifact is missing.
438
445
 
446
+ **2b. The approvals are of THIS tree.** A `PASS` in the newest QA file says what QA
447
+ saw, not what you are about to ship: a commit after gate:ship, or a fix after the
448
+ review, ships unreviewed bytes under an old approval. Check each approving stage
449
+ against the working tree you deploy (the build uploads the directory, not HEAD):
450
+ ```bash
451
+ 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"
452
+ [ -f "$RC" ] || RC="$(pwd)/scripts/lib/receipt.mjs"
453
+ STAGES=""
454
+ [ "$PROJECT_SIZE" != "nano" ] && STAGES="qa-engineer"
455
+ [ "$TIER_EFFECTIVE" != "baseline" ] && STAGES="$STAGES security-officer"
456
+ # The verdict logs live in the main checkout's .great_cto, also from a worktree.
457
+ VSTATE="$(node "$RC" --state-dir)"
458
+ [ -s "$VSTATE/verdicts/code-reviewer.log" ] && STAGES="$STAGES code-reviewer"
459
+ for S in $STAGES; do
460
+ node "$RC" --verify --agent "$S" --require; RC_EXIT=$?
461
+ case $RC_EXIT in
462
+ 0) ;;
463
+ 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 ;;
464
+ 3) echo "FRESHNESS NOT VERIFIED: the $S approval carries no receipt or no seal — say so in the deploy report; do not write 'verified'" ;;
465
+ 4) echo "BLOCKED: the newest $S approval's seal does not match — it was written or edited outside log-verdict.sh. Re-run $S; report it"; exit 1 ;;
466
+ *) echo "BLOCKED: could not compare $S's receipt with this tree (exit $RC_EXIT) — an unread comparison is not a match"; exit 1 ;;
467
+ esac
468
+ done
469
+ ```
470
+ An acceptance is the operator's, from a controlling terminal; never run `--accept`
471
+ yourself. **What this step does not prove:** verdict lines and acceptances are
472
+ sealed by their writers (`log-verdict.sh`, `--accept`), so a line appended or
473
+ edited by hand fails (exit 4) — but code that runs those writers can still make
474
+ a valid one (ADR-046). Never write either yourself, and report one you did not
475
+ expect. *(Freshness on the deployed tree adapted from gstack's land-and-deploy
476
+ readiness gate, MIT.)*
477
+
439
478
  3. **Read** `.great_cto/PROJECT.md` → get `archetype`, `type`, and deploy params:
440
479
  ```bash
441
480
  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 —
@@ -582,6 +585,11 @@ done
582
585
 
583
586
  5b2. **Skeptical triage of P0/P1 findings** (before writing report)
584
587
 
588
+ **Every finding states its gate effect in the same breath as its grade:** an
589
+ unmitigated P0 or P1 with no signed exception → `gate:ship: BLOCKED until fixed or
590
+ excepted`; P2/P3 → `does not block`. A correct grade with the decision left
591
+ implicit is half a finding — the reader has to know the policy to know the outcome.
592
+
585
593
  Apply the **skeptical-triage skill** (`skills/skeptical-triage/SKILL.md`) to every P0/P1 audit finding before marking it in the CSO report. Reduces false-positive `gate:ship` blocks. P2/P3 findings skip triage.
586
594
 
587
595
  Run the 4-step pattern from the skill: Reachability → Verify Defenses → Missed Angles → Arbiter. Apply hard rules (absence of defense → VALID; code quality ≠ security; name the line or it does not exist).
@@ -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,8 +509,19 @@ 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
520
+ - **A red test is a defect until shown otherwise.** "Stale file", "cache", "flaky"
521
+ are hypotheses: clean (remove the build output, clear the cache), rerun the
522
+ same command, and paste both runs. Green only after the clean rerun counts;
523
+ until then the task stays `in_progress` and nothing is committed or closed on
524
+ the explanation.
509
525
  - **Two fixes, one premise → stop.** When two fixes resting on the same assumption have
510
526
  failed the same check, write that assumption in one sentence and make no third edit on
511
527
  it. Test the assumption itself first (print the value, query the real dependency, run
@@ -2,13 +2,14 @@
2
2
  // Forked into a newly owned group; ordinary module imports do not start work.
3
3
  import { fork, spawn } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { isMainModule } from '../../../scripts/lib/is-main.mjs';
5
6
  const entry = fileURLToPath(import.meta.url);
6
7
  export function spawnBdRead(binary, { readDeadlineMs = 20250, commandArgs, ...options }) {
7
8
  const args = [binary, String(readDeadlineMs)];
8
9
  if (commandArgs !== undefined) args.push(JSON.stringify(commandArgs));
9
10
  return fork(entry, args, { ...options, silent: true });
10
11
  }
11
- if (process.argv[1] === entry) {
12
+ if (isMainModule(import.meta.url)) {
12
13
  const grouped = process.platform !== 'win32';
13
14
  let child;
14
15
  function killOwnedGroup() {
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { treeReceipt, latestApproval, compareReceipts, mergeBase } from '../../../scripts/lib/receipt.mjs';
6
6
  import { secondOpinionForTree } from '../../../scripts/lib/second-opinion-for-tree.mjs';
7
7
  import { affectedTests } from '../../../scripts/lib/affected-tests.mjs';
8
+ import { isMainModule } from '../../../scripts/lib/is-main.mjs';
8
9
  const entry = fileURLToPath(import.meta.url);
9
10
  export const receiptReadEntry = entry;
10
11
 
@@ -20,7 +21,7 @@ function bindingTree() {
20
21
  if (!receipt && gitMetadata) throw Error('Repository receipt unavailable');
21
22
  return receipt;
22
23
  }
23
- if (process.argv[1] === entry) {
24
+ if (isMainModule(import.meta.url)) {
24
25
  try {
25
26
  const mode = process.argv[2] || 'binding';
26
27
  if (!['binding', 'approval', 'second-opinion', 'touches'].includes(mode) || process.argv.length > 3) throw Error('Invalid evidence operation');
@@ -24,6 +24,7 @@ import { execFileSync } from "node:child_process";
24
24
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
25
25
  import { join } from "node:path";
26
26
  import { adaptiveSpecialistPlan, planNotice } from '../lib/specialist-plan.mjs';
27
+ import { isMainModule } from "../lib/is-main.mjs";
27
28
 
28
29
  const cwd = process.cwd();
29
30
  const greatCtoDir = join(cwd, ".great_cto");
@@ -444,6 +445,5 @@ function main() {
444
445
  }
445
446
 
446
447
  // Guarded so reviewer-nudge.mjs can import RULES/shouldExclude without running the scan.
447
- import { fileURLToPath } from "node:url";
448
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
448
+ const isMain = isMainModule(import.meta.url);
449
449
  if (isMain) main();
@@ -24,7 +24,7 @@
24
24
  import { spawnSync } from 'node:child_process';
25
25
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
26
26
  import { join, dirname, basename, extname, relative, sep } from 'node:path';
27
- import { fileURLToPath } from 'node:url';
27
+ import { isMainModule } from './is-main.mjs';
28
28
 
29
29
  // Dependency, build and tool-state directories: they hold no test of the project's
30
30
  // own code, and walking them (node_modules alone can be 100k files) is the slow part.
@@ -115,7 +115,7 @@ export function affectedTests({ cwd = process.cwd(), changed } = {}) {
115
115
  return { mode: commands.length ? 'targeted' : 'full', commands, tests, unmapped };
116
116
  }
117
117
 
118
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
118
+ const isMain = isMainModule(import.meta.url);
119
119
  if (isMain) {
120
120
  const a = process.argv.slice(2);
121
121
  const i = a.indexOf('--cwd');
@@ -27,6 +27,7 @@
27
27
  import { isGlobalLayer, isProjectState } from './great-cto-scope.mjs';
28
28
  import { appendFileSync, closeSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, statSync } from 'node:fs';
29
29
  import { join } from 'node:path';
30
+ import { isMainModule } from './is-main.mjs';
30
31
 
31
32
  export const EVENTS_FILE = 'events.jsonl';
32
33
  // `hint`: a context hook (edit-impact, lesson-tripwire) put text in front of the model.
@@ -195,7 +196,7 @@ export function readEventsSince(dir, cursor, { limit = 50, maxDelta = MAX_DELTA
195
196
  return { state: 'live', mode: 'delta', events, cursor: next, bad };
196
197
  }
197
198
 
198
- if (import.meta.url === `file://${process.argv[1]}`) {
199
+ if (isMainModule(import.meta.url)) {
199
200
  try {
200
201
  const argv = process.argv.slice(2);
201
202
  const arg = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : undefined; };
@@ -25,6 +25,7 @@ import os from 'node:os';
25
25
  import path from 'node:path';
26
26
  import readline from 'node:readline';
27
27
  import { createHash } from 'node:crypto';
28
+ import { isMainModule } from './is-main.mjs';
28
29
 
29
30
  const INDEX_VERSION = 1;
30
31
 
@@ -154,7 +155,7 @@ export async function agentUsage({ logsDir = DEFAULT_LOGS_DIR, cacheFile = DEFAU
154
155
  }
155
156
 
156
157
  // CLI: node scripts/lib/agent-usage.mjs [--json]
157
- const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
158
+ const isMain = isMainModule(import.meta.url);
158
159
  if (isMain) {
159
160
  const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
160
161
  const agents = fs.readdirSync(path.join(root, 'agents')).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3));
@@ -18,6 +18,8 @@
18
18
  // agent's prompt, re-derived each time; one testable definition means the agents
19
19
  // agree by construction.
20
20
 
21
+ import { isMainModule } from './is-main.mjs';
22
+
21
23
  /** Gate sets per level, in pipeline order. */
22
24
  const LEVEL_GATES = Object.freeze({
23
25
  auto: [],
@@ -162,7 +164,7 @@ export function describeLevel(level, opts) {
162
164
  }
163
165
 
164
166
  // ── CLI ─────────────────────────────────────────────────────────────────────
165
- const isMain = import.meta.url === `file://${process.argv[1]}`;
167
+ const isMain = isMainModule(import.meta.url);
166
168
  if (isMain) {
167
169
  const argv = process.argv.slice(2);
168
170
  const level = argv.find((a) => !a.startsWith('--')) || DEFAULT_LEVEL;
@@ -24,9 +24,9 @@
24
24
  * → one JSON line: { cursor, changes: [...], timedOut }
25
25
  * With no --since it answers at once with the current cursor and nothing else.
26
26
  */
27
- import { fileURLToPath } from 'node:url';
28
27
  import { latestVerdicts } from './ship-evidence.mjs';
29
28
  import { readSessionStatus } from './session-status.mjs';
29
+ import { isMainModule } from './is-main.mjs';
30
30
 
31
31
  export const MAX_TIMEOUT_S = 600;
32
32
 
@@ -82,7 +82,7 @@ export async function waitForChange({ cwd = process.cwd(), since = null, timeout
82
82
  }
83
83
  }
84
84
 
85
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
85
+ const isMain = isMainModule(import.meta.url);
86
86
  if (isMain) {
87
87
  const a = process.argv.slice(2);
88
88
  const opt = (k, d) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : d; };
@@ -12,6 +12,7 @@
12
12
  //
13
13
  // Plan: docs/plans/PLAN-2026-06-18-gate-tiering-reviewer-consolidation.md
14
14
 
15
+ import { isMainModule } from './is-main.mjs';
15
16
  export const TIER_RANK = { T0: 0, T1: 1, T2: 2 };
16
17
 
17
18
  // Files that, on their own, never change external behavior. A change touching ONLY
@@ -113,7 +114,7 @@ export function classify(signals = {}) {
113
114
 
114
115
  // CLI: node scripts/lib/change-tier.mjs file1 file2 … → prints the tier + reasons
115
116
  // (a thin probe; the orchestrator imports classify() directly with richer signals).
116
- if (import.meta.url === `file://${process.argv[1]}`) {
117
+ if (isMainModule(import.meta.url)) {
117
118
  const r = classify({ changedFiles: process.argv.slice(2) });
118
119
  process.stdout.write(`${r.tier} ${r.reasons.join(', ')}\n`);
119
120
  }