@dzhechkov/skills-feature-adr 1.5.0 → 1.5.1

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
@@ -5,7 +5,7 @@
5
5
  "files": [
6
6
  {
7
7
  "path": "CHANGELOG.md",
8
- "sha256": "59592f0b4612a14bdec61c96c20655644701cd77a0bf9d68315e1c98461900ff"
8
+ "sha256": "88e0389016f697b90731ee8743a35e523f0ccb079426d59861d894f33e516392"
9
9
  },
10
10
  {
11
11
  "path": "LICENSE",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "README.md",
16
- "sha256": "8df52c2c7d7c70ef29670339ac01a6c0b0b6a36bb51a0be15dcc03357097cca8"
16
+ "sha256": "47f9402d4c3954d6d5a75ad906ec7b592010fdfd80f347d4a87dd14dd9e300ce"
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": "0fc1ae047774c2c67c6acf77a88eef8c9532c609bfc68b9e9cb37591cb61158f"
28
+ "sha256": "3fbe9ef86c9f6fa7c86bbe7760729094e80edb67577e397678b2d98dca1c2034"
29
29
  },
30
30
  {
31
31
  "path": "scripts/sync-templates.js",
@@ -165,7 +165,7 @@
165
165
  },
166
166
  {
167
167
  "path": "templates/.claude/skills/feature-adr/modules/08-qe.md",
168
- "sha256": "97e9a7bc54f196b381c4ef71258cddcb2fb48f216df1ba015927c176bad57084"
168
+ "sha256": "bd5b5809142c12fa1378de446b68027a0c342a7a91ad17e4fb3395ca8f6a8508"
169
169
  },
170
170
  {
171
171
  "path": "templates/.claude/skills/feature-adr/modules/09-fleet-qe.md",
@@ -321,7 +321,7 @@
321
321
  },
322
322
  {
323
323
  "path": "templates/.claude/workflows/feature-adr.js",
324
- "sha256": "3c8838c6a5107ea8a4b909cd9ccfa4d416324bb4ed9341f50b8e23f1b569d244"
324
+ "sha256": "0c74e8995d82bb986ef3233e91beebfa0e761aae4434f24554077bb05d2199e9"
325
325
  },
326
326
  {
327
327
  "path": "templates/lib/memory-protocol.md",
@@ -349,5 +349,5 @@
349
349
  }
350
350
  ]
351
351
  },
352
- "signature": "8JbfbAiP55HvylGRoFfLwEtYCoo02jM9PwfOcTiYWQXfN/Utcg+Wc7uFo08yfqPUyONXsMCcEfqB7Y5eKWIvDg=="
352
+ "signature": "9WVoqK2D8eKSnmhhMqHPt31NedxwZCHiHyxJ5VLMxSVMecMqyXgrCJ2GQ3OOfIeiquB2D01bh7QjZW/EMVM9Aw=="
353
353
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.1] - 2026-08-21
4
+
5
+ ### Changed — the Step-8 amendment gate is a COMMAND, and the durable writers are witnessed
6
+
7
+ - `AMENDMENT_GATE` no longer asks the QE agent to judge whether every `AM-N` row names a real test.
8
+ It runs `dz amendment-check --slug <slug> --json` and reports the parsed verdict; the old judgement
9
+ wording is REMOVED from both workflow copies and all four `modules/08-qe.md` copies, not
10
+ accompanied. MEASURED 2026-08-21: `features/qe-scoped-review` shipped with five named amendment
11
+ test ids of which none existed, while its plan recorded `## Amendments: None`.
12
+ - the run-cost ledger and the training-pair capture (including backfill) stop handing a subagent a
13
+ pre-baked shell string carrying their payload — the shape a security classifier blocked nine times
14
+ in one run. Both now call `dz feature-adr-record`. The `sed` that rewrote `"date":null` / `"ts":null`
15
+ inside an already-serialised document is gone: the command stamps before serialising.
16
+ - a record failure still NEVER fails the run, but now survives it: `recordFailures` is returned from
17
+ every exit instead of scrolling past inside one log line.
18
+
19
+ Requires `@dzhechkov/harness-core >= 0.6.1`.
20
+
3
21
  ## [1.5.0] - 2026-08-21
4
22
 
5
23
  ### Changed — feature `qe-scoped-review`
package/README.md CHANGED
@@ -169,6 +169,25 @@ 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)
173
+
174
+ Every `AM-N` row must resolve to a test found INSIDE the file the row names, and the check is
175
+ `dz amendment-check --slug <slug> --json` rather than a paragraph asking the reviewer to confirm it.
176
+ MEASURED 2026-08-21: `features/qe-scoped-review` shipped with five named amendment test ids of which
177
+ none existed, while its plan recorded `## Amendments: None`. The gate did not fail — a prompt cannot.
178
+
179
+ The PLAN is authoritative when it carries rows (Step 6 owes "carry AM-N into the plan verbatim"), and
180
+ the rule that keeps that honest is coverage: an amendment the plan drops — or rewords under the same
181
+ id — is a failure. Renaming the TEST stays legitimate; tests are named later than ideation guesses.
182
+
183
+ The durable writers were migrated in the same release: the run-cost ledger and the training-pair
184
+ capture no longer hand a subagent a pre-baked shell string with their payload baked in — the shape a
185
+ security classifier blocked nine times in one run. Both call `dz feature-adr-record`, which refuses
186
+ before writing and verifies the append by re-reading the tail. A record failure still never fails the
187
+ run; it now survives it in `recordFailures`.
188
+
189
+ Requires `@dzhechkov/harness-core >= 0.6.1`.
190
+
172
191
  ### Step 0 writes the assessment down, and the acid check gets its input back (v1.5.0)
173
192
 
174
193
  Step 0 classifies the feature and now **writes `00_complexity_assessment.md` before it returns** — the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
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
@@ -15,7 +15,7 @@
15
15
  "hashes": [
16
16
  {
17
17
  "alg": "SHA-256",
18
- "content": "59592f0b4612a14bdec61c96c20655644701cd77a0bf9d68315e1c98461900ff"
18
+ "content": "88e0389016f697b90731ee8743a35e523f0ccb079426d59861d894f33e516392"
19
19
  }
20
20
  ]
21
21
  },
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "8df52c2c7d7c70ef29670339ac01a6c0b0b6a36bb51a0be15dcc03357097cca8"
38
+ "content": "47f9402d4c3954d6d5a75ad906ec7b592010fdfd80f347d4a87dd14dd9e300ce"
39
39
  }
40
40
  ]
41
41
  },
@@ -65,7 +65,7 @@
65
65
  "hashes": [
66
66
  {
67
67
  "alg": "SHA-256",
68
- "content": "0fc1ae047774c2c67c6acf77a88eef8c9532c609bfc68b9e9cb37591cb61158f"
68
+ "content": "3fbe9ef86c9f6fa7c86bbe7760729094e80edb67577e397678b2d98dca1c2034"
69
69
  }
70
70
  ]
71
71
  },
@@ -415,7 +415,7 @@
415
415
  "hashes": [
416
416
  {
417
417
  "alg": "SHA-256",
418
- "content": "97e9a7bc54f196b381c4ef71258cddcb2fb48f216df1ba015927c176bad57084"
418
+ "content": "bd5b5809142c12fa1378de446b68027a0c342a7a91ad17e4fb3395ca8f6a8508"
419
419
  }
420
420
  ]
421
421
  },
@@ -805,7 +805,7 @@
805
805
  "hashes": [
806
806
  {
807
807
  "alg": "SHA-256",
808
- "content": "3c8838c6a5107ea8a4b909cd9ccfa4d416324bb4ed9341f50b8e23f1b569d244"
808
+ "content": "0c74e8995d82bb986ef3233e91beebfa0e761aae4434f24554077bb05d2199e9"
809
809
  }
810
810
  ]
811
811
  },
@@ -160,18 +160,32 @@ The Confirmation-to-test link is load-bearing. If absent, grade no better than C
160
160
 
161
161
  ### 3b. Amendment Gate (P2 — amendments inherit the Confirmation discipline)
162
162
 
163
- Read the `## Amendments` sections of `03.5_ideation_report.md` and `06_implementation_plan.md` (skip
164
- silently if absent). For EVERY `AM-N` row verify:
165
- 1. the row contains a `→ test ` token naming a real test (the fixed shape);
166
- 2. the named test **exists** in the suite;
167
- 3. it is **non-vacuous** — include the amendment test files in the SAME
168
- `dz discrimination-check --test <f1,f2,...> --base HEAD --json` run as the ADR property test. An
169
- amendment test that stays green at pre-feature base proves nothing about its amendment;
170
- 4. a **safeguard** amendment's test proves the safeguard FIRES on a real input (a structurally-dead
171
- safeguard passes an existence test and never fires in production).
172
-
173
- A missing, unnamed, or vacuous amendment test is a HIGH gap. This closes the distance between "we caught
174
- the design flaw" (challenge-panel/QCSD) and "we proved the fix works".
163
+ **Do not judge this yourself — RUN the check.** Resolution of every `AM-N` row to a real test is a
164
+ deterministic question, and a deterministic question must not live in prose (cost-of-detection ladder).
165
+ It used to live here, and `features/qe-scoped-review` shipped with five named amendment test ids of
166
+ which none existed, while its plan recorded `## Amendments: None`.
167
+
168
+ ```bash
169
+ dz amendment-check --slug <feature-slug> --json # or --feature-dir <abs path>
170
+ ```
171
+
172
+ | Outcome | Exit | What it means for the gate |
173
+ |---|---|---|
174
+ | `pass` | 0 | every row resolves to a test found inside the file the row names |
175
+ | `skip` | 0 | no `## Amendments` section — an absence, stated, never a silent pass |
176
+ | `fail` | 1 | a HIGH gap: quote every reason verbatim into the QE report |
177
+ | `not-established` | 3 | the grammar matched nothing, or the inputs were unreadable — **never a pass** |
178
+
179
+ Empty stdout, a crash, or a missing `dz` is `not-established`, not a clean gate.
180
+
181
+ **What the command does NOT prove:** that a resolved test discriminates. Vacuity stays yours, in the
182
+ SAME `dz discrimination-check --test <f1,f2,...> --base HEAD --json` run as the ADR property test — an
183
+ amendment test that stays green at pre-feature base proves nothing about its amendment. A **safeguard**
184
+ amendment additionally needs its test to prove the safeguard FIRES on a real input: a structurally-dead
185
+ safeguard passes an existence check and never fires in production.
186
+
187
+ This closes the distance between "we caught the design flaw" (challenge-panel/QCSD) and "we proved the
188
+ fix works".
175
189
 
176
190
  ### 3c. I/O-on-pure-path + fixture-swap hunt (P5)
177
191
 
@@ -459,6 +459,10 @@ const TP_MAX_IO = 48000
459
459
  const TP_DIR = REPO + '/.dz/fa-training/' + SLUG
460
460
  const TP_MARK_DIR = REPO + '/.dz/fa-training/.backfill-marks'
461
461
  const LEDGER_FILE = REPO + '/.dz/feature-adr/run-cost-ledger.jsonl'
462
+ // ADR-003: a record failure NEVER fails the run — a cost row is observability, and observability must
463
+ // not take a six-hour run down with it. But today's silence is not "secondary", it is invisible: one
464
+ // log line among thousands. Failures accumulate here and are RETURNED, so they survive the run.
465
+ const recordFailures = []
462
466
  const TP_README = REPO + '/.dz/fa-training/README.md'
463
467
  const TP_BACKFILL_OK = 'TP-BACKFILL-OK'
464
468
  const TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP'
@@ -554,40 +558,49 @@ async function capturePairs(stage, phaseName, records, resumeGuardStage) {
554
558
  const markStage = String(stage).replace(/\.\./g, '_').replace(/\//g, '_')
555
559
  // The workflow pair line carries the stable feature SLUG, so this content key stays stable across resumes.
556
560
  const mark = TP_MARK_DIR + '/' + markStage + '-' + fnv1a64(stage + '\0' + lines.join('\n'))
557
- let cmd = 'mkdir -p ' + shq(TP_DIR) + ' && { [ -f ' + shq(TP_README) + ' ] || printf \'%s\\n\' ' + shq(TP_PRIVACY_NOTE) + ' > ' + shq(TP_README) + '; }'
558
- // Release the mark only when the append fails so a failed backfill stays retryable; DUP is distinct from SKIP.
561
+ // WITNESSED WRITE (ADR-001), backfill mode. The mark is still an atomic mkdir — but it is the
562
+ // COMMAND that takes it, so the same invocation that claims the mark is the one that validates
563
+ // and writes. The old form claimed the mark in a shell pipeline the courier never inspected.
559
564
  if (mode === 'backfill') {
560
- cmd += ' && mkdir -p ' + shq(TP_MARK_DIR) + ' && if mkdir ' + shq(mark) + ' 2>/dev/null; then if [ -f ' + shq(file) + ' ]; then echo ' + shq(TP_BACKFILL_SKIP) + '; else { '
565
+ let outcome = 'written'
561
566
  for (let i = 0; i < lines.length; i++) {
562
- if (i > 0) cmd += ' && '
563
- // Keep the workflow-owned shell-side timestamp fill inside the absence-guarded append.
564
- cmd += 'printf \'%s\' ' + shq(lines[i]) + ' | sed "s/\\"ts\\":null/\\"ts\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\"/" >> ' + shq(file) + ' && printf \'\\n\' >> ' + shq(file)
565
- }
566
- cmd += ' && echo ' + shq(TP_BACKFILL_OK) + '; } || { rmdir ' + shq(mark) + ' 2>/dev/null; false; }; fi; else echo ' + shq(TP_BACKFILL_DUP) + '; fi'
567
- const out = await agent('Run EXACTLY this one shell command via your Bash tool and reply with only its stdout: ' + cmd, { label: 'trainpair:backfill:' + stage, phase: phaseName, effort: 'low' })
568
- const readback = String(out == null ? '' : out)
569
- if (/TP-BACKFILL-OK/.test(readback)) {
570
- log('training-pair: ' + stage + ' missing pair backfilled from the checkpoint')
571
- } else if (/TP-BACKFILL-DUP/.test(readback)) {
572
- log('training-pair: ' + stage + ' another run already captured this pair (nothing written)')
573
- } else if (/TP-BACKFILL-SKIP/.test(readback)) {
574
- log('training-pair: ' + stage + ' pair already existed (nothing written)')
575
- } else {
576
- log('training-pair: ' + stage + ' backfill UNVERIFIED (no TP-BACKFILL readback) — the pair may be missing on disk (non-blocking; the run continues)')
577
- captureFailures.push(captureFailureRecord(stage, mode, 'backfill-unverified', 'no TP-BACKFILL-OK or TP-BACKFILL-SKIP readback'))
567
+ const cmdOne = DZ + ' feature-adr-record --kind training-pair --slug ' + shq(SLUG) + ' --stage ' + shq(stage)
568
+ + ' --project ' + shq(REPO) + ' --mark ' + shq(markStage + '-' + fnv1a64(stage + '\0' + lines.join('\n')) + '-' + i)
569
+ + ' --once --pair ' + shq(lines[i]) + ' --json'
570
+ const out = await agent('Run this command via your Bash tool and reply with only its stdout: ' + cmdOne, { label: 'trainpair:backfill:' + stage, phase: phaseName, effort: 'low' })
571
+ const readback = String(out == null ? '' : out)
572
+ const m = /"verdict"\s*:\s*"(written|duplicate|skipped)"/.exec(readback)
573
+ if (m === null) {
574
+ outcome = 'unverified'
575
+ captureFailures.push(captureFailureRecord(stage, mode, 'backfill-unverified', readback === '' ? 'no output from the record command' : readback.slice(0, 200)))
576
+ recordFailures.push({ kind: 'training-pair', stage: stage, reason: 'backfill: ' + (readback === '' ? 'no output from the record command' : readback.slice(0, 300)) })
577
+ break
578
+ }
579
+ if (m[1] !== 'written' && outcome === 'written') outcome = m[1]
578
580
  }
581
+ if (outcome === 'written') log('training-pair: ' + stage + ' missing pair backfilled from the checkpoint')
582
+ else if (outcome === 'duplicate') log('training-pair: ' + stage + ' another run already captured this pair (nothing written)')
583
+ else if (outcome === 'skipped') log('training-pair: ' + stage + ' pair already existed (nothing written)')
584
+ else log('training-pair: ' + stage + ' backfill NOT WRITTEN (non-blocking; the run continues)')
579
585
  return
580
586
  }
587
+ // WITNESSED WRITE (ADR-001): one command invocation per pair, data passed as an ARGUMENT. The
588
+ // `sed` that used to rewrite `"ts":null` inside an already-serialised document is gone — the
589
+ // command stamps the timestamp BEFORE serialising, so no text surgery touches a JSON value.
590
+ let allWritten = true
581
591
  for (const line of lines) {
582
- // sed fills the top-level "ts":null with the real UTC time; the top-level ts precedes input in
583
- // key order, so the FIRST match on the line is always the record's own ts, never payload text.
584
- cmd += ' && printf \'%s\' ' + shq(line) + ' | sed "s/\\"ts\\":null/\\"ts\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\"/" >> ' + shq(file) + ' && printf \'\\n\' >> ' + shq(file)
592
+ const cmdOne = DZ + ' feature-adr-record --kind training-pair --slug ' + shq(SLUG) + ' --stage ' + shq(stage)
593
+ + ' --project ' + shq(REPO) + ' --pair ' + shq(line) + ' --json'
594
+ const out = await agent('Run this command via your Bash tool and reply with only its stdout: ' + cmdOne, { label: 'trainpair:' + stage, phase: phaseName, effort: 'low' })
595
+ const readback = String(out == null ? '' : out)
596
+ if (!/"verdict"\s*:\s*"(written|duplicate|skipped)"/.test(readback)) {
597
+ allWritten = false
598
+ captureFailures.push(captureFailureRecord(stage, mode, 'unverified', readback === '' ? 'no output from the record command' : readback.slice(0, 200)))
599
+ recordFailures.push({ kind: 'training-pair', stage: stage, reason: readback === '' ? 'no output from the record command' : readback.slice(0, 300) })
600
+ }
585
601
  }
586
- cmd += ' && echo TP-OK'
587
- const out = await agent('Run EXACTLY this one shell command via your Bash tool and reply with only its stdout: ' + cmd, { label: 'trainpair:' + stage, phase: phaseName, effort: 'low' })
588
- if (!/TP-OK/.test(String(out == null ? '' : out))) {
589
- log('training-pair: ' + stage + ' capture UNVERIFIED (no TP-OK readback) — the pair may be missing on disk (non-blocking; the run continues)')
590
- captureFailures.push(captureFailureRecord(stage, mode, 'unverified', 'no TP-OK readback'))
602
+ if (!allWritten) {
603
+ log('training-pair: ' + stage + ' capture NOT WRITTEN for at least one pair (non-blocking; the run continues)')
591
604
  }
592
605
  } catch (e) {
593
606
  const message = e && e.message ? e.message : String(e)
@@ -615,17 +628,18 @@ async function appendRunCostRow(stage, phaseName) {
615
628
  auto: true,
616
629
  mode: (typeof MODE === 'string' && MODE !== '') ? MODE : null,
617
630
  })
618
- const dir = LEDGER_FILE.replace(/\/[^/]*$/, '')
619
- // Safe sed target: JSON-escaped scalar values cannot contain the raw `"date":null` token,
620
- // and the top-level `"date":null` field appears exactly once.
621
- const cmd = 'mkdir -p ' + shq(dir)
622
- + " && printf '%s' " + shq(line)
623
- + ' | sed "s/\\"date\\":null/\\"date\\":\\"$(date -u +%Y-%m-%d)\\"/" >> ' + shq(LEDGER_FILE)
624
- + " && printf '\\n' >> " + shq(LEDGER_FILE)
625
- + ' && echo LEDGER-OK'
626
- const out = await agent('Run EXACTLY this one shell command via your Bash tool and reply with only its stdout: ' + cmd, { label: 'ledger:append', phase: phaseName, effort: 'low' })
627
- if (!/LEDGER-OK/.test(String(out == null ? '' : out))) {
628
- log('run-cost ledger: ' + stage + ' row UNVERIFIED (no LEDGER-OK readback) — the row may be missing on disk (SECONDARY; the run continues)')
631
+ // WITNESSED WRITE (ADR-001): the subagent RUNS a command with data arguments; it is no longer
632
+ // handed a shell pipeline with the row baked in. The command refuses a malformed row, stamps the
633
+ // date BEFORE serialising (no sed over a serialised document) and verifies the append by
634
+ // re-reading the tail. A courier could do none of those three.
635
+ const cmd = DZ + ' feature-adr-record --kind ledger --stage ' + shq(stage) + ' --project ' + shq(REPO)
636
+ + ' --row ' + shq(line) + ' --json'
637
+ const out = await agent('Run this command via your Bash tool and reply with only its stdout: ' + cmd, { label: 'ledger:append', phase: phaseName, effort: 'low' })
638
+ const readback = String(out == null ? '' : out)
639
+ if (!/"verdict"\s*:\s*"written"/.test(readback)) {
640
+ // ADR-003: SECONDARY — never fails the run — but the failure now SURVIVES it.
641
+ recordFailures.push({ kind: 'ledger', stage: stage, reason: readback === '' ? 'no output from the record command' : readback.slice(0, 300) })
642
+ log('run-cost ledger: ' + stage + ' row NOT WRITTEN (SECONDARY; the run continues) — ' + readback.slice(0, 200))
629
643
  }
630
644
  } catch (e) {
631
645
  log('run-cost ledger: ' + stage + ' append failed (SECONDARY; non-blocking): ' + (e && e.message ? e.message : String(e)))
@@ -1895,7 +1909,7 @@ const DISCRIMINATION_GATE = '\u00a742 TEST-DISCRIMINATION GATE (run right after
1895
1909
  // shape (a linter can assert the `→ test ` token); Step-8 verifies existence + non-vacuity via the SAME
1896
1910
  // dz discrimination-check that guards the ADR property (cost-of-detection ladder: judgment → step gate).
1897
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).'
1898
- const AMENDMENT_GATE = 'AMENDMENT GATE (P2): read the `## Amendments` sections of ' + FDIR + '/03.5_ideation_report.md and ' + FDIR + '/06_implementation_plan.md (skip silently if absent). EVERY `AM-N` row must contain a `→ test ` token naming a real test. Verify: (a) the named test EXISTS in the suite; (b) it is NON-VACUOUS — include the amendment test files in the SAME `dz discrimination-check --test <f1,f2,...> --base HEAD --json` run as the ADR property test (an amendment test that stays green at pre-feature base proves nothing); (c) a safeguard amendment\'s test proves the safeguard FIRES on a real input. A missing, unnamed, or vacuous amendment test is a HIGH gap. ' +
1912
+ 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. ' +
1899
1913
  '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.'
1900
1914
 
1901
1915
  // Step 0: Router + MANDATORY self-learning recall
@@ -2172,7 +2186,7 @@ if (!fanVerdict.complete) {
2172
2186
  // invisible to cost analysis, which is how a stage that keeps dying stays cheap-looking.
2173
2187
  // (coderUsed/qe are the outer bindings, both still null here, so the row reports null honestly.)
2174
2188
  await appendRunCostRow('design-gate', 'Design')
2175
- return { tier: tier, phase: 'design-incomplete', slug: SLUG, artifactsDir: FDIR, missingSubstages: fanVerdict.missingSubstages, missingArtifacts: fanVerdict.missingArtifacts, reason: fanVerdict.reason, modelsUsed: modelsUsed, gates: { design: fanVerdict.reason === 'probe-not-established' ? 'not-established' : 'incomplete', plan: 'not-run', planCompleteness: 'not-run', challengePanel: 'not-run', code: 'not-run', qe: 'not-run' }, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off', captureFailures: captureFailures, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: 'REFUSED at the Step-5/6 boundary: ' + what + ', so the design is incomplete and Step 6 was NOT dispatched. Planning off a partial design produces a plan with no ADR behind it. ' + repair + ' If a sibling died on a Claude limit, add usage-adaptive routing or route that stage to Codex first (args.models). To rebuild the whole design from scratch instead, re-invoke with args.resume=\'never\'.' }
2189
+ return { tier: tier, phase: 'design-incomplete', slug: SLUG, artifactsDir: FDIR, missingSubstages: fanVerdict.missingSubstages, missingArtifacts: fanVerdict.missingArtifacts, reason: fanVerdict.reason, modelsUsed: modelsUsed, gates: { design: fanVerdict.reason === 'probe-not-established' ? 'not-established' : 'incomplete', plan: 'not-run', planCompleteness: 'not-run', challengePanel: 'not-run', code: 'not-run', qe: 'not-run' }, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off', captureFailures: captureFailures, recordFailures: recordFailures, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: 'REFUSED at the Step-5/6 boundary: ' + what + ', so the design is incomplete and Step 6 was NOT dispatched. Planning off a partial design produces a plan with no ADR behind it. ' + repair + ' If a sibling died on a Claude limit, add usage-adaptive routing or route that stage to Codex first (args.models). To rebuild the whole design from scratch instead, re-invoke with args.resume=\'never\'.' }
2176
2190
  }
2177
2191
 
2178
2192
  // Step 6: Plan — optionally routed to Codex's top model (opt-in via args.planner='codex').
@@ -2320,7 +2334,7 @@ if (planGate.verdict !== 'pass') {
2320
2334
  // NAMED REFUSAL — the run stops here and the coder is never dispatched. This precedes the L/XL
2321
2335
  // checkpoint deliberately: an incomplete plan is not something to steer, it is something to fix.
2322
2336
  await appendRunCostRow('plan-gate', 'Plan')
2323
- return { tier: tier, phase: 'plan-gate-failed', slug: SLUG, artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: modelsUsed, planGate: planGate, gates: { plan: (plan ? 'produced' : 'missing'), planCompleteness: planGate.verdict, challengePanel: 'not-run', code: 'not-run', qe: 'not-run' }, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off', captureFailures: captureFailures, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: refusalNoteFor(planGate, SLUG) }
2337
+ return { tier: tier, phase: 'plan-gate-failed', slug: SLUG, artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: modelsUsed, planGate: planGate, gates: { plan: (plan ? 'produced' : 'missing'), planCompleteness: planGate.verdict, challengePanel: 'not-run', code: 'not-run', qe: 'not-run' }, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off', captureFailures: captureFailures, recordFailures: recordFailures, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: refusalNoteFor(planGate, SLUG) }
2324
2338
  }
2325
2339
 
2326
2340
  // Hybrid checkpoint for L/XL
@@ -2341,7 +2355,7 @@ if (stopHere) {
2341
2355
  // presence), never from prose, so a skipped gate shows as 'not-run' instead of being silently forgotten.
2342
2356
  const planGates = { plan: (plan ? 'produced' : 'missing'), planCompleteness: planGate.verdict, challengePanel: (challengeVerdict ? 'ran' : 'not-run'), code: 'not-run', qe: 'not-run' }
2343
2357
  await appendRunCostRow('plan', 'Plan')
2344
- return { tier: tier, phase: 'checkpoint-after-plan', artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: plannedModels, challengeVerdict: challengeVerdict, gates: planGates, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off', captureFailures: captureFailures, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: 'L/XL checkpoint - review the ADR + plan (+ the planned code/qe/fleet models) + the challenge panel verdict (advisory) + the gates line, then re-invoke with args.stopAfter="none" to implement + QE (durable checkpoints make the re-invoke resume router+design+plan instead of re-running them). Present the gates map as a `🚦 Gates:` line in the checkpoint banner, rendering the planCompleteness entry as `K2 plan-completeness ✓` (pass) / `✗` (fail) / `inconclusive`.' }
2358
+ return { tier: tier, phase: 'checkpoint-after-plan', artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: plannedModels, challengeVerdict: challengeVerdict, gates: planGates, resumedStages: resumedStages, checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off', trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off', captureFailures: captureFailures, recordFailures: recordFailures, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: 'L/XL checkpoint - review the ADR + plan (+ the planned code/qe/fleet models) + the challenge panel verdict (advisory) + the gates line, then re-invoke with args.stopAfter="none" to implement + QE (durable checkpoints make the re-invoke resume router+design+plan instead of re-running them). Present the gates map as a `🚦 Gates:` line in the checkpoint banner, rendering the planCompleteness entry as `K2 plan-completeness ✓` (pass) / `✗` (fail) / `inconclusive`.' }
2345
2359
  }
2346
2360
 
2347
2361
  // Step 7: Code (optional Codex fallback on Claude-limit exhaustion)
@@ -2974,7 +2988,7 @@ return {
2974
2988
  resumedStages: resumedStages,
2975
2989
  checkpointing: CHECKPOINTS_ON ? RESUME_MODE : 'off',
2976
2990
  trainingPairs: CAPTURE_PAIRS ? TP_DIR : 'off',
2977
- captureFailures: captureFailures,
2991
+ captureFailures: captureFailures, recordFailures: recordFailures,
2978
2992
  reqeDue: reqeDue,
2979
2993
  brain: BRAIN,
2980
2994
  polymorphism: POLY.hasManifest ? POLY.report : null,