@dzhechkov/skills-feature-adr 1.5.2 → 1.5.4

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": "eb5998619673f928f02b6b462278acfc4871387e1f6495ab0b613a965611cda7"
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": "8e082526415c4ce4427c51dff222c248e29ef5fbdcd17f0d957c28d621738513"
29
29
  },
30
30
  {
31
31
  "path": "src/cli.js",
@@ -145,11 +145,11 @@
145
145
  },
146
146
  {
147
147
  "path": "templates/.claude/skills/feature-adr/modules/05-architecture.md",
148
- "sha256": "2ef7cf316903f0cbb1fabe1f0e1770c22b2cb3a11a4118cac0529c25074b5e8a"
148
+ "sha256": "4d20dbc85ea5bb93ca90fd8cd089255bab9c76a912ca3ad954f3a4bb670f3c45"
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": "a6ff055d50f4eb5790dab36deee188e880eec1362188ec16a5504710294d87c3"
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": "4u6AmuuLsokLePBMiy6Gg9ETVwszE2NVE2fArT62nFcJSWPEd06QhKu4eb2/awzNjH4bhY3HXbacMZsS7B7gBA=="
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.4)
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,56 @@ 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.4` — **Step 5 now asks how the shipped feature will be watched.** The architecture artifact must
1117
+ carry a section headed exactly `Observability` answering what the feature logs, what it counts, what a
1118
+ failure looks like from outside, and who would notice. **"Nothing to observe" is a complete answer** —
1119
+ a pure refactor or a CI-only gate genuinely emits nothing, and a check that cannot express a true fact
1120
+ gets switched off. What is not acceptable is leaving the question unanswered. MEASURED before the
1121
+ change: the word appeared nowhere in the pipeline's prompts, and ZERO of 107 architecture artifacts
1122
+ carried such a section, while two fully-written observability skills (818 and 946 lines) sat
1123
+ unreachable because nothing called them. `dz score` reports the answer as a discipline — descriptive,
1124
+ never a gate, because the whole existing corpus predates the requirement.
1125
+
1126
+ Also in the bundled workflow: the K2 gate stops GUESSING the workspace. When it was not pinned, the
1127
+ workspace candidate was taken from the gate agent's own working directory, which against an external
1128
+ target repo resolved to that repo — so the candidate silently became a duplicate of the repo
1129
+ candidate and a skill installed in the workspace was never found (`NOT-ESTABLISHED`, exit 3, the
1130
+ coding step never ran). New `args.workspace` pins it, the shipped call site passes it, each candidate
1131
+ is labelled in the audit line, and a `K2_GATE_NOTE` fires when two collapse onto one path. And a
1132
+ pre-code probe that returns nothing no longer becomes an all-null baseline that later reads as
1133
+ "every target changed".
1134
+
1135
+
1136
+ `1.5.4` — **the workflow stops crashing on the way into Step 7.** `1.5.4` shipped a workflow that
1137
+ CALLED three helpers it never defined — `changeSetProbeCmd`, `parseHashProbe`, `changedFromHashes`
1138
+ (5 call sites, 0 definitions). `QE_SCOPE` defaults to `uncommitted`, so the guarded branch was true
1139
+ by default and every ultracode run that reached the coding step died with a `ReferenceError` while
1140
+ the corresponding unit tests stayed green — they exercise the exported module, the pipeline runs an
1141
+ inline mirror of it. The three are now restored FROM that canonical export, not reconstructed, and
1142
+ two guards make the class visible: the mirror is checked by lifting each function out of the shipped
1143
+ file and comparing its BEHAVIOUR case-by-case against the export, and a new layer-1 test parses the
1144
+ workflow and asserts every referenced identifier is declared or is one of the eight documented
1145
+ sandbox globals.
1146
+
1147
+ Also in this release, both halves of the K2 plan-completeness gate that field use found:
1148
+
1149
+ - **C6 scopes each amendment to its own block** — from its `AM-N` line to the line where the next one
1150
+ begins. The old three-line window refused amendments whose `→ test` marker sat on a `Confirmation:`
1151
+ line further down (measured: line 3 passes, line 4 fails), and — worse, and not reported — it
1152
+ PASSED a testless amendment that happened to sit next to a tested one, which borrowed its
1153
+ neighbour's marker. The plural `→ tests \`a\` and \`b\`` form is now matched, and a wrapped
1154
+ `AM-1..AM-4;` range no longer opens a phantom amendment. Measured over 142 plans: 402 C6 failures
1155
+ before, 190 after, **0 plans newly failing**. The Step-6 planner prompt now states that the row is
1156
+ machine-read and where the marker must sit.
1157
+ - **The K2 gate stops guessing the workspace.** When the workspace was not pinned it was taken from
1158
+ the gate agent's own working directory, which against an external target repo resolved to that
1159
+ repo — so the workspace candidate silently became a duplicate of the repo candidate and a skill
1160
+ installed in the workspace was never found (`NOT-ESTABLISHED`, exit 3, the coding step never ran).
1161
+ New `args.workspace` pins it, the shipped call site passes it, each candidate is now labelled in the
1162
+ 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.4",
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": "eb5998619673f928f02b6b462278acfc4871387e1f6495ab0b613a965611cda7"
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": "8e082526415c4ce4427c51dff222c248e29ef5fbdcd17f0d957c28d621738513"
69
69
  }
70
70
  ]
71
71
  },
@@ -365,7 +365,7 @@
365
365
  "hashes": [
366
366
  {
367
367
  "alg": "SHA-256",
368
- "content": "2ef7cf316903f0cbb1fabe1f0e1770c22b2cb3a11a4118cac0529c25074b5e8a"
368
+ "content": "4d20dbc85ea5bb93ca90fd8cd089255bab9c76a912ca3ad954f3a4bb670f3c45"
369
369
  }
370
370
  ]
371
371
  },
@@ -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": "a6ff055d50f4eb5790dab36deee188e880eec1362188ec16a5504710294d87c3"
789
789
  }
790
790
  ]
791
791
  },
@@ -118,6 +118,32 @@ GET /api/v1/{resource}/:id
118
118
  Errors: 401, 404
119
119
  ```
120
120
 
121
+ ## Observability — a MANDATORY section of `05_architecture.md`
122
+
123
+ The artifact must carry a section headed exactly **`Observability`**, answering how anyone would
124
+ know this feature is working once it ships:
125
+
126
+ - what it **logs**, and at what level;
127
+ - what it **counts** — the one or two numbers that would move if it broke;
128
+ - what a **failure looks like from outside** — the symptom, not the stack trace;
129
+ - **who would notice**, and how.
130
+
131
+ If the feature genuinely emits nothing at runtime — a pure refactor, a doc change, a gate that only
132
+ runs in CI — write **"nothing to observe"** and say why. That is a complete answer, not a gap. What
133
+ is not acceptable is leaving the question unanswered.
134
+
135
+ The heading is read by a machine (`dz score` reports it as a discipline), so it is spelled exactly
136
+ `Observability` — the prompt above and the check derive that word from one shared constant, because
137
+ a prompt asking for one heading while a check greps another produces a gate that fails every honest
138
+ run.
139
+
140
+ **Why this section exists.** MEASURED 2026-08-25: across 107 existing `05_architecture.md` files,
141
+ ZERO carried such a section, and the word `observability` appeared nowhere in the pipeline's own
142
+ prompts. The pipeline kept five telemetry stores about itself and asked for none about what it
143
+ built. The two capable skills — `observability` (818 lines) and `observability-testing-patterns`
144
+ (946 lines) — were fully written and unreachable, because nothing called them. This section is the
145
+ call site.
146
+
121
147
  ## Write discipline (the 180-second rule)
122
148
 
123
149
  An executor that returns from a tool call and then thinks in silence past **180 seconds** is killed by the
@@ -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
 
@@ -2064,7 +2125,7 @@ if (isMplus) {
2064
2125
  designThunks.push(() => subCheckpoint('qcsd', qcsdPromptText, qcsdOpts, ['03.5_ideation_report.md'], () => designStage(qcsdPromptText, qcsdOpts, FDIR + '/03.5_ideation_report.md', 'qcsd')))
2065
2126
  designPairMeta.push({ role: 'design:qcsd', prompt: qcsdPromptText, model: modelLabel(qcsdOpts), lessons: [] })
2066
2127
  const archExtra = isLplus ? ' Also ' + FDIR + '/04_domain_model.md (DDD).' : ''
2067
- const archPromptText = (isLplus ? 'Step 4 (DDD) + ' : '') + 'Step 5 (Architecture) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the code. Write ' + FDIR + '/05_architecture.md (components, data flow, integration points, the emit/merge/wiring shape).' + archExtra + ' Return wrote[] + summary.' + WRITE_DISCIPLINE
2128
+ const archPromptText = (isLplus ? 'Step 4 (DDD) + ' : '') + 'Step 5 (Architecture) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the code. Write ' + FDIR + '/05_architecture.md (components, data flow, integration points, the emit/merge/wiring shape). It MUST carry a section headed exactly "Observability" answering how anyone would know this feature is working once it ships: what it logs, what it counts, what a failure looks like from outside, and who would notice. If the feature genuinely emits nothing at runtime — a pure refactor, a CI-only gate — say "nothing to observe" and why; that is a complete answer, not a gap. What is not acceptable is leaving the question unanswered.' + archExtra + ' Return wrote[] + summary.' + WRITE_DISCIPLINE
2068
2129
  designThunks.push(() => subCheckpoint('architecture', archPromptText, archOpts, ['05_architecture.md'].concat(isLplus ? ['04_domain_model.md'] : []), () => designStage(archPromptText, archOpts, FDIR + '/05_architecture.md', 'architecture')))
2069
2130
  designPairMeta.push({ role: 'design:architecture', prompt: archPromptText, model: modelLabel(archOpts), lessons: [] })
2070
2131
  }
@@ -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
  }