@dzhechkov/skills-feature-adr 1.5.1 → 1.5.3

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/.dz-manifest.json CHANGED
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "README.md",
16
- "sha256": "47f9402d4c3954d6d5a75ad906ec7b592010fdfd80f347d4a87dd14dd9e300ce"
16
+ "sha256": "1e3c1a4ed97981b70a3a5099f6355556fc987e493bf478b82cabe552888d2583"
17
17
  },
18
18
  {
19
19
  "path": "bin/cli.js",
@@ -25,15 +25,7 @@
25
25
  },
26
26
  {
27
27
  "path": "package.json",
28
- "sha256": "3fbe9ef86c9f6fa7c86bbe7760729094e80edb67577e397678b2d98dca1c2034"
29
- },
30
- {
31
- "path": "scripts/sync-templates.js",
32
- "sha256": "790d53c2e98a4b9aa589c72c6394c0e3aeb6b99d4a0bade4c881d2a8d11cf12c"
33
- },
34
- {
35
- "path": "sources.json",
36
- "sha256": "2e7f84f7fa7e97aa38514a94e920747e648a6f2ad0a503fc50e7519658116065"
28
+ "sha256": "2224692687da750844ef25096d1fc41fc86d7e79743ac7a8f42bf51ba9018750"
37
29
  },
38
30
  {
39
31
  "path": "src/cli.js",
@@ -121,7 +113,7 @@
121
113
  },
122
114
  {
123
115
  "path": "templates/.claude/skills/feature-adr/SKILL.md",
124
- "sha256": "bdedfed69d985eb6969f158a72d92f63c6b1b993008c3661f7ffc2c6a7ccfbae"
116
+ "sha256": "67eb730c815e6b8f9b9b61084c67ceb85440548f00b21f52187d16e81e3d7af8"
125
117
  },
126
118
  {
127
119
  "path": "templates/.claude/skills/feature-adr/examples/sample-feature-output.md",
@@ -157,7 +149,7 @@
157
149
  },
158
150
  {
159
151
  "path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
160
- "sha256": "3903e2694ad47782914b35869bbf19c5f0803b6dafdcfb629df343a5ff00d1b9"
152
+ "sha256": "b3efdfa1d44f4c51ff46551b5827066955c1f0bfee212d371ac1e2fb1c99980f"
161
153
  },
162
154
  {
163
155
  "path": "templates/.claude/skills/feature-adr/modules/07-code.md",
@@ -257,7 +249,7 @@
257
249
  },
258
250
  {
259
251
  "path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
260
- "sha256": "2e1afeb8cbc08ce872fc8280a0809e7712ee76879d08b48caa5a0d4082cb8647"
252
+ "sha256": "eddf116620050e80b88497ace026fb8e0a12986f94f216181480c7daa0e08be4"
261
253
  },
262
254
  {
263
255
  "path": "templates/.claude/skills/frontend-design/LICENSE.txt",
@@ -321,7 +313,7 @@
321
313
  },
322
314
  {
323
315
  "path": "templates/.claude/workflows/feature-adr.js",
324
- "sha256": "0c74e8995d82bb986ef3233e91beebfa0e761aae4434f24554077bb05d2199e9"
316
+ "sha256": "a4e277040b42646d9bc1b06f78e32aa11f118a924f1626be67640f87ede8b292"
325
317
  },
326
318
  {
327
319
  "path": "templates/lib/memory-protocol.md",
@@ -330,24 +322,8 @@
330
322
  {
331
323
  "path": "templates/lib/reward-tracker.md",
332
324
  "sha256": "581feeda1b4b920f7013bde5938f5cb6ffe3e44f5d2426263c49f1047ade8d55"
333
- },
334
- {
335
- "path": "test/back-compat.test.js",
336
- "sha256": "74fa0b7f1e595eddef251b37b0bf6d6f97971f1c2d03348228a9913380128d54"
337
- },
338
- {
339
- "path": "test/helpers/sandbox.js",
340
- "sha256": "61e883fb74a00301b1578987aecfc5b0ff4f4db4ee64c90fae0035a24dcabc99"
341
- },
342
- {
343
- "path": "test/three-way.test.js",
344
- "sha256": "16c3b973996b6cdd2d070e413de54e9835a038bd37b0d3769f5cccccdd57e971"
345
- },
346
- {
347
- "path": "test/update-3way.test.js",
348
- "sha256": "8c34fb6b5202393b52cbfd29d3cd64b6d270675c2d8327d4c1a8366af8088ae7"
349
325
  }
350
326
  ]
351
327
  },
352
- "signature": "9WVoqK2D8eKSnmhhMqHPt31NedxwZCHiHyxJ5VLMxSVMecMqyXgrCJ2GQ3OOfIeiquB2D01bh7QjZW/EMVM9Aw=="
328
+ "signature": "uUN7yuKgSOed/hE0xooh1K788NcAFhnvsh92AHcu4oHn/uMZurn0VnktfhsUDHUv3jR8F6bMoKgrb/F6dHrvCQ=="
353
329
  }
package/README.md CHANGED
@@ -169,7 +169,7 @@ refused BY NAME: `path traversal ('..' segment)`, `degenerate path segment`, `em
169
169
  `illegal character '<c>'`, `empty stem after the leading dot`. The old substring traversal test also
170
170
  rejected the ordinary filename `foo..bar.ts`; it is a segment test now.
171
171
 
172
- ### The Step-8 amendment gate runs a command instead of judging (v1.5.1)
172
+ ### The Step-8 amendment gate runs a command instead of judging (v1.5.3)
173
173
 
174
174
  Every `AM-N` row must resolve to a test found INSIDE the file the row names, and the check is
175
175
  `dz amendment-check --slug <slug> --json` rather than a paragraph asking the reviewer to confirm it.
@@ -1107,3 +1107,36 @@ structure chosen before understanding, and section-per-edit raises tool-call ove
1107
1107
 
1108
1108
  Steps whose deliverable is a returned verdict rather than a document (the complexity router) are
1109
1109
  deliberately excluded — "skeleton first" is nonsense there.
1110
+
1111
+
1112
+ ---
1113
+
1114
+ ## Status
1115
+
1116
+ `1.5.3` — **the workflow stops crashing on the way into Step 7.** `1.5.3` shipped a workflow that
1117
+ CALLED three helpers it never defined — `changeSetProbeCmd`, `parseHashProbe`, `changedFromHashes`
1118
+ (5 call sites, 0 definitions). `QE_SCOPE` defaults to `uncommitted`, so the guarded branch was true
1119
+ by default and every ultracode run that reached the coding step died with a `ReferenceError` while
1120
+ the corresponding unit tests stayed green — they exercise the exported module, the pipeline runs an
1121
+ inline mirror of it. The three are now restored FROM that canonical export, not reconstructed, and
1122
+ two guards make the class visible: the mirror is checked by lifting each function out of the shipped
1123
+ file and comparing its BEHAVIOUR case-by-case against the export, and a new layer-1 test parses the
1124
+ workflow and asserts every referenced identifier is declared or is one of the eight documented
1125
+ sandbox globals.
1126
+
1127
+ Also in this release, both halves of the K2 plan-completeness gate that field use found:
1128
+
1129
+ - **C6 scopes each amendment to its own block** — from its `AM-N` line to the line where the next one
1130
+ begins. The old three-line window refused amendments whose `→ test` marker sat on a `Confirmation:`
1131
+ line further down (measured: line 3 passes, line 4 fails), and — worse, and not reported — it
1132
+ PASSED a testless amendment that happened to sit next to a tested one, which borrowed its
1133
+ neighbour's marker. The plural `→ tests \`a\` and \`b\`` form is now matched, and a wrapped
1134
+ `AM-1..AM-4;` range no longer opens a phantom amendment. Measured over 142 plans: 402 C6 failures
1135
+ before, 190 after, **0 plans newly failing**. The Step-6 planner prompt now states that the row is
1136
+ machine-read and where the marker must sit.
1137
+ - **The K2 gate stops guessing the workspace.** When the workspace was not pinned it was taken from
1138
+ the gate agent's own working directory, which against an external target repo resolved to that
1139
+ repo — so the workspace candidate silently became a duplicate of the repo candidate and a skill
1140
+ installed in the workspace was never found (`NOT-ESTABLISHED`, exit 3, the coding step never ran).
1141
+ New `args.workspace` pins it, the shipped call site passes it, each candidate is now labelled in the
1142
+ audit line, and a `K2_GATE_NOTE` fires when the two collapse onto one path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "Adaptive Feature Development skill pack for Claude Code — 11-step pipeline with Complexity Router (S/M/L/XL), ADR-driven architecture, 15 agentic-qe skills, multi-agent fleet QE. Supports --full-qe, --full-qe-extended, --with-learning, and --knowledge-extractor modes.",
5
5
  "bin": {
6
6
  "skills-feature-adr": "./bin/cli.js"
package/sbom.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "47f9402d4c3954d6d5a75ad906ec7b592010fdfd80f347d4a87dd14dd9e300ce"
38
+ "content": "1e3c1a4ed97981b70a3a5099f6355556fc987e493bf478b82cabe552888d2583"
39
39
  }
40
40
  ]
41
41
  },
@@ -65,27 +65,7 @@
65
65
  "hashes": [
66
66
  {
67
67
  "alg": "SHA-256",
68
- "content": "3fbe9ef86c9f6fa7c86bbe7760729094e80edb67577e397678b2d98dca1c2034"
69
- }
70
- ]
71
- },
72
- {
73
- "type": "file",
74
- "name": "scripts/sync-templates.js",
75
- "hashes": [
76
- {
77
- "alg": "SHA-256",
78
- "content": "790d53c2e98a4b9aa589c72c6394c0e3aeb6b99d4a0bade4c881d2a8d11cf12c"
79
- }
80
- ]
81
- },
82
- {
83
- "type": "file",
84
- "name": "sources.json",
85
- "hashes": [
86
- {
87
- "alg": "SHA-256",
88
- "content": "2e7f84f7fa7e97aa38514a94e920747e648a6f2ad0a503fc50e7519658116065"
68
+ "content": "2224692687da750844ef25096d1fc41fc86d7e79743ac7a8f42bf51ba9018750"
89
69
  }
90
70
  ]
91
71
  },
@@ -305,7 +285,7 @@
305
285
  "hashes": [
306
286
  {
307
287
  "alg": "SHA-256",
308
- "content": "bdedfed69d985eb6969f158a72d92f63c6b1b993008c3661f7ffc2c6a7ccfbae"
288
+ "content": "67eb730c815e6b8f9b9b61084c67ceb85440548f00b21f52187d16e81e3d7af8"
309
289
  }
310
290
  ]
311
291
  },
@@ -395,7 +375,7 @@
395
375
  "hashes": [
396
376
  {
397
377
  "alg": "SHA-256",
398
- "content": "3903e2694ad47782914b35869bbf19c5f0803b6dafdcfb629df343a5ff00d1b9"
378
+ "content": "b3efdfa1d44f4c51ff46551b5827066955c1f0bfee212d371ac1e2fb1c99980f"
399
379
  }
400
380
  ]
401
381
  },
@@ -645,7 +625,7 @@
645
625
  "hashes": [
646
626
  {
647
627
  "alg": "SHA-256",
648
- "content": "2e1afeb8cbc08ce872fc8280a0809e7712ee76879d08b48caa5a0d4082cb8647"
628
+ "content": "eddf116620050e80b88497ace026fb8e0a12986f94f216181480c7daa0e08be4"
649
629
  }
650
630
  ]
651
631
  },
@@ -805,7 +785,7 @@
805
785
  "hashes": [
806
786
  {
807
787
  "alg": "SHA-256",
808
- "content": "0c74e8995d82bb986ef3233e91beebfa0e761aae4434f24554077bb05d2199e9"
788
+ "content": "a4e277040b42646d9bc1b06f78e32aa11f118a924f1626be67640f87ede8b292"
809
789
  }
810
790
  ]
811
791
  },
@@ -828,46 +808,6 @@
828
808
  "content": "581feeda1b4b920f7013bde5938f5cb6ffe3e44f5d2426263c49f1047ade8d55"
829
809
  }
830
810
  ]
831
- },
832
- {
833
- "type": "file",
834
- "name": "test/back-compat.test.js",
835
- "hashes": [
836
- {
837
- "alg": "SHA-256",
838
- "content": "74fa0b7f1e595eddef251b37b0bf6d6f97971f1c2d03348228a9913380128d54"
839
- }
840
- ]
841
- },
842
- {
843
- "type": "file",
844
- "name": "test/helpers/sandbox.js",
845
- "hashes": [
846
- {
847
- "alg": "SHA-256",
848
- "content": "61e883fb74a00301b1578987aecfc5b0ff4f4db4ee64c90fae0035a24dcabc99"
849
- }
850
- ]
851
- },
852
- {
853
- "type": "file",
854
- "name": "test/three-way.test.js",
855
- "hashes": [
856
- {
857
- "alg": "SHA-256",
858
- "content": "16c3b973996b6cdd2d070e413de54e9835a038bd37b0d3769f5cccccdd57e971"
859
- }
860
- ]
861
- },
862
- {
863
- "type": "file",
864
- "name": "test/update-3way.test.js",
865
- "hashes": [
866
- {
867
- "alg": "SHA-256",
868
- "content": "8c34fb6b5202393b52cbfd29d3cd64b6d270675c2d8327d4c1a8366af8088ae7"
869
- }
870
- ]
871
811
  }
872
812
  ]
873
813
  }
@@ -100,6 +100,9 @@ ROUTER → REQUIREMENTS → RESEARCH → ADR → QCSD SWARM
100
100
  SPARC-GOAP PLAN
101
101
  (all)
102
102
 
103
+ K1 NAME-AVAILABILITY GATE
104
+ (every new command/module/export)
105
+
103
106
  K2 PLAN-COMPLETENESS GATE
104
107
  (all — exit 0 or Step 7 waits)
105
108
 
@@ -147,6 +150,27 @@ Step | S | M | L | XL | Model | Agentic QE Skill
147
150
  10 Delivery Gate (OPT-IN) | o | o | o | o | cross-family of coder | 4 planes: regressions ‖ security ‖ code-quality ‖ product-honesty (o = runs only when explicitly requested; absent ⇒ byte-identical) |
148
151
  ```
149
152
 
153
+ ### K1 name-availability gate (Step-6/7 boundary, MANDATORY, every new name)
154
+
155
+ Before Step 7 writes a line, every NEW name the plan introduces is checked:
156
+
157
+ ```bash
158
+ dz name-check --command <cmd> --module <basename> --export <a,b,c>
159
+ ```
160
+
161
+ `exit 0` → proceed · `exit 1` → rename in the plan first · `exit 2` → NOT ESTABLISHED (the sweep
162
+ found nothing of that kind — fix the invocation, never read it as free).
163
+
164
+ **Why this is a gate and not advice.** Twice on 2026-08-23/24 a collision broke the build outright —
165
+ `dz retro` was already a command and its star re-export clash stopped the CLI from importing at all;
166
+ `decideProvenance` was already an export. Both were answerable before any code. The check costs one
167
+ command; the miss costs a rename across every artifact of the feature. An agent's intention to
168
+ remember is layer 4 on the cost-of-detection ladder, and this line exists because that layer failed
169
+ twice in one day.
170
+
171
+ The check reads workspace SOURCE, never `dist` — a stale build answers "free" confidently. Its honest
172
+ limit is printed on the passing path: a re-export under a different name stays the build's job.
173
+
150
174
  ### K2 plan-completeness gate (Step-6/7 boundary, MANDATORY, all tiers)
151
175
 
152
176
  Step 7 does not start until the plan passes a SCRIPT — not a reviewer's impression:
@@ -185,6 +185,11 @@ Create `features/<slug>/06_implementation_plan.md` with:
185
185
  - `## Amendments` — every correction folded into this plan (a Step-3.5 CONDITIONAL condition, a
186
186
  challenge-panel confirmed finding, a user checkpoint steer) as a fixed-shape row:
187
187
  `AM-N (source): <change>. Confirmation: <property> → test `test_name` (fails if reverted).`
188
+ The row is MACHINE-READ by the K2 C6 gate: the `→ test \`name\`` marker (or `superseded by AM-N`)
189
+ must sit inside that amendment's OWN block — anywhere between its `AM-N` line and the line where
190
+ the NEXT `AM-N` begins. Multi-line amendments are fine; what is NOT fine is putting a marker after
191
+ the following amendment has already started, because it then belongs to that one. A bare range
192
+ like `AM-1..AM-4` never opens a row.
188
193
  A safeguard amendment's named test must prove it TRIGGERS on a real input, not merely that its code
189
194
  path exists. Step 8 verifies every named test exists and is non-vacuous (`dz discrimination-check`).
190
195
 
@@ -292,6 +292,91 @@ if (acidArg) {
292
292
  } else if (acidTokens.length === 0) skips.push('C4: no acid corpus declared (no --acid, no `| A<n> |` table in 00_complexity_assessment.md) — acid-naming check SKIPPED');
293
293
  else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(plan)) failures.push(`C4: acid token "${safe(t)}" (from ${acidSource}) not named in the plan`);
294
294
 
295
+ // ── C6 amendment integrity (backlog 72b89e14, seams б/в/г) ──────────────────────────────────────
296
+ // The measured shape: AM-rows living OUTSIDE `## Amendments` were invisible to every gate; four
297
+ // amendments named no test at all; a retracted amendment (AM-23 cancelling AM-20) left the old one
298
+ // standing. One deterministic check for all three: every AM-token belongs to the section, and every
299
+ // AM row in the section carries `→ test ...` OR an explicit `superseded by AM-N`.
300
+ {
301
+ // Line-scan, not regex-over-document: \Z is Python, and $-lookahead under /m matches every
302
+ // line end — a section with no FOLLOWING heading silently failed to parse (caught by the
303
+ // fixture the moment the tests ran).
304
+ const planLines = plan.split('\n');
305
+ let sectionStart = -1, sectionEnd = -1, cursor = 0;
306
+ for (const pl of planLines) {
307
+ if (sectionStart < 0 && /^##\s*Amendments\s*$/.test(pl)) sectionStart = cursor + pl.length + 1;
308
+ else if (sectionStart >= 0 && sectionEnd < 0 && /^##\s/.test(pl)) sectionEnd = cursor;
309
+ cursor += pl.length + 1;
310
+ }
311
+ if (sectionStart >= 0 && sectionEnd < 0) sectionEnd = plan.length;
312
+ const amSection = sectionStart >= 0 ? plan.slice(sectionStart, sectionEnd) : '';
313
+ // Only a DEFINITION-shaped line counts as a stray: a list item opening with the AM token.
314
+ // A mid-prose REFERENCE («per 01_requirements.md (AM-1..AM-6)», «(AM-1: the clause stays
315
+ // dropped)») cites an amendment defined in ANOTHER artifact and is legitimate — the first cut
316
+ // failed the real wave1 corpus on exactly that (caught by the standing acid test).
317
+ {
318
+ let cursor2 = 0;
319
+ for (const pl of planLines) {
320
+ const isDef = /^\s*(?:[-*|]\s*)\*{0,2}AM-(?:CP-)?\d+\b/.test(pl);
321
+ const inSection = sectionStart >= 0 && cursor2 >= sectionStart && cursor2 < sectionEnd;
322
+ if (isDef && !inSection) {
323
+ const tok = (/AM-(?:CP-)?\d+/.exec(pl) || ['AM-?'])[0];
324
+ failures.push(`C6: ${tok} is DEFINED outside the \`## Amendments\` section (line: "${pl.trim().slice(0, 80)}") — an amendment outside the section is invisible to every downstream reader`);
325
+ }
326
+ cursor2 += pl.length + 1;
327
+ }
328
+ }
329
+ if (sectionStart >= 0) {
330
+ // Each amendment's confirmation is looked for in ITS OWN BLOCK: from its definition line to the
331
+ // line where the NEXT amendment begins. Three defects die with the old three-line window
332
+ // (MEASURED 2026-08-25 against the 142-plan corpus — 292 C6 failures before, 33 after, and ZERO
333
+ // rows newly caught that pass today):
334
+ // • FALSE REFUSAL — a marker on the `Confirmation:` line 4+ lines down was invisible, so plans
335
+ // that DID name their test were rejected (7 per run, twice in one day, on a user's machine).
336
+ // • FALSE PASS — the window is three LINES, not one amendment, so `- AM-1 testless` followed by
337
+ // `- AM-2 … -> test x` PASSED: AM-1 borrowed its neighbour's marker. That is the safety half,
338
+ // and it is why the boundary is the next DEFINITION rather than a blank line — a blank line
339
+ // does not separate adjacent bullet rows.
340
+ // • A substring seek (`amSection.indexOf(lnRaw)`) let a duplicated line read someone else's
341
+ // window. Indices remove that hazard for free.
342
+ // The boundary is not invented: parseAmendments() in harness-core/src/amendment-trace.ts has used
343
+ // the same next-definition bound all along, so this also ends a divergence between two checkers.
344
+ const secLines = amSection.split('\n');
345
+ // The bullet stays OPTIONAL: requiring it drops 104 of the corpus's 347 real AM rows out of the
346
+ // check entirely (MEASURED). The `(?!\s*\.)` guard is what refuses a wrapped `AM-1..AM-4;`
347
+ // range preamble, which used to open a phantom amendment and double-count AM-1 — it costs 0 rows.
348
+ const DEF = /^(?:[-*|]\s*)?\*{0,2}(AM-(?:CP-)?\d+)\*{0,2}\b(?!\s*\.)/;
349
+ // `tests?` — the two-id form `-> tests \`a\` and \`b\`` is corpus-canonical and is what
350
+ // amendment-trace.ts parses; matching only the singular turned two currently-green rows red.
351
+ const MARK = /\u2192\s*tests?\b|->\s*tests?\b/;
352
+ const defs = [];
353
+ secLines.forEach((l, i) => { const m = DEF.exec(l.trim()); if (m) defs.push({ i, id: m[1] }); });
354
+ for (let k = 0; k < defs.length; k++) {
355
+ const to = k + 1 < defs.length ? defs[k + 1].i : secLines.length;
356
+ const block = secLines.slice(defs[k].i, to).join('\n');
357
+ const hasTest = MARK.test(block);
358
+ const superseded = /superseded by AM-(?:CP-)?\d+/i.test(block);
359
+ if (!hasTest && !superseded) failures.push(`C6: ${defs[k].id} carries neither \`\u2192 test <name>\` nor \`superseded by AM-N\` — an amendment without a confirmation is a wish, and a retracted one must say its successor`);
360
+ }
361
+ }
362
+ }
363
+
364
+ // ── C7 ADR↔plan command-name drift (seam д) — WARN, deliberately never FAIL ─────────────────────
365
+ // The measured incident: the ADR said `dz policy-sync`, the plan said `dz agents-sync` (and even
366
+ // carried a test that the OTHER alias must not exist) — whichever file the coder opened first won.
367
+ // Deterministically provable is only the SET DIFFERENCE, not which side is right, so this warns
368
+ // with both sides named and never blocks.
369
+ {
370
+ const adrAll = adrFiles.map((f) => { try { return readFileSync(join(adrDir, f), 'utf-8'); } catch { return ''; } }).join('\n');
371
+ const cmds = (t) => new Set([...t.matchAll(/`dz ([a-z][a-z0-9-]+)`/g)].map((m) => m[1]));
372
+ const inAdr = cmds(adrAll); const inPlan = cmds(plan);
373
+ const onlyAdr = [...inAdr].filter((c) => !inPlan.has(c));
374
+ const onlyPlan = [...inPlan].filter((c) => !inAdr.has(c));
375
+ if (adrAll !== '' && onlyAdr.length > 0 && onlyPlan.length > 0) {
376
+ warnings.push(`C7: ADR and plan cite DIFFERENT dz commands — ADR-only: ${onlyAdr.join(', ')}; plan-only: ${onlyPlan.join(', ')} — if these name the SAME thing, one of the two files is lying to the coder (seam д)`);
377
+ }
378
+ }
379
+
295
380
  // C5 — Inputs read line
296
381
  if (!/Inputs read:/i.test(plan)) warnings.push('C5: no "Inputs read:" line (wave-2 seam, WARN only)');
297
382
  else for (const need of ['03_adr','05_architecture']) if (!plan.includes(need)) warnings.push(`C5: Inputs read line missing ${need}`);
@@ -143,6 +143,11 @@ log('dz binary: ' + DZ)
143
143
  // HERE, at invocation time, so a bad value fails at the same layer the pure half fails rather than
144
144
  // two layers later inside an emitted shell command.
145
145
  const GATE_SCRIPT_ARG = (A.gateScript === undefined || A.gateScript === null) ? undefined : assertAbsoluteNoTraversal(A.gateScript, 'gateScript')
146
+ // ADR-002 amendment (field report doc-21): WS was populated ONLY for a relative args.repo, and the
147
+ // shell fallback WS=$(pwd -P) runs in the GATE AGENT own cwd. On a run against an external repo it
148
+ // equalled REPO, so the workspace candidate pointed at the target repo and the skill installed in
149
+ // the workspace was never found - NOT-ESTABLISHED, exit 3, Step 7 never ran. args.workspace pins it.
150
+ if (A.workspace !== undefined && A.workspace !== null) WS = assertAbsoluteNoTraversal(A.workspace, 'workspace')
146
151
  // CANONICAL BRAIN store: the self-learning loop (Step-0 recall → Step-8 teach) MUST read+write ONE
147
152
  // shared pattern store so lessons never fragment into a target repo's .dz when the Step-7 coder cd's
148
153
  // away. BRAIN defaults to the workspace root (REPO) — so an OMITTED args.brain is behaviorally inert
@@ -152,7 +157,11 @@ const BRAIN = (A.brain || REPO).replace(/\/+$/, '')
152
157
  // Helpers PIN every learn-loop command to the canonical brain: `cd <BRAIN> &&` survives a cd'd agent
153
158
  // (belt); `--project <BRAIN>` is explicit (suspenders). Either alone fixes it; together they also
154
159
  // survive the relative-vs-absolute --project resolution asymmetry between recall and teach.
155
- const DZ_RECALL = (terms) => 'cd ' + BRAIN + ' && ' + DZ + ' recall "' + terms + '" --project ' + BRAIN
160
+ // `--run fa:<SLUG>` threads the run key into the usage log, so the panel's `--recalled auto`
161
+ // counts THIS run's recall events instead of asserting a literal (the old `--recalled 3`). The
162
+ // key is the slug: a resumed run keeps accumulating under the same key, which matches how the
163
+ // panel is read — per-feature, not per-invocation.
164
+ const DZ_RECALL = (terms) => 'cd ' + BRAIN + ' && ' + DZ + ' recall "' + terms + '" --project ' + BRAIN + ' --run fa:' + SLUG
156
165
  const DZ_TEACH = (lesson, reward, domain) =>
157
166
  'cd ' + BRAIN + ' && ' + DZ + ' teach "' + lesson + '" --reward ' + reward + ' --domain ' + domain + ' --project ' + BRAIN
158
167
 
@@ -692,7 +701,7 @@ const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
692
701
  // block threads workflow-local state (MODELS/knobs) through shapes the generic blobs do not carry;
693
702
  // regeneration via model-resolver/usage-probes/codex-dispatch blobs is the tracked Stage-B item.
694
703
  const MODELS = (A.models && typeof A.models === 'object') ? A.models : {}
695
- const KNOWN_CODEX = { 'auto': 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-sol': 1 }
704
+ const KNOWN_CODEX = { 'auto': 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 }
696
705
  const CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 }
697
706
  const VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1 }
698
707
  const DEFAULT_MODELS = { router: 'fable', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: null, qe: null, fleet: 'sonnet' }
@@ -1743,7 +1752,12 @@ function planCompletenessGateCmd(repo, featureDir, tier, opts) {
1743
1752
  // and the tried paths live OUTSIDE the verdict line so no path can smuggle a second verdict word
1744
1753
  // into it.
1745
1754
  'echo "K2_GATE_SCRIPT=${GS:-none}"',
1746
- 'echo "K2_GATE_TRIED=${C1:-(none)} | $C2 | $C3"',
1755
+ 'echo "K2_GATE_TRIED=C1(args.gateScript)=${C1:-<unset>} | C2(workspace)=$C2 | C3(target-repo)=$C3"',
1756
+ // A COLLAPSE is not a second candidate. When the workspace was not pinned, WS falls back to the
1757
+ // gate agent own cwd — in the field that WAS the target repo, so C2 and C3 printed the same path
1758
+ // twice and the chain silently degenerated from three candidates to two. Saying so turns a
1759
+ // puzzling duplicate into an instruction. Not verdict-shaped, so the parser anchoring is untouched.
1760
+ '[ "$C2" = "$C3" ] && echo "K2_GATE_NOTE=the workspace candidate resolved to the TARGET repo (WS==repo), so only two distinct candidates were tried; pass args.workspace or args.gateScript when the feature-adr skill is installed outside the target repo"',
1747
1761
  'if [ -z "$GS" ]; then echo "K2 plan-completeness: NOT-ESTABLISHED — tooling-missing: no gate script at any candidate on the K2_GATE_TRIED line above"; echo "K2_EXIT=3"; else cd ' + q(repo) + ' && node "$GS" ' + q(featureDir) + t + ' 2>&1; echo "K2_EXIT=$?"; fi',
1748
1762
  ].join('\n')
1749
1763
  }
@@ -1830,6 +1844,57 @@ function decideModeBScope(o) {
1830
1844
  return { ok: true, files: files, dropped: dropped }
1831
1845
  }
1832
1846
 
1847
+ // ── QE CHANGE-SET PROBE — inline mirror of harness-core/src/feature-adr-routing.ts:2309-2381.
1848
+ // These three travelled as CALL SITES only: commit 6a92d189 ("measure the DELTA, not the current
1849
+ // state") shipped the TS module, its 19 tests and both call sites below, and never inlined the
1850
+ // helpers here. Every run reaching Step 7 therefore died with a ReferenceError on changeSetProbeCmd
1851
+ // while those 19 tests stayed green — they exercise the export, this file runs the mirror. Ported
1852
+ // byte-faithfully from the canonical: sha256sum (not shasum), the ~1 and ...HEAD ref forms, a Map
1853
+ // snapshot seeded with nulls from the declared list (absence is a null VALUE, never an ABSENT
1854
+ // line), and null returned ONLY by changedFromHashes — that is the single "not established" signal.
1855
+ // NOTE: no template literals below. This region sits inside the parser-safe block the routing test
1856
+ // guards (it runs from const MODELS to const ROUTER), so even a backtick in a COMMENT reddens it.
1857
+ function parseHashProbe(text, declared) {
1858
+ const out = new Map()
1859
+ for (const p of declared) out.set(String(p), null)
1860
+ for (const raw of String(text === null || text === undefined ? '' : text).split('\n')) {
1861
+ const line = raw.trim()
1862
+ if (line === '') continue
1863
+ const m = /^([0-9a-f]{64})\s+(.+)$/.exec(line)
1864
+ if (m === null || m[1] === undefined || m[2] === undefined) continue
1865
+ const path = m[2].trim().replace(/^\.\//, '')
1866
+ if (out.has(path)) out.set(path, m[1])
1867
+ }
1868
+ return out
1869
+ }
1870
+
1871
+ function changedFromHashes(before, after) {
1872
+ if (before === null || before === undefined || after === null || after === undefined) return null
1873
+ const changed = []
1874
+ for (const [path, afterHash] of after) {
1875
+ const beforeHash = before.has(path) ? (before.get(path) === undefined ? null : before.get(path)) : null
1876
+ if (beforeHash !== (afterHash === null || afterHash === undefined ? null : afterHash)) changed.push(path)
1877
+ }
1878
+ return changed.sort()
1879
+ }
1880
+
1881
+ function changeSetProbeCmd(opts) {
1882
+ const paths = opts.paths.map(function (p) { return String(p) }).filter(function (p) { return p !== '' })
1883
+ if (paths.length === 0) return null
1884
+ const quoted = paths.map(opts.quote).join(' ')
1885
+ const ref = String(opts.ref === null || opts.ref === undefined ? '' : opts.ref).trim()
1886
+ if (opts.scope === 'commit') {
1887
+ if (ref === '') return null
1888
+ return 'git diff --name-only ' + opts.quote(ref) + '~1 ' + opts.quote(ref) + ' -- ' + quoted
1889
+ }
1890
+ if (opts.scope === 'base') {
1891
+ if (ref === '') return null
1892
+ return 'git diff --name-only ' + opts.quote(ref) + '...HEAD -- ' + quoted
1893
+ }
1894
+ // uncommitted: hash the declared targets; the caller pairs this with a pre-code baseline.
1895
+ return 'sha256sum -- ' + quoted + ' 2>/dev/null || true'
1896
+ }
1897
+
1833
1898
  function crossFamilyQe(o) {
1834
1899
  // NORMALISE first — raw-string comparison let 'Claude' vs 'claude' report a cross-family review
1835
1900
  // that never happened (caught by codex review --commit on the TS-only fix: the exported helper
@@ -1871,7 +1936,12 @@ async function designStage(promptText, opts, artifactPath, baseLabel) {
1871
1936
  if (res && probe && /landed=/.test(String(probe))) return { wrote: [artifactPath], summary: String(res).slice(0, 300) }
1872
1937
  log('design artifact did not land on codex (' + artifactPath + ') — falling back to Claude')
1873
1938
  const fallbackOpts = {}
1874
- return await agent(promptText, mergeOpts({ label: stageLabel((baseLabel || 'design') + ':claude-fb', fallbackOpts), phase: 'Design', schema: ARTIFACT }, fallbackOpts))
1939
+ const fb = await agent(promptText, mergeOpts({ label: stageLabel((baseLabel || 'design') + ':claude-fb', fallbackOpts), phase: 'Design', schema: ARTIFACT }, fallbackOpts))
1940
+ // d926ee89: the fallback used to keep CODEX provenance — modelsUsed, the checkpoint label and the
1941
+ // training-pair family all still said codex after Claude wrote the artifact. The WRITER is the
1942
+ // provenance; overwrite it here, at the one place that knows the fallback fired.
1943
+ if (fb && baseLabel && modelsUsed[baseLabel] !== undefined) modelsUsed[baseLabel] = modelLabel(fallbackOpts) + ' (claude-fallback after codex not-landed)'
1944
+ return fb
1875
1945
  }
1876
1946
 
1877
1947
  const ROUTER = { type: 'object', additionalProperties: false, required: ['tier', 'activeSteps', 'rationale'], properties: { tier: { type: 'string', enum: ['S', 'M', 'L', 'XL'] }, activeSteps: { type: 'array', items: { type: 'number' } }, rationale: { type: 'string' } } }
@@ -1893,7 +1963,7 @@ const ADR_FITNESS_CHECKLIST = 'ADR fitness checklist for Step 8: read every ' +
1893
1963
  // judgment cannot carry (444 tests stayed green around undefended protections, including the exact MEASURED
1894
1964
  // exploit string). Advisory, exactly like the discrimination gate: findings are HIGH gaps, never an auto-abort,
1895
1965
  // and the pipeline never blocks on the tool.
1896
- const MUTATION_GATE = 'MUTATION GATE (feature ha-mutation-gate — run alongside the discrimination gate): if a touched package carries a mutation registry (test/mutation-registry.json or mutation-registry.json), via Bash run EXACTLY `dz mutation-gate --package <that package dir> --json` and parse {results, summary, exitCode}. Any `UNDEFENDED` result = a named protection whose suite stays GREEN with the protection deleted → record a HIGH gap naming the property (advisory — the owner decides). `NOT_APPLIED` = the registry drifted from the code → HIGH gap "mutation registry drifted: <id>" (a skipped mutation proves nothing — inconclusive is never a pass). A result with `drop: true` (failing count below the recorded `observed`) is the early warning that a protection is LOSING test coverage — note it in the report before the property breaks. If this feature ADDED or FIXED a named safety property in a package that has a registry, ADD a registry entry for it (an exact {find, replace} that deletes the protection, with the measured failing count as `observed`) so the property stays machine-defended after this run. If no registry exists or `dz mutation-gate` is unavailable, note that and continue never block on the tool. Record the verdict in the 08_qe_report.md ADR Fitness section.'
1966
+ const MUTATION_GATE = 'MUTATION GATE (feature ha-mutation-gate — run alongside the discrimination gate): if a touched package carries a mutation registry (test/mutation-registry.json or mutation-registry.json), via Bash run EXACTLY `' + DZ + ' mutation-gate --package <that package dir> --json` (the PINNED workspace bin — the global `dz` on this host measurably LACKS the command: `dz --help | grep -c mutation-gate` -> 0 while the workspace dist has it, so a bare `dz` silently loses the gate) and parse {results, summary, exitCode}. Any `UNDEFENDED` result = a named protection whose suite stays GREEN with the protection deleted → record a HIGH gap naming the property (advisory — the owner decides). `NOT_APPLIED` = the registry drifted from the code → HIGH gap "mutation registry drifted: <id>" (a skipped mutation proves nothing — inconclusive is never a pass). A result with `drop: true` (failing count below the recorded `observed`) is the early warning that a protection is LOSING test coverage — note it in the report before the property breaks. If this feature ADDED or FIXED a named safety property in a package that has a registry, ADD a registry entry for it (an exact {find, replace} that deletes the protection, with the measured failing count as `observed`) so the property stays machine-defended after this run. If NO REGISTRY exists that is a clean skip — say so. But instrument-failure is NOT a skip (backlog 52d0ed08): `mutation-gate` unavailable at the pinned path, erroring, or overrunning its window → record a HIGH gap `mutation gate INCONCLUSIVE: <unavailable|error|timeout>` — an instrument that could not run proves nothing and must not read as «не применимо». Still never abort the run. Record the verdict in the 08_qe_report.md ADR Fitness section.'
1897
1967
  // no-stubs (backlog 0b403a0106103901, Karpathy-Michaels rule XI): an unfinished stub left in the
1898
1968
  // run's own touched files means the task shipped incomplete — a deterministic grep is layer 1 on the
1899
1969
  // cost-of-detection ladder; reviewer judgment is layer 4. Marker strings are ASSEMBLED so this
@@ -1902,18 +1972,38 @@ const MUTATION_GATE = 'MUTATION GATE (feature ha-mutation-gate — run alongside
1902
1972
  const STUB_RX = '(^|[^A-Za-z0-9_])(' + ['TO' + 'DO', 'FIX' + 'ME', 'HA' + 'CK', 'XX' + 'X', 'PLACE' + 'HOLDER'].join('|') + ')([^A-Za-z0-9_]|$)'
1903
1973
  const STUB_PHRASE = 'imple' + 'ment later'
1904
1974
  const NO_STUBS_GATE = 'NO-STUBS GATE (backlog 0b403a0106103901 — layer 1 of the cost-of-detection ladder): over the files THIS RUN touched (the Step-7 change list; for a Codex coder, the landed-barrier file list), via Bash run EXACTLY `grep -nE \'' + STUB_RX + '\' <touched files>` (case-SENSITIVE — never add -i) plus `grep -niE \'' + STUB_PHRASE.replace(' ', '[[:space:]]+') + '\' <touched files>`. ANY match = the task shipped incomplete → HIGH gap naming file:line, UNLESS the line carries an inline `no-stubs: <reason>` waiver WITH a non-empty reason, or `.dz/guard.json` stubWaivers lists the path WITH a reason — a REASONLESS waiver is itself a HIGH gap, never an exemption. Cross-check mechanically: `dz guard check --op publish --json` runs the same scan as the SOFT `no-stubs` rule over the working-tree diff. When you QUOTE a marker in 08_qe_report.md, backtick it so the report itself scans clean (the same convention as the claim-check forbidden-phrase escape). Record the verdict in the 08_qe_report.md ADR Fitness section.'
1905
- const DISCRIMINATION_GATE = '\u00a742 TEST-DISCRIMINATION GATE (run right after asserting the property has a test): the ADR Confirmation names `Required automated check: <test file>` for the load-bearing property. Prove that test DISCRIMINATES \u2014 via Bash run EXACTLY `dz discrimination-check --test <that test file> --base HEAD --json` (the Step-7 feature diff is UNCOMMITTED, so HEAD is the pre-feature base). Parse the JSON: read `perTest[]` (each row carries verdict + reason), `findings[]` (ALL entries, not only the first), `measurementValid`, and `primaryAction` \u2014 the singular `finding` is a DEPRECATED alias; do not consume it. The SEVEN verdicts and the required QE action for each: `DISCRIMINATES` (assertion-red at base, execution-evidenced) = PASS. `DISCRIMINATES_VIA_ERROR` (evidenced load-error at base + evidenced pass at tip) = PASS \u2014 note the inference. `NON_DISCRIMINATING` (evidenced pass at base \u2014 a proven false green) \u2192 HIGH gap "property test does not discriminate: <file>"; advisory, not an automatic blocker. `TEST_FILE_ABSENT` (the named test is not a regular file) \u2192 HIGH gap; action create-missing-test; NEVER a pass. `LOAD_ERROR_AT_BOTH_REVS` (the instrument could not execute the test at either rev \u2014 zero signal) \u2192 HIGH gap; action fix-runner-invocation. `FAILS_AT_TIP` (the feature\'s own test is red WITH the feature present) \u2192 HIGH gap; action fix-red-feature-test \u2014 grade the feature code accordingly. `CANNOT_ISOLATE` (no established observation; the row\'s `reason` is one of no-execution-evidence | unrecognised-runner-output | no-tests-executed | inconsistent-evidence | tip-control-missing | tip-evidence-missing | timeout) \u2192 HIGH gap NAMING the reason; action per `primaryAction` (map-a-test or fix-runner-invocation). `measurementValid` false or \'partial\' means the instrument did not (fully) measure \u2014 report it verbatim; never convert a degraded reading into a pass. Record every verdict + reason in the 08_qe_report.md ADR Fitness section. If `dz discrimination-check` is unavailable, note it and continue \u2014 never block on the tool.'
1975
+ const DISCRIMINATION_GATE = '\u00a742 TEST-DISCRIMINATION GATE (run right after asserting the property has a test): the ADR Confirmation names `Required automated check: <test file>` for the load-bearing property. Prove that test DISCRIMINATES \u2014 via Bash run EXACTLY `' + DZ + ' discrimination-check --test <that test file> --base HEAD --json` (the PINNED workspace bin, never bare `dz` — the global install measurably lags the workspace) (the Step-7 feature diff is UNCOMMITTED, so HEAD is the pre-feature base). Parse the JSON: read `perTest[]` (each row carries verdict + reason), `findings[]` (ALL entries, not only the first), `measurementValid`, and `primaryAction` \u2014 the singular `finding` is a DEPRECATED alias; do not consume it. The SEVEN verdicts and the required QE action for each: `DISCRIMINATES` (assertion-red at base, execution-evidenced) = PASS. `DISCRIMINATES_VIA_ERROR` (evidenced load-error at base + evidenced pass at tip) = PASS \u2014 note the inference. `NON_DISCRIMINATING` (evidenced pass at base \u2014 a proven false green) \u2192 HIGH gap "property test does not discriminate: <file>"; advisory, not an automatic blocker. `TEST_FILE_ABSENT` (the named test is not a regular file) \u2192 HIGH gap; action create-missing-test; NEVER a pass. `LOAD_ERROR_AT_BOTH_REVS` (the instrument could not execute the test at either rev \u2014 zero signal) \u2192 HIGH gap; action fix-runner-invocation. `FAILS_AT_TIP` (the feature\'s own test is red WITH the feature present) \u2192 HIGH gap; action fix-red-feature-test \u2014 grade the feature code accordingly. `CANNOT_ISOLATE` (no established observation; the row\'s `reason` is one of no-execution-evidence | unrecognised-runner-output | no-tests-executed | inconsistent-evidence | tip-control-missing | tip-evidence-missing | timeout) \u2192 HIGH gap NAMING the reason; action per `primaryAction` (map-a-test or fix-runner-invocation). `measurementValid` false or \'partial\' means the instrument did not (fully) measure \u2014 report it verbatim; never convert a degraded reading into a pass. Record every verdict + reason in the 08_qe_report.md ADR Fitness section. If `discrimination-check` is unavailable at the pinned path, errors, or overruns its window \u2192 record a HIGH gap `discrimination gate INCONCLUSIVE: <unavailable|error|timeout>` (backlog 52d0ed08: an instrument that could not run is never a pass and never applicable-by-silence). Still never abort the run.'
1906
1976
  // P2 (amendment-confirmation-discipline, fa-improvements 2026-07-18): amendments are where the SHARPEST design
1907
1977
  // corrections land (challenge-panel/QCSD) and were the least-tested — prose deltas with no proving test. Every
1908
1978
  // amendment is a mini-ADR: it carries a one-line Confirmation naming the test that falsifies it. Machine-checkable
1909
1979
  // shape (a linter can assert the `→ test ` token); Step-8 verifies existence + non-vacuity via the SAME
1910
1980
  // dz discrimination-check that guards the ADR property (cost-of-detection ladder: judgment → step gate).
1911
- const AMENDMENT_RULE = 'AMENDMENT CONFIRMATION DISCIPLINE (every amendment is a mini-ADR): whenever a correction/amendment is folded in (a QCSD CONDITIONAL condition, a challenge-panel confirmed finding, or a user checkpoint steer), record it in a `## Amendments` section as a fixed-shape row: `AM-N (source): <change>. Confirmation: <property> → test `test_name` (fails if reverted).` — naming the test that would FAIL if the amendment were reverted/broken. For a SAFEGUARD amendment (a warning/guard/fallback), the named test must prove the safeguard actually TRIGGERS on a real input — not merely that its code path exists (a structurally-dead safeguard passes an existence test and never fires in production).'
1981
+ const AMENDMENT_RULE = 'AMENDMENT CONFIRMATION DISCIPLINE (every amendment is a mini-ADR): whenever a correction/amendment is folded in (a QCSD CONDITIONAL condition, a challenge-panel confirmed finding, or a user checkpoint steer), record it in a `## Amendments` section as a fixed-shape row: `AM-N (source): <change>. Confirmation: <property> → test `test_name` (fails if reverted).` — naming the test that would FAIL if the amendment were reverted/broken. The row is MACHINE-READ by the K2 C6 gate: the marker (or a superseded-by-AM-N note) must sit inside the OWN block of that amendment — anywhere between its AM-N line and the line where the NEXT AM-N begins. Multi-line amendments are fine; what is NOT fine is placing a marker after the following amendment has already started, because it then belongs to that one. A bare range such as AM-1..AM-4 never opens a row. For a SAFEGUARD amendment (a warning/guard/fallback), the named test must prove the safeguard actually TRIGGERS on a real input — not merely that its code path exists (a structurally-dead safeguard passes an existence test and never fires in production).'
1912
1982
  const AMENDMENT_GATE = 'AMENDMENT GATE (P2): do NOT judge this yourself — RUN the check and report what it says. Via Bash run EXACTLY `' + DZ + ' amendment-check --slug ' + SLUG + ' --json` (add `--feature-dir ' + FDIR + '` if the slug does not resolve from your CWD). Parse the JSON and report `amendments: {outcome, counts, reasons}` in your return object. outcome `pass` or `skip` clears the gate; `fail` is a HIGH gap and every reason must be quoted verbatim into the QE report; `not-established` means the check could not be run or the grammar matched nothing — that is NEVER a pass, report it as inconclusive with the tool error. Empty stdout, a crash, or a missing `dz` is `not-established`, not a clean gate. This check proves each amendment RESOLVES to a real test; it does NOT prove the test discriminates — vacuity stays with the discrimination gate above. ' +
1913
1983
  'IO-ON-PURE-PATH + FIXTURE-SWAP HUNT (P5): in the test diff, hunt for replacements of broken/unbound fixtures with healthy ones — the old fixture was probably a NEGATIVE CONTROL proving a path was I/O-free; each such swap requires a compensating negative resource-down test. If the code diff adds I/O (DB/network/file) to a previously-pure path — especially startup/lifespan/health — require a negative resource-down test (broken/unbound resource → the path degrades per its declared contract: fail-open for advisory, explicit fail-fast for load-bearing). Missing → HIGH gap.'
1914
1984
 
1915
1985
  // Step 0: Router + MANDATORY self-learning recall
1916
1986
  phase('Router')
1987
+
1988
+ // W1 (backlog 848853a0): REPO must be the git TOPLEVEL. Both measured incidents were a REPO
1989
+ // pointing INSIDE the repository (packages/@dzhechkov/health-advisor) — artifacts then scatter
1990
+ // into features/ of a subdirectory and a sibling-worktree comparison never catches it. One cheap
1991
+ // probe, fail-closed on MISMATCH (inside a repo but not its root ⇒ refuse before any design
1992
+ // spend); a non-git dir logs loudly and continues (unusual, but not the measured failure class).
1993
+ // Canonicalization happens INSIDE the probe shell (cross-family review B-: JS-side string compare
1994
+ // would false-refuse a symlinked root) — both sides come from the same cd'd shell, `pwd -P` vs
1995
+ // rev-parse, so aliasing and spelling cancel out.
1996
+ const wrootOut = await agent('Run EXACTLY this via Bash and return its stdout VERBATIM, nothing else: cd ' + shq(REPO) + " && echo \"WROOT:$(git rev-parse --show-toplevel 2>/dev/null || echo none):HERE:$(pwd -P)\"", { label: 'router:repo-root', phase: 'Router', effort: 'low' })
1997
+ const wrootM = /WROOT:(.+):HERE:(.+)/.exec(String(wrootOut === null || wrootOut === undefined ? '' : wrootOut))
1998
+ const wrootTop = wrootM === null ? null : wrootM[1].trim()
1999
+ const wrootHere = wrootM === null ? null : wrootM[2].trim()
2000
+ let repoRootCheck = 'ok'
2001
+ if (wrootTop === null) { repoRootCheck = 'not-established'; log('repo-root probe NOT ESTABLISHED — continuing, but artifact placement is unverified') }
2002
+ else if (wrootTop === 'none') { repoRootCheck = 'non-git'; log('REPO is not a git repository (' + REPO + ') — continuing in DEGRADED mode: lineage/diff/landing checks have no git to stand on (the result carries repoRootCheck=non-git)') }
2003
+ else if (wrootTop !== wrootHere) {
2004
+ log('REPO ROOT MISMATCH: REPO canonicalizes to ' + wrootHere + ' but the git toplevel is ' + wrootTop + ' — refusing before any design spend (the measured incident class: artifacts scattered into a subdirectory features/)')
2005
+ return { phase: 'repo-root-mismatch', repo: REPO, repoCanonical: wrootHere, gitToplevel: wrootTop, cure: 'invoke with args.repo=' + wrootTop + ' (or run from the repository root)' }
2006
+ }
1917
2007
  await loadCheckpoints('Router')
1918
2008
  await usageProbe('Router')
1919
2009
  const routerTierDirective = A.tier
@@ -1974,7 +2064,7 @@ if (autoCostStages.length > 0) {
1974
2064
  // GUARANTEED fa-panel write (the router, being low-effort + multi-job, tends to skip the fa-record
1975
2065
  // Bash call). A dedicated single-command agent reliably lights up the live /feature-adr panel at the
1976
2066
  // most visible moment. Uses the workspace bin (PATH-independent). Best-effort — never blocks.
1977
- if (resumedStages.indexOf('router') === -1) await agent('Run EXACTLY this one shell command via your Bash tool and report its stdout verbatim — do nothing else, do not summarize: ' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 0 recall" --recalled 3 --stored 0 --mode ' + MODE + ' --project ' + REPO, { label: 'fa-record:step0', phase: 'Router', effort: 'low' })
2067
+ if (resumedStages.indexOf('router') === -1) await agent('Run EXACTLY this one shell command via your Bash tool and report its stdout verbatim — do nothing else, do not summarize: ' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 0 recall" --recalled auto --run fa:' + SLUG + ' --count-project ' + BRAIN + ' --stored 0 --mode ' + MODE + ' --project ' + REPO, { label: 'fa-record:step0', phase: 'Router', effort: 'low' })
1978
2068
 
1979
2069
  // R1 product-architecture-lens (ADR-001 Decision 3): forward-looking сверка of THIS feature vs the LIVE
1980
2070
  // product map + vision. NON-BLOCKING/soft by design — it LOGS {signal,confidence} so a real command
@@ -2028,8 +2118,8 @@ designThunks.push(() => subCheckpoint('requirements', reqPromptText, reqOpts, ['
2028
2118
  // the requirements prompt is where the Step-0 recalled lessons are INJECTED — recorded per pair
2029
2119
  designPairMeta.push({ role: 'design:requirements', prompt: reqPromptText, model: modelLabel(reqOpts), lessons: (LEARNED && LEARNED !== 'none recalled') ? [String(LEARNED)] : [] })
2030
2120
  if (isMplus) {
2031
- const adrPromptText = 'Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. ' + ADR_TEMPLATE_GUIDE + ' Write ' + FDIR + '/03_adr/001-' + SLUG + '.md as a MADR-structured ADR that PASSES the Step-8 ADR fitness checklist (do NOT emit the legacy shape). Emit ALL of these sections, in order: a decision-shaped # Title (present-tense imperative verb the auto-filename tracks the feature slug, so the IMPERATIVE signal lives in the title); ## Status (proposed/accepted/rejected/deprecated/superseded + a reversibility/revisit clause); ## Context (neutral, problem-first, BEFORE the Decision); ## Decision Drivers (ranked/weighted D1, D2, …); ## Considered Options (frame the CHOSEN approach as one option ALONGSIDE the rejected ones, each with symmetric Pros:/Cons:); ## Decision (concrete/testable — exact names, versions, paths, commands); ## Rationale (map each point to a driver Dn + why the losers lost); ## Consequences (Positive + Negative/Accepted Downsides + Follow-up ADRs + After-action Review with owner + date); a REQUIRED ## Confirmation stanza with Method:, Monitoring:, Success metric:, Owner:, Load-bearing property:, and Required automated check: `<test file>` NAMING the load-bearing property that MUST have a Step-8 test (the recurring lesson: the key safety property is often the untested one); and a ## Links traceability block (requirements, driving use case, related ADRs). Add a one-line provenance note (model-generated, edited for clarity) and, for a long ADR, a top-of-file table of contents. Do NOT use an "Alternatives considered" or "Testability/shift-left" heading in place of Considered Options / Confirmation. When creating ADDITIONAL ADRs, name them 03_adr/NNN-{decision-slug}.md with a lowercase-kebab, present-tense imperative, dateless, ticketless slug. Return wrote[] + summary.' + WRITE_DISCIPLINE
2032
- designThunks.push(() => subCheckpoint('adr', adrPromptText, adrOpts, ['03_adr/001-' + SLUG + '.md'], () => designStage(adrPromptText, adrOpts, FDIR + '/03_adr/001-' + SLUG + '.md', 'adr')))
2121
+ const adrPromptText = 'Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. ' + ADR_TEMPLATE_GUIDE + ' Write ' + FDIR + '/03_adr/001-<imperative-decision-slug>.md the SLUG IS THE DECISION in lowercase-kebab present-tense imperative (e.g. 001-pin-the-trust-root.md), NEVER the feature name (three ADR-fitness FAILs on 2026-08-17 were exactly first-ADRs named after their feature) — as a MADR-structured ADR that PASSES the Step-8 ADR fitness checklist (do NOT emit the legacy shape). Emit ALL of these sections, in order: a decision-shaped # Title (present-tense imperative verb, matching the imperative filename slug); ## Status (proposed/accepted/rejected/deprecated/superseded + a reversibility/revisit clause); ## Context (neutral, problem-first, BEFORE the Decision); ## Decision Drivers (ranked/weighted D1, D2, …); ## Considered Options (frame the CHOSEN approach as one option ALONGSIDE the rejected ones, each with symmetric Pros:/Cons:); ## Decision (concrete/testable — exact names, versions, paths, commands); ## Rationale (map each point to a driver Dn + why the losers lost); ## Consequences (Positive + Negative/Accepted Downsides + Follow-up ADRs + After-action Review with owner + date); a REQUIRED ## Confirmation stanza with Method:, Monitoring:, Success metric:, Owner:, Load-bearing property:, and Required automated check: `<test file>` NAMING the load-bearing property that MUST have a Step-8 test (the recurring lesson: the key safety property is often the untested one); and a ## Links traceability block (requirements, driving use case, related ADRs). Add a one-line provenance note (model-generated, edited for clarity) and, for a long ADR, a top-of-file table of contents. Do NOT use an "Alternatives considered" or "Testability/shift-left" heading in place of Considered Options / Confirmation. When creating ADDITIONAL ADRs, name them 03_adr/NNN-{decision-slug}.md with a lowercase-kebab, present-tense imperative, dateless, ticketless slug. Return wrote[] + summary.' + WRITE_DISCIPLINE
2122
+ designThunks.push(() => subCheckpoint('adr', adrPromptText, adrOpts, ['03_adr/'], () => designStage(adrPromptText, adrOpts, FDIR + '/03_adr/', 'adr')))
2033
2123
  designPairMeta.push({ role: 'design:adr', prompt: adrPromptText, model: modelLabel(adrOpts), lessons: [] })
2034
2124
  const qcsdPromptText = 'Step 3.5 (QCSD ideation swarm - HTSM quality criteria + SFDIPOT risk) of /feature-adr for "' + DESC + '" (' + SLUG + '). Assess quality criteria + product-factors risk. Write ' + FDIR + '/03.5_ideation_report.md with a GO/CONDITIONAL/NO-GO verdict + top quality risks for QE. On a CONDITIONAL verdict, write each condition as an amendment row in a `## Amendments` section. ' + AMENDMENT_RULE + ' Return wrote[] + summary.' + WRITE_DISCIPLINE
2035
2125
  designThunks.push(() => subCheckpoint('qcsd', qcsdPromptText, qcsdOpts, ['03.5_ideation_report.md'], () => designStage(qcsdPromptText, qcsdOpts, FDIR + '/03.5_ideation_report.md', 'qcsd')))
@@ -2047,7 +2137,7 @@ if (isMplus) {
2047
2137
  // (Codex QE #2 — a one-file probe accepted a design missing its ADR/architecture).
2048
2138
  const designHash = ckptHash('design', [tier, DESC, CODE_HINT, fnv1a64(String(LEARNED)), MODELS.requirements === undefined ? null : MODELS.requirements, MODELS.adr === undefined ? null : MODELS.adr, MODELS.ideation === undefined ? null : MODELS.ideation, MODELS.architecture === undefined ? null : MODELS.architecture, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, CODEX_MODEL])
2049
2139
  const designArtifacts = ['01_requirements.md']
2050
- if (isMplus) designArtifacts.push('03_adr/001-' + SLUG + '.md', '03.5_ideation_report.md', '05_architecture.md')
2140
+ if (isMplus) designArtifacts.push('03_adr/', '03.5_ideation_report.md', '05_architecture.md')
2051
2141
  if (isLplus) designArtifacts.push('02_research.md', '04_domain_model.md')
2052
2142
  /**
2053
2143
  * PER-SIBLING checkpoint (SP-2, no-amplification). Before 2026-08-20 the whole fan shared ONE
@@ -2124,7 +2214,7 @@ const designFan = await parallel(designThunks)
2124
2214
  // line, so it can no longer forge one — and `-f` on an exact path cannot be satisfied by a name that
2125
2215
  // merely renders like it. MEASURED red/green below in a real shell against a newline-bearing filename.
2126
2216
  let fanChecks = ''
2127
- for (const rel of designArtifacts) fanChecks += '[ -f ' + shq(rel) + ' ] && echo ' + shq('HAVE:' + rel) + '; '
2217
+ for (const rel of designArtifacts) fanChecks += (rel.endsWith('/') ? 'ls ' + shq(rel) + ' 2>/dev/null | grep -q "^001-.*\\.md$" && echo ' + shq('HAVE:' + rel) + '; ' : '[ -f ' + shq(rel) + ' ] && echo ' + shq('HAVE:' + rel) + '; ')
2128
2218
  const fanLsCmd = 'cd ' + shq(FDIR) + ' 2>/dev/null && { ' + fanChecks + 'echo ' + shq(FAN_LS_SENTINEL) + '; } || true'
2129
2219
  const fanLsOut = await agent('Run EXACTLY this via Bash and return its stdout VERBATIM (it may be empty) with NO code fences and NO commentary: ' + fanLsCmd, { label: 'design:artifact-probe', phase: 'Design', effort: 'low' })
2130
2220
  // The transcript is validated STRICTLY, not scanned. Round 7 measured the difference: an agent that
@@ -2266,7 +2356,7 @@ async function runChallengePanel(planRel, plannerName) {
2266
2356
  if (!authorIsCodex) {
2267
2357
  // author=Claude → Codex adversary (cross-family). Compact prompt: Codex reads the files itself (no 24k
2268
2358
  // brief inlined). safeCodexAgent is the honest exec path; null/invalid ⇒ loud Claude fallback below.
2269
- const cx = await safeCodexAgent('You are a FRESH adversarial reviewer of an implementation plan you did NOT write. Read these files: ' + planRel + ' , architecture/vision.md , architecture/degradations.md (relative to repo ' + REPO + '). BREAK the plan, do not confirm it. Answer C1 arch-anti-cement (deviating from a pattern in the degradations registry is NOT a finding), C2 prod-ready, C3 test sufficiency+honesty both ways, C4 overengineering, C5 silent decisions, C6 runtime consistency, C7 scope>1.5x, C8 executability. Output ONLY minified JSON {"findings":[{"c","severity":"P0|P1|P2","title","why","where"}],"summary"}.', { label: 'challenge:codex-adversary', phase: 'Plan' })
2359
+ const cx = await safeCodexAgent('You are a FRESH adversarial reviewer of an implementation plan you did NOT write. Read these files: ' + planRel + ' , architecture/vision.md , architecture/testing.md , architecture/map.json , architecture/degradations.md (relative to repo ' + REPO + '). BREAK the plan, do not confirm it. Answer C1 arch-anti-cement (deviating from a pattern in the degradations registry is NOT a finding), C2 prod-ready, C3 test sufficiency+honesty both ways, C4 overengineering, C5 silent decisions, C6 runtime consistency, C7 scope>1.5x, C8 executability. Output ONLY minified JSON {"findings":[{"c","severity":"P0|P1|P2","title","why","where"}],"summary"}.', { label: 'challenge:codex-adversary', phase: 'Plan' })
2270
2360
  if (cx) { try { verdict = sanitizeChallengeVerdict(JSON.parse(String(cx).replace(/^[^{]*/, '').replace(/[^}]*$/, ''))) } catch { verdict = null } }
2271
2361
  if (!verdict) { log('Challenge panel: Codex adversary unavailable/unparseable/invalid — falling back to a FRESH Claude panel (NOT cross-family; run `dz challenge` + codex manually for a cross-family pass)'); adversary = 'claude-fallback' }
2272
2362
  }
@@ -2326,7 +2416,7 @@ if (plan) {
2326
2416
  // plan straight into Step 7. The gate is forced NOT-ESTABLISHED without probing the tree at all.
2327
2417
  let planGate = { verdict: 'not-established', exit: null, reason: 'plan-stage-null', output: 'The Step-6 plan stage returned no result for THIS run (agent died, or produced nothing). Any 06_implementation_plan.md present on disk belongs to an earlier run and cannot vouch for this one, so the gate refuses without reading it.' }
2328
2418
  if (plan) {
2329
- const planGateOut = await agent('Run EXACTLY this shell snippet via your Bash tool, as ONE command, and return its stdout VERBATIM, nothing else — do not summarize it, do not judge the plan yourself, do not omit the K2_GATE_SCRIPT / K2_GATE_TRIED lines or the trailing K2_EXIT line:\n' + planCompletenessGateCmd(REPO, 'features/' + SLUG, tier, { gateScript: GATE_SCRIPT_ARG }), { label: 'plan:k2-gate', phase: 'Plan', effort: 'low' })
2419
+ const planGateOut = await agent('Run EXACTLY this shell snippet via your Bash tool, as ONE command, and return its stdout VERBATIM, nothing else — do not summarize it, do not judge the plan yourself, do not omit the K2_GATE_SCRIPT / K2_GATE_TRIED lines or the trailing K2_EXIT line:\n' + planCompletenessGateCmd(REPO, 'features/' + SLUG, tier, { gateScript: GATE_SCRIPT_ARG, workspace: WS === null ? undefined : WS }), { label: 'plan:k2-gate', phase: 'Plan', effort: 'low' })
2330
2420
  planGate = parsePlanGateVerdict(planGateOut)
2331
2421
  }
2332
2422
  log('K2 plan-completeness gate: ' + planGate.verdict + ' (exit=' + (planGate.exit === null ? 'unknown' : planGate.exit) + ', reason=' + planGate.reason + ')')
@@ -2351,6 +2441,24 @@ if (stopHere) {
2351
2441
  let challengeVerdict = null
2352
2442
  try { challengeVerdict = plan ? await runChallengePanel('features/' + SLUG + '/06_implementation_plan.md', plan.planner) : null }
2353
2443
  catch (e) { log('Challenge panel errored (advisory, ignored): ' + (e && e.message ? e.message : String(e))) }
2444
+ // Seam а (backlog 72b89e14): the panel's verdict used to reach only the OPERATOR — the
2445
+ // finding→plan-amendment bridge was manual, and on L/XL the coder runs in a SECOND invocation
2446
+ // that reads the PLAN FILE, not the first invocation's memory. So P0/P1 findings are appended to
2447
+ // the plan's ## Amendments as AM-CP-<n> rows by an effort-low agent — append-only, idempotent
2448
+ // (the marker line is checked first), each row carrying the C6-required shape.
2449
+ try {
2450
+ const cpFindings = (challengeVerdict && Array.isArray(challengeVerdict.findings))
2451
+ ? challengeVerdict.findings.filter((f) => f && (f.severity === 'P0' || f.severity === 'P1'))
2452
+ : []
2453
+ if (cpFindings.length > 0) {
2454
+ const rows = cpFindings.map((f, i) => '- AM-CP-' + (i + 1) + ' [' + f.severity + '] ' + String(f.title || '').replace(/[\r\n`]/g, ' ').slice(0, 160) + ' \u2192 test `названный кодером при реализации — заменить на имя реального теста` (panel ' + String(f.c || '') + ')').join('\n')
2455
+ const marker = '<!-- challenge-panel amendments appended ' + fnv1a64(rows) + ' -->'
2456
+ const planPath = FDIR + '/06_implementation_plan.md'
2457
+ const appendCmd = 'cd ' + shq(REPO) + ' && grep -qF ' + shq(marker) + ' ' + shq(planPath) + ' && echo CP-DUP || { grep -q "^## Amendments" ' + shq(planPath) + ' || printf "\n## Amendments\n" >> ' + shq(planPath) + '; printf "%s\n%s\n" ' + shq(marker) + ' ' + shq(rows) + ' >> ' + shq(planPath) + '; echo CP-APPENDED; }'
2458
+ const cpOut = await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + appendCmd, { label: 'challenge:append-amendments', phase: 'Plan', effort: 'low' })
2459
+ log('challenge panel \u2192 plan amendments: ' + (/CP-APPENDED/.test(String(cpOut || '')) ? cpFindings.length + ' AM-CP row(s) appended' : /CP-DUP/.test(String(cpOut || '')) ? 'already appended (idempotent)' : 'NOT appended (probe answered: ' + String(cpOut || '').slice(0, 80) + ')'))
2460
+ }
2461
+ } catch (e2) { log('challenge panel \u2192 amendments append failed (advisory): ' + (e2 && e2.message ? e2.message : String(e2))) }
2354
2462
  // P4 (checkpoint-gate-line): a DERIVED gates map — each entry comes from machine state (artifact/verdict
2355
2463
  // presence), never from prose, so a skipped gate shows as 'not-run' instead of being silently forgotten.
2356
2464
  const planGates = { plan: (plan ? 'produced' : 'missing'), planCompleteness: planGate.verdict, challengePanel: (challengeVerdict ? 'ran' : 'not-run'), code: 'not-run', qe: 'not-run' }
@@ -2377,14 +2485,21 @@ if (QE_SCOPE === 'uncommitted') {
2377
2485
  const baseCmd = changeSetProbeCmd({ scope: 'uncommitted', paths: preCodeTargets, quote: shq })
2378
2486
  if (baseCmd) {
2379
2487
  const baseOut = await agent('Run EXACTLY this via Bash from ' + REPO + ' and return its stdout VERBATIM with NO commentary: cd ' + shq(REPO) + ' && ' + baseCmd, { label: 'qe:baseline-hash', phase: 'Code', effort: 'low' })
2380
- if (baseOut !== null && baseOut !== undefined) preCodeBaseline = parseHashProbe(String(baseOut), preCodeTargets)
2488
+ // An EMPTY relay reply is not a measurement. parseHashProbe seeds every declared path with null
2489
+ // and returns a valid-looking snapshot, so a failed baseline would later compare null -> hash for
2490
+ // every target and hand Mode B a change set of files Step 7 never touched (cross-family review of
2491
+ // this restoration, P1). A probe that produced no parsable hash line leaves the baseline NULL —
2492
+ // the established not-measured signal that the log below already names and that changedFromHashes
2493
+ // turns into "scope NOT ESTABLISHED", which is never a pass.
2494
+ const baseText = (baseOut === null || baseOut === undefined) ? '' : String(baseOut)
2495
+ if (/^[0-9a-f]{64}\s+\S/m.test(baseText)) preCodeBaseline = parseHashProbe(baseText, preCodeTargets)
2381
2496
  }
2382
2497
  if (preCodeBaseline === null) log('QE: pre-code baseline NOT captured — the change set will read as unmeasured, and a scoped review will refuse rather than guess')
2383
2498
  }
2384
2499
 
2385
2500
  phase('Code')
2386
2501
  await usageProbe('Code')
2387
- const codePrompt = 'Step 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG + '). Implement the feature per the plan + ADR + architecture in ' + FDIR + '. Write the ACTUAL production code + its tests (mirror the closest existing implementation named in research/architecture). If the plan carries a `## Amendments` section, implement every AM-N row AND its named Confirmation test (for a safeguard amendment: a test proving it FIRES on a real input). IO-ON-PURE-PATH RULE: if your diff adds I/O (DB/network/file) to a previously-pure path — especially a startup/lifespan/health path — also write a NEGATIVE resource-down test (broken/unbound resource handle → the path degrades per its declared contract: fail-open for an advisory feature, explicit fail-fast for a load-bearing one) alongside the happy-path test; never fix a failing test by swapping a broken fixture for a healthy one without keeping BOTH cases. Follow repo conventions; build must pass. Write a change manifest ' + FDIR + '/07_code_changes/change_manifest.md listing every file touched. Return wrote[] (incl. real source files) + summary.' + ABSOLUTE_PATH_NOTE + PS_GUIDANCE('code')
2502
+ const codePrompt = 'Step 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ THESE INPUTS FIRST, by name (0691e163: the coder used to get one directory pointer; measured over three real runs, the plan was opened by all coders but the ADR unevenly and requirements/domain model not at all): ' + FDIR + '/06_implementation_plan.md (the tasks + EXPECTED_CODE_TARGETS + Amendments), every ' + FDIR + '/03_adr/NNN-*.md (each names a load-bearing property and its Required automated check), ' + FDIR + '/05_architecture.md, ' + FDIR + '/01_requirements.md, and ' + FDIR + '/04_domain_model.md when present (L/XL). Then implement the feature. Write the ACTUAL production code + its tests (mirror the closest existing implementation named in research/architecture). If the plan carries a `## Amendments` section, implement every AM-N row AND its named Confirmation test (for a safeguard amendment: a test proving it FIRES on a real input). IO-ON-PURE-PATH RULE: if your diff adds I/O (DB/network/file) to a previously-pure path — especially a startup/lifespan/health path — also write a NEGATIVE resource-down test (broken/unbound resource handle → the path degrades per its declared contract: fail-open for an advisory feature, explicit fail-fast for a load-bearing one) alongside the happy-path test; never fix a failing test by swapping a broken fixture for a healthy one without keeping BOTH cases. Follow repo conventions; build must pass. Write a change manifest ' + FDIR + '/07_code_changes/change_manifest.md listing every file touched. Return wrote[] (incl. real source files) + summary.' + ABSOLUTE_PATH_NOTE + PS_GUIDANCE('code')
2388
2503
  // Resolve the coder model. args.models.code wins (a direct 'codex' spec = codex-first); else the legacy
2389
2504
  // CODER knob drives it (with its codex-fallback null-guard). resolveStageModel('code') folds both via the
2390
2505
  // code:null sentinel → resolveCoderSpec(). A Claude resolution merges {model} onto the Claude branch;
@@ -2475,7 +2590,7 @@ if (needsCodeLandedBarrier(coderUsed)) {
2475
2590
  const targetsLine = 'Expected code targets (' + (sourcing.establishedBy === null ? 'NONE ESTABLISHED, reason=' + sourcing.reason : 'established by ' + sourcing.establishedBy) + '): ' + (barrierPlan.expectedPaths.length ? barrierPlan.expectedPaths.join(', ') : '(none)')
2476
2591
  const scrapeLine = scrapeDiagnostic.length ? '\ncodex-self-declared (diagnostic, not matched): ' + scrapeDiagnostic.join(', ') : ''
2477
2592
  if (landingStatus === 'landed') {
2478
- landedNote = '\n\nCODEX-CODED (out-of-band): Step 7.5 landing barrier verdict LANDED (mode=' + barrierPlan.mode + ', window=' + barrierPlan.pollWindowSeconds + 's). Read the listed files and do NOT report "Step 7 never ran".\n' + targetsLine + scrapeLine + '\n' + probeText
2593
+ landedNote = '\n\nCODEX-CODED (out-of-band): Step 7.5 landing barrier verdict LANDED (mode=' + barrierPlan.mode + ', window=' + barrierPlan.pollWindowSeconds + 's). Treat the verdict as EVIDENCE, not authority: read the listed files and CHECK the declared-target diff yourself if it is empty, SAY SO and grade accordingly (a barrier can be wrong; your own reading outranks it).\n' + targetsLine + scrapeLine + '\n' + probeText
2479
2594
  } else if (landingStatus === 'genuinely-not-landed') {
2480
2595
  landedNote = '\n\nCODEX-CODED (out-of-band): Step 7.5 landing barrier verdict GENUINELY-NOT-LANDED — the bounded ' + barrierPlan.pollWindowSeconds + 's window found no newly-changed expected target.\n' + targetsLine + scrapeLine + '\n' + probeText
2481
2596
  } else {
@@ -2504,8 +2619,48 @@ if (resumedStages.indexOf('code') !== -1 && landedNote !== '') {
2504
2619
 
2505
2620
  // Step 8: QE (brutal-honesty, agentic-qe) + MANDATORY teach
2506
2621
  phase('QE')
2622
+
2623
+ // ── Writer-quiescence probe (feature qe-writer-quiescence, backlog 700b46a4) ─────────────────────
2624
+ // Step-8 used to grade a MOVING tree (crossrt-1: a background worker wrote AFTER the verdict,
2625
+ // clobbering a file the round had just written). A BELT, not the root (worktree isolation is
2626
+ // 9520e506): the probe NEVER blocks — a moving/inconclusive tree loudly downgrades the verdict's
2627
+ // standing. Inline byte-mirror of harness-core/src/writer-quiescence.ts:decideWriterQuiescence;
2628
+ // the drift test asserts this body matches the module.
2629
+ function decideWriterQuiescence(probeText, requiredQuiet) {
2630
+ const need = requiredQuiet === undefined ? 3 : requiredQuiet
2631
+ const text = probeText === null || probeText === undefined ? '' : String(probeText)
2632
+ const windows = []
2633
+ for (const line of text.split(/\r?\n/)) {
2634
+ const m = /WQ-WINDOW\s+\d+\s+changed=(\d+|ERR)/.exec(line)
2635
+ if (m) windows.push(m[1] === 'ERR' ? -1 : Number(m[1]))
2636
+ }
2637
+ if (windows.length === 0) {
2638
+ return { verdict: 'inconclusive', windows: windows, note: 'quiescence probe returned no windows — grading standing NOT established (probe failure is never quiet)' }
2639
+ }
2640
+ let streak = 0
2641
+ for (const c of windows) {
2642
+ streak = c === 0 ? streak + 1 : 0
2643
+ if (streak >= need) {
2644
+ return { verdict: 'quiet', windows: windows, note: 'no observed writes in ' + need + ' consecutive windows (mtime evidence only — not a writer-lifecycle guarantee)' }
2645
+ }
2646
+ }
2647
+ if (windows.some((c) => c < 0)) {
2648
+ return { verdict: 'inconclusive', windows: windows, note: 'quiescence probe could not observe every window (find errored) — grading standing NOT established' }
2649
+ }
2650
+ return { verdict: 'moving', windows: windows, note: 'tree is MOVING: no ' + need + ' consecutive quiet windows within budget (per-window changed counts: ' + windows.join(',') + ') — the verdict below was graded on a moving tree and must say so' }
2651
+ }
2652
+
2653
+ const wqPaths = [FDIR].concat(Array.isArray(expectedTargets) ? expectedTargets : []).filter((p) => typeof p === 'string' && p !== '' && p.indexOf("'") < 0 && p.charAt(0) !== '-')
2654
+ const wqTargets = wqPaths.map((p) => "'" + p + "'").join(' ')
2655
+ const wqScript = 'cd ' + shq(REPO) + ' && quiet=0; n=0; while [ $n -lt 9 ]; do n=$((n+1)); sleep 20; out=$(find ' + wqTargets + " -type f -newermt '-25 seconds' 2>&1 >/tmp/wq-list.$$); st=$?; if [ $st -ne 0 ] || [ -n \"$out\" ]; then c=ERR; else c=$(wc -l < /tmp/wq-list.$$); fi; rm -f /tmp/wq-list.$$; echo \"WQ-WINDOW $n changed=$c\"; if [ \"$c\" = \"0\" ]; then quiet=$((quiet+1)); if [ $quiet -ge 3 ]; then echo \"WQ-DONE quiet\"; exit 0; fi; else quiet=0; fi; done; echo \"WQ-DONE budget\""
2656
+ const wqProbe = await agent('Run EXACTLY this via Bash and return its stdout VERBATIM with NO commentary (it takes ~1-3 minutes of sleeping; that is the point): ' + wqScript, { label: 'qe:writer-quiescence', phase: 'QE', effort: 'low' })
2657
+ const writerQuiescence = decideWriterQuiescence(wqProbe)
2658
+ log('Step 8 writer-quiescence: ' + writerQuiescence.verdict + ' (windows: ' + (writerQuiescence.windows.join(',') || 'none') + ')')
2659
+ const wqNote = writerQuiescence.verdict === 'quiet'
2660
+ ? ' WRITER-QUIESCENCE: quiet (' + writerQuiescence.note + ').'
2661
+ : ' WRITER-QUIESCENCE GATE (MANDATORY to acknowledge): ' + writerQuiescence.note + ' State this standing explicitly in 08_qe_report.md next to the grade.'
2507
2662
  await usageProbe('QE')
2508
- const qePrompt = 'Step 8 (QE - brutal-honesty review, agentic-qe) of /feature-adr for "' + DESC + '" (' + SLUG + '). Adversarially review the SHIPPED code (read it): correctness, edge cases, error handling, and the LOAD-BEARING property the ADR named (ASSERT it has a test that DISCRIMINATES - the recurring lesson: a test that would still pass with the protection deleted is documentation, not a gate). Run this ADR gate before final grading: ' + ADR_FITNESS_CHECKLIST + ' ' + DISCRIMINATION_GATE + ' ' + MUTATION_GATE + ' ' + NO_STUBS_GATE + ' ' + AMENDMENT_GATE + ' Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md with an ADR Fitness Checklist section showing PASS/FAIL per ADR and evidence for the Confirmation-linked test. MANDATORY SELF-LEARNING STORE (close the loop, never skip): compare every candidate lesson against the Step-0 recalled LEARNED patterns above. Teach ONLY lessons NOT covered by Step-0 recall. On overlap, run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate; if --reinforce is unavailable, skip the duplicate teach and report `reinforced existing pattern <id>` in the QE report. Store every genuinely new lesson in the CANONICAL BRAIN store at `' + BRAIN + '` so it is NOT lost to a target repo you may have cd`d into. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal) — the `cd ' + BRAIN + ' &&` prefix + `--project ' + BRAIN + '` pin guarantee the lesson lands in the brain regardless of your CWD. Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled 3 --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. AUTHORING-TIME CLAIM-CHECK (Deliverable of claim-check-authoring-time): after writing ' + FDIR + '/08_qe_report.md, run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash, parse the {ok, findings, scanned} JSON, and report claimCheck: {findings: N, high: N, medium: N} (counts by severity) in your return object. TAG EVERY QUANTITATIVE CLAIM you write in the report using the convention the checker recognizes as honest — write "1131 tests pass (MEASURED — `npx vitest run`)", never a bare "1131 tests pass" — and where you QUOTE a forbidden phrase as an example (e.g. the retracted "100% passing" framing), backtick the literal so it reads as code, not an assertion, so your own compliant report scans clean. Return {grade, gaps, codeTestsAdequate, docTestsPresent, claimCheck}.' + ABSOLUTE_PATH_NOTE + landedNote + PS_GUIDANCE('qe')
2663
+ const qePrompt = 'Step 8 (QE - brutal-honesty review, agentic-qe) of /feature-adr for "' + DESC + '" (' + SLUG + '). Adversarially review the SHIPPED code (read it): correctness, edge cases, error handling, and the LOAD-BEARING property the ADR named (ASSERT it has a test that DISCRIMINATES - the recurring lesson: a test that would still pass with the protection deleted is documentation, not a gate). Run this ADR gate before final grading: ' + ADR_FITNESS_CHECKLIST + ' ' + DISCRIMINATION_GATE + ' ' + MUTATION_GATE + ' ' + NO_STUBS_GATE + ' ' + AMENDMENT_GATE + ' Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md with an ADR Fitness Checklist section showing PASS/FAIL per ADR and evidence for the Confirmation-linked test. MANDATORY SELF-LEARNING STORE (close the loop, never skip): compare every candidate lesson against the Step-0 recalled LEARNED patterns above. Teach ONLY lessons NOT covered by Step-0 recall. On overlap, run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate; if --reinforce is unavailable, skip the duplicate teach and report `reinforced existing pattern <id>` in the QE report. Store every genuinely new lesson in the CANONICAL BRAIN store at `' + BRAIN + '` so it is NOT lost to a target repo you may have cd`d into. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal) — the `cd ' + BRAIN + ' &&` prefix + `--project ' + BRAIN + '` pin guarantee the lesson lands in the brain regardless of your CWD. Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled auto --run fa:' + SLUG + ' --count-project ' + BRAIN + ' --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. AUTHORING-TIME CLAIM-CHECK (Deliverable of claim-check-authoring-time): after writing ' + FDIR + '/08_qe_report.md, run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash, parse the {ok, findings, scanned} JSON, and report claimCheck: {findings: N, high: N, medium: N} (counts by severity) in your return object. TAG EVERY QUANTITATIVE CLAIM you write in the report using the convention the checker recognizes as honest — write "1131 tests pass (MEASURED — `npx vitest run`)", never a bare "1131 tests pass" — and where you QUOTE a forbidden phrase as an example (e.g. the retracted "100% passing" framing), backtick the literal so it reads as code, not an assertion, so your own compliant report scans clean. Return {grade, gaps, codeTestsAdequate, docTestsPresent, claimCheck}.' + ABSOLUTE_PATH_NOTE + landedNote + wqNote + PS_GUIDANCE('qe')
2509
2664
  // CROSS-MODEL QE (load-bearing): resolveStageModel('qe') derives the OTHER family than the resolved
2510
2665
  // coder when args.models.qe is unset (coder-codex ⇒ opus; coder-Claude ⇒ codex, or opus if codex absent).
2511
2666
  // An explicit args.models.qe wins. A Claude qe spec is merged onto the qe-code-reviewer base (role
@@ -2646,7 +2801,7 @@ if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
2646
2801
  // with no 08_qe_report.md at all. Named by cross-family review of b6973199. The verdict itself
2647
2802
  // is real (Codex produced it), so a failed transcription DEGRADES the run rather than voiding
2648
2803
  // it — but it must be visible, and it must never read as a clean QE.
2649
- const scribePrompt = 'Step 8 (QE) of /feature-adr for "' + DESC + '" (' + SLUG + '). The independent cross-family review has ALREADY BEEN DONE, by Codex. You are the SCRIBE, not the reviewer: RECORD it, do NOT re-grade it, do NOT soften it, do NOT add a verdict of your own, and do NOT mark anything resolved that the reviewer flagged. The grade is ' + codexQe.grade + ' and it is FINAL.\n\nWrite ' + FDIR + '/08_qe_report.md with: (1) the grade ' + codexQe.grade + ' stated verbatim; (2) HOW it was obtained — dispatch mode ' + codexQe.mode + ', scope ' + codexQe.scopeRef + ', wall-clock ' + codexQe.elapsedSeconds + 's, gradeSource ' + codexQe.gradeSource + ' (a DERIVED grade means the reviewer could not be asked for a letter and it was computed from the severities it reported — say so plainly); (3) the reviewer text below, verbatim, as the findings section; (4) an ADR Fitness Checklist section with PASS/FAIL per ADR and the evidence pointer for the Confirmation-linked test.\n\nREVIEWER TEXT (verbatim, do not edit or summarise):\n' + String(codexQe.text) + '\n\nMANDATORY SELF-LEARNING STORE (close the loop, never skip): compare candidate lessons against the Step-0 recalled LEARNED patterns. Teach ONLY lessons NOT already covered; on overlap run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal). Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled 3 --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` verbatim via Bash. Finally run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash and TAG every quantitative claim you write the way the checker recognises as honest.' + ABSOLUTE_PATH_NOTE
2804
+ const scribePrompt = 'Step 8 (QE) of /feature-adr for "' + DESC + '" (' + SLUG + '). The independent cross-family review has ALREADY BEEN DONE, by Codex. You are the SCRIBE, not the reviewer: RECORD it, do NOT re-grade it, do NOT soften it, do NOT add a verdict of your own, and do NOT mark anything resolved that the reviewer flagged. The grade is ' + codexQe.grade + ' and it is FINAL.\n\nWrite ' + FDIR + '/08_qe_report.md with: (1) the grade ' + codexQe.grade + ' stated verbatim; (2) HOW it was obtained — dispatch mode ' + codexQe.mode + ', scope ' + codexQe.scopeRef + ', wall-clock ' + codexQe.elapsedSeconds + 's, gradeSource ' + codexQe.gradeSource + ' (a DERIVED grade means the reviewer could not be asked for a letter and it was computed from the severities it reported — say so plainly); (3) the reviewer text below, verbatim, as the findings section; (4) an ADR Fitness Checklist section with PASS/FAIL per ADR and the evidence pointer for the Confirmation-linked test.\n\nREVIEWER TEXT (verbatim, do not edit or summarise):\n' + String(codexQe.text) + '\n\nMANDATORY SELF-LEARNING STORE (close the loop, never skip): compare candidate lessons against the Step-0 recalled LEARNED patterns. Teach ONLY lessons NOT already covered; on overlap run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal). Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled auto --run fa:' + SLUG + ' --count-project ' + BRAIN + ' --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` verbatim via Bash. Finally run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash and TAG every quantitative claim you write the way the checker recognises as honest.' + ABSOLUTE_PATH_NOTE
2650
2805
  // WITNESS THE REWRITE, not the existence. On a re-QE or a resume with the same slug an OLD
2651
2806
  // 08_qe_report.md is already sitting there, and an existence probe reports that stale file as
2652
2807
  // landed — so a scribe that wrote nothing still marked the new verdict recorded, and the stage
@@ -2962,14 +3117,29 @@ if (DELIVERY_ON) {
2962
3117
  // L/XL checkpoint-after-plan return above — no code has landed there yet.)
2963
3118
  await agent('Run EXACTLY this one shell command via your Bash tool and report its stdout verbatim — do nothing else, do not summarize: cd ' + REPO + ' && ' + DZ + ' architecture --json > architecture/map.json && echo arch-map-updated', { label: 'arch-map:refresh', phase: (isLplus ? 'FleetQE' : 'QE'), effort: 'low' })
2964
3119
 
2965
- const tags = ['FEATURE_ADR_ROUTED', 'FEATURE_ADR_DESIGNED', 'FEATURE_ADR_PLANNED', 'FEATURE_ADR_IMPLEMENTED', 'FEATURE_ADR_VERIFIED']
2966
- if (isLplus) tags.push('FEATURE_ADR_FLEET_VERIFIED')
3120
+ // W4 (backlog 848853a0, the carrier defect): promise tags used to be STAMPED unconditionally —
3121
+ // two consecutive runs with an EMPTY Step 7 were tagged «implemented». A tag is now EARNED by its
3122
+ // stage's evidence; missing evidence emits <TAG>_INCOMPLETE (the promise-system convention the
3123
+ // reward rules already price at ≤0.3). The checkpoint/landing machinery guards RESUME; this guards
3124
+ // the CLAIM.
3125
+ function earnedTag(name, earned) { return earned ? name : name + '_INCOMPLETE' }
3126
+ const designEvidence = Array.isArray(design) && design.filter(Boolean).length > 0
3127
+ const implementedEvidence = code !== null && code !== undefined && (needsCodeLandedBarrier(coderUsed) ? landingStatus === 'landed' : true)
3128
+ const tags = [
3129
+ earnedTag('FEATURE_ADR_ROUTED', router !== null && router !== undefined),
3130
+ earnedTag('FEATURE_ADR_DESIGNED', designEvidence),
3131
+ earnedTag('FEATURE_ADR_PLANNED', plan !== null && plan !== undefined),
3132
+ earnedTag('FEATURE_ADR_IMPLEMENTED', implementedEvidence),
3133
+ earnedTag('FEATURE_ADR_VERIFIED', qe !== null && qe !== undefined && typeof qe.grade === 'string' && qe.grade !== ''),
3134
+ ]
3135
+ if (isLplus) tags.push(earnedTag('FEATURE_ADR_FLEET_VERIFIED', fleet !== null && fleet !== undefined))
2967
3136
  // QE-D#3/#4: the promise tag asserts the gate RAN AND its review doc LANDED — an errored gate or a missing
2968
3137
  // artifact must not claim it (a promise about a file that does not exist is exactly a fabricated completeness).
2969
3138
  if (delivery && delivery.artifact === 'written' && delivery.handoff !== 'errored') tags.push('FEATURE_ADR_DELIVERY_GATED')
2970
3139
  await appendRunCostRow('full', (isLplus ? 'FleetQE' : 'QE'))
2971
3140
  return {
2972
3141
  slug: SLUG, tier: tier, mode: MODE, artifactsDir: FDIR,
3142
+ repoRootCheck: repoRootCheck,
2973
3143
  design: design.filter(Boolean).map((d) => d.wrote).flat(),
2974
3144
  codeWrote: code ? code.wrote : [],
2975
3145
  qeGrade: qe ? qe.grade : null,