@dzhechkov/skills-feature-adr 1.5.2 → 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": "fe7f99695df5590ae1474395e3a2b26754a6bfd63695c5d8c804fb290b8eb6b1"
16
+ "sha256": "1e3c1a4ed97981b70a3a5099f6355556fc987e493bf478b82cabe552888d2583"
17
17
  },
18
18
  {
19
19
  "path": "bin/cli.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  {
27
27
  "path": "package.json",
28
- "sha256": "e3ce1d2e92b37dae085819111a7a107cf9b170a7602ceea381423873523ad1d2"
28
+ "sha256": "2224692687da750844ef25096d1fc41fc86d7e79743ac7a8f42bf51ba9018750"
29
29
  },
30
30
  {
31
31
  "path": "src/cli.js",
@@ -149,7 +149,7 @@
149
149
  },
150
150
  {
151
151
  "path": "templates/.claude/skills/feature-adr/modules/06-implementation-plan.md",
152
- "sha256": "3903e2694ad47782914b35869bbf19c5f0803b6dafdcfb629df343a5ff00d1b9"
152
+ "sha256": "b3efdfa1d44f4c51ff46551b5827066955c1f0bfee212d371ac1e2fb1c99980f"
153
153
  },
154
154
  {
155
155
  "path": "templates/.claude/skills/feature-adr/modules/07-code.md",
@@ -249,7 +249,7 @@
249
249
  },
250
250
  {
251
251
  "path": "templates/.claude/skills/feature-adr/scripts/check-plan-completeness.mjs",
252
- "sha256": "5cef77e68c58f163be64516013fb90866ef7201e415dc8888cc21a0f4d92b7d8"
252
+ "sha256": "eddf116620050e80b88497ace026fb8e0a12986f94f216181480c7daa0e08be4"
253
253
  },
254
254
  {
255
255
  "path": "templates/.claude/skills/frontend-design/LICENSE.txt",
@@ -313,7 +313,7 @@
313
313
  },
314
314
  {
315
315
  "path": "templates/.claude/workflows/feature-adr.js",
316
- "sha256": "2d895ee2b348745dce23b6beb6f932908beb932f2de309afe812ed3e1b96a7db"
316
+ "sha256": "a4e277040b42646d9bc1b06f78e32aa11f118a924f1626be67640f87ede8b292"
317
317
  },
318
318
  {
319
319
  "path": "templates/lib/memory-protocol.md",
@@ -325,5 +325,5 @@
325
325
  }
326
326
  ]
327
327
  },
328
- "signature": "RVit/JWzAfaMRgh3KVc200QVdpU0sNO6AcEkRGYt8c8fqq1TG01DKPXiGvV08hWRHH8/a/67+qOko8sYEgqEBw=="
328
+ "signature": "uUN7yuKgSOed/hE0xooh1K788NcAFhnvsh92AHcu4oHn/uMZurn0VnktfhsUDHUv3jR8F6bMoKgrb/F6dHrvCQ=="
329
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.2)
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.2",
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": "fe7f99695df5590ae1474395e3a2b26754a6bfd63695c5d8c804fb290b8eb6b1"
38
+ "content": "1e3c1a4ed97981b70a3a5099f6355556fc987e493bf478b82cabe552888d2583"
39
39
  }
40
40
  ]
41
41
  },
@@ -65,7 +65,7 @@
65
65
  "hashes": [
66
66
  {
67
67
  "alg": "SHA-256",
68
- "content": "e3ce1d2e92b37dae085819111a7a107cf9b170a7602ceea381423873523ad1d2"
68
+ "content": "2224692687da750844ef25096d1fc41fc86d7e79743ac7a8f42bf51ba9018750"
69
69
  }
70
70
  ]
71
71
  },
@@ -375,7 +375,7 @@
375
375
  "hashes": [
376
376
  {
377
377
  "alg": "SHA-256",
378
- "content": "3903e2694ad47782914b35869bbf19c5f0803b6dafdcfb629df343a5ff00d1b9"
378
+ "content": "b3efdfa1d44f4c51ff46551b5827066955c1f0bfee212d371ac1e2fb1c99980f"
379
379
  }
380
380
  ]
381
381
  },
@@ -625,7 +625,7 @@
625
625
  "hashes": [
626
626
  {
627
627
  "alg": "SHA-256",
628
- "content": "5cef77e68c58f163be64516013fb90866ef7201e415dc8888cc21a0f4d92b7d8"
628
+ "content": "eddf116620050e80b88497ace026fb8e0a12986f94f216181480c7daa0e08be4"
629
629
  }
630
630
  ]
631
631
  },
@@ -785,7 +785,7 @@
785
785
  "hashes": [
786
786
  {
787
787
  "alg": "SHA-256",
788
- "content": "2d895ee2b348745dce23b6beb6f932908beb932f2de309afe812ed3e1b96a7db"
788
+ "content": "a4e277040b42646d9bc1b06f78e32aa11f118a924f1626be67640f87ede8b292"
789
789
  }
790
790
  ]
791
791
  },
@@ -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
 
@@ -327,13 +327,36 @@ else for (const t of acidTokens) if (!new RegExp(`\\b${t.replace(/[.*+?^${}()|[\
327
327
  }
328
328
  }
329
329
  if (sectionStart >= 0) {
330
- for (const lnRaw of amSection.split('\n')) {
331
- const ln = lnRaw.trim();
332
- const am = /^(?:[-*|]\s*)?\*{0,2}(AM-(?:CP-)?\d+)\*{0,2}\b/.exec(ln);
333
- if (!am) continue;
334
- const hasTest = /\u2192\s*test\b|->\s*test\b/.test(ln) || /\u2192\s*test\b|->\s*test\b/.test(amSection.slice(amSection.indexOf(lnRaw)).split('\n').slice(0, 3).join('\n'));
335
- const superseded = /superseded by AM-(?:CP-)?\d+/i.test(ln);
336
- if (!hasTest && !superseded) failures.push(`C6: ${am[1]} 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`);
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`);
337
360
  }
338
361
  }
339
362
  }
@@ -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
@@ -1747,7 +1752,12 @@ function planCompletenessGateCmd(repo, featureDir, tier, opts) {
1747
1752
  // and the tried paths live OUTSIDE the verdict line so no path can smuggle a second verdict word
1748
1753
  // into it.
1749
1754
  'echo "K2_GATE_SCRIPT=${GS:-none}"',
1750
- '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"',
1751
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',
1752
1762
  ].join('\n')
1753
1763
  }
@@ -1834,6 +1844,57 @@ function decideModeBScope(o) {
1834
1844
  return { ok: true, files: files, dropped: dropped }
1835
1845
  }
1836
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
+
1837
1898
  function crossFamilyQe(o) {
1838
1899
  // NORMALISE first — raw-string comparison let 'Claude' vs 'claude' report a cross-family review
1839
1900
  // that never happened (caught by codex review --commit on the TS-only fix: the exported helper
@@ -1917,7 +1978,7 @@ const DISCRIMINATION_GATE = '\u00a742 TEST-DISCRIMINATION GATE (run right after
1917
1978
  // amendment is a mini-ADR: it carries a one-line Confirmation naming the test that falsifies it. Machine-checkable
1918
1979
  // shape (a linter can assert the `→ test ` token); Step-8 verifies existence + non-vacuity via the SAME
1919
1980
  // dz discrimination-check that guards the ADR property (cost-of-detection ladder: judgment → step gate).
1920
- 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).'
1921
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. ' +
1922
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.'
1923
1984
 
@@ -2295,7 +2356,7 @@ async function runChallengePanel(planRel, plannerName) {
2295
2356
  if (!authorIsCodex) {
2296
2357
  // author=Claude → Codex adversary (cross-family). Compact prompt: Codex reads the files itself (no 24k
2297
2358
  // brief inlined). safeCodexAgent is the honest exec path; null/invalid ⇒ loud Claude fallback below.
2298
- 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' })
2299
2360
  if (cx) { try { verdict = sanitizeChallengeVerdict(JSON.parse(String(cx).replace(/^[^{]*/, '').replace(/[^}]*$/, ''))) } catch { verdict = null } }
2300
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' }
2301
2362
  }
@@ -2355,7 +2416,7 @@ if (plan) {
2355
2416
  // plan straight into Step 7. The gate is forced NOT-ESTABLISHED without probing the tree at all.
2356
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.' }
2357
2418
  if (plan) {
2358
- 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' })
2359
2420
  planGate = parsePlanGateVerdict(planGateOut)
2360
2421
  }
2361
2422
  log('K2 plan-completeness gate: ' + planGate.verdict + ' (exit=' + (planGate.exit === null ? 'unknown' : planGate.exit) + ', reason=' + planGate.reason + ')')
@@ -2424,7 +2485,14 @@ if (QE_SCOPE === 'uncommitted') {
2424
2485
  const baseCmd = changeSetProbeCmd({ scope: 'uncommitted', paths: preCodeTargets, quote: shq })
2425
2486
  if (baseCmd) {
2426
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' })
2427
- 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)
2428
2496
  }
2429
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')
2430
2498
  }