@dzhechkov/skills-feature-adr 1.5.7 → 1.5.8

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": "4ab808507be5c64cfa7b74ef3c186145d0654cd9c2fad86229685edaac6edb10"
16
+ "sha256": "dc985b443d8ac9d4091b09367330f7f610c35572d99f635855989fd893c5e6ac"
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": "1efad8edf817286284ef35578e3b5046205350603b7e9cb9b979993a69f85c8a"
28
+ "sha256": "d57357d1feb9f111d0674f1c307a4d15a41cb005074351d37ebaf7dc1afe3575"
29
29
  },
30
30
  {
31
31
  "path": "src/cli.js",
@@ -313,7 +313,7 @@
313
313
  },
314
314
  {
315
315
  "path": "templates/.claude/workflows/feature-adr.js",
316
- "sha256": "f5454faf9cca12a3cbef5444a3fe586823bfe2a164ab391c9589ed206a0b9b9f"
316
+ "sha256": "f287a73bdf8c11c99eedbc071c6babb3201a6cec7d644d65f20ece3cd4ca51c1"
317
317
  },
318
318
  {
319
319
  "path": "templates/lib/memory-protocol.md",
@@ -325,5 +325,5 @@
325
325
  }
326
326
  ]
327
327
  },
328
- "signature": "iuNdrUAvCEsJrxokWl6qDVTsiQJb9/v+oSUREgL2dkMEEzslnKnzYH144Cxg/qxpNZqocyHqvsH2A8I1Eu2wCQ=="
328
+ "signature": "t3rY38fu7zb8GjsIjG0OOeqtw04W4nlzawE//PhPKbHbtLUMgT/1SOaXFUWEyjQvNTb13dCJXrHggR0yf/5RBA=="
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.7)
172
+ ### The Step-8 amendment gate runs a command instead of judging (v1.5.8)
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.5.7",
3
+ "version": "1.5.8",
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"
@@ -42,12 +42,12 @@
42
42
  },
43
43
  "repository": {
44
44
  "type": "git",
45
- "url": "https://github.com/djd1m/dz-harness-hub.git",
45
+ "url": "git+https://github.com/djd1m/dz-harness.git",
46
46
  "directory": "packages/@dzhechkov/skills-feature-adr"
47
47
  },
48
- "homepage": "https://github.com/djd1m/dz-skill-bundles/blob/main/docs/feature-adr-team-onboarding.md",
48
+ "homepage": "https://github.com/djd1m/dz-harness/tree/main/packages/@dzhechkov/skills-feature-adr#readme",
49
49
  "bugs": {
50
- "url": "https://github.com/djd1m/dz-skill-bundles/issues"
50
+ "url": "https://github.com/djd1m/dz-harness/issues"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@dzhechkov/keysarium-core": "^1.0.0"
package/sbom.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "4ab808507be5c64cfa7b74ef3c186145d0654cd9c2fad86229685edaac6edb10"
38
+ "content": "dc985b443d8ac9d4091b09367330f7f610c35572d99f635855989fd893c5e6ac"
39
39
  }
40
40
  ]
41
41
  },
@@ -69,7 +69,7 @@
69
69
  },
70
70
  {
71
71
  "name": "dz:canonical-json-sha256-v2",
72
- "value": "1efad8edf817286284ef35578e3b5046205350603b7e9cb9b979993a69f85c8a"
72
+ "value": "d57357d1feb9f111d0674f1c307a4d15a41cb005074351d37ebaf7dc1afe3575"
73
73
  }
74
74
  ]
75
75
  },
@@ -789,7 +789,7 @@
789
789
  "hashes": [
790
790
  {
791
791
  "alg": "SHA-256",
792
- "content": "f5454faf9cca12a3cbef5444a3fe586823bfe2a164ab391c9589ed206a0b9b9f"
792
+ "content": "f287a73bdf8c11c99eedbc071c6babb3201a6cec7d644d65f20ece3cd4ca51c1"
793
793
  }
794
794
  ]
795
795
  },
@@ -184,7 +184,7 @@ const CKPT_FILE = FDIR + '/.fa-state/checkpoints.jsonl'
184
184
  // M10 Stage-A, feature loop-designer). This region is now a GENERATED BLOB (regen-diff-gated by
185
185
  // loop-blobs-regen.test.ts): edit the canonical TS FIRST, run node scripts/gen-loop-blobs.mjs,
186
186
  // then re-splice. The value-pinned wiring tests in feature-adr-checkpoints.test.ts stay the net.
187
- // ── BEGIN BLOB checkpoints@1.1.0 sha256:a44560c6036fd143b7a3f125fec00fa8b91c3c06ac5b9e1ecfb83d27a5211e9b src=packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts ──
187
+ // ── BEGIN BLOB checkpoints@1.2.0 sha256:a44560c6036fd143b7a3f125fec00fa8b91c3c06ac5b9e1ecfb83d27a5211e9b src=packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts ──
188
188
  const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];
189
189
  const STAGE_ARTIFACTS = {
190
190
  router: '00_complexity_assessment.md',
@@ -347,7 +347,7 @@ function parseArtifactProbe(opts) {
347
347
  return null;
348
348
  return found;
349
349
  }
350
- // ── END BLOB checkpoints@1.1.0 ──
350
+ // ── END BLOB checkpoints@1.2.0 ──
351
351
  // glue (workflow-owned, hand-reviewed): legacy mirror names → the blob exports. The hash
352
352
  // composition is UNCHANGED (same CKPT_SCHEMA_VERSION salt + JSON-tuple), so recorded
353
353
  // checkpoints from pre-wrap runs still resume.
@@ -463,7 +463,7 @@ async function withCheckpoint(stage, phaseName, inputHash, runFn, ckptOpts) {
463
463
  // via the reqe-proven sed idiom (the sandbox forbids Date; the pure half takes ts as a parameter).
464
464
  const CAPTURE_PAIRS = A.captureTrainingPairs !== false
465
465
  const captureFailures = []
466
- const TP_SCHEMA = 'fa-trainpair-2'
466
+ const TP_SCHEMA = 'fa-trainpair-3'
467
467
  const TP_MAX_IO = 48000
468
468
  const TP_DIR = REPO + '/.dz/fa-training/' + SLUG
469
469
  const TP_MARK_DIR = REPO + '/.dz/fa-training/.backfill-marks'
@@ -505,6 +505,26 @@ function captureFailureRecord(stage, mode, reason, detail) {
505
505
  }
506
506
  function tpFamily(spec) { return /codex|gpt|openai/i.test(String(spec == null ? '' : spec)) ? 'codex' : 'claude' }
507
507
  function tpText(v) { if (typeof v === 'string') return v; if (v === null || v === undefined) return ''; try { const s = JSON.stringify(v); return typeof s === 'string' ? s : String(v) } catch (e) { return String(v) } }
508
+ function tpBudget(raw) {
509
+ try {
510
+ if (raw === undefined) return { primary: 'claude', claude: 'normal', codex: 'normal', preset: 'unset' }
511
+ if (raw === null || typeof raw !== 'object') return null
512
+ const primary = raw.primary
513
+ const claude = raw.claude
514
+ const codex = raw.codex
515
+ if (primary !== 'claude' && primary !== 'codex') return null
516
+ if (claude !== 'normal' && claude !== 'eco') return null
517
+ if (codex !== 'normal' && codex !== 'eco') return null
518
+ if (raw.preset === 'unset') return { primary: primary, claude: claude, codex: codex, preset: 'unset' }
519
+ let preset = 'custom'
520
+ if (claude === 'normal' && codex === 'normal') preset = 'normal'
521
+ else if (claude === 'eco' && codex === 'eco') preset = 'eco'
522
+ else if (claude === 'eco' && codex === 'normal') preset = 'hybrid'
523
+ return { primary: primary, claude: claude, codex: codex, preset: preset }
524
+ } catch (e) {
525
+ return null
526
+ }
527
+ }
508
528
  // Operator-profile redaction (mirror of harness-core redactProfileBlock — ADR-001 Decision 5 /
509
529
  // CF-6 of operator-profile). The PERSIST seam in `dz feature-adr-record` redacts independently;
510
530
  // this mirror exists so the truncation fnv1a64 hashes below are hashes of the REDACTED text and
@@ -526,7 +546,7 @@ function tpRedact(text) {
526
546
  rest = rest.slice(end + TP_PROFILE_END.length)
527
547
  }
528
548
  }
529
- function buildTrainingPair(slug, stage, ts, inputRaw, outputRaw, evaluation, provenance, captureMode, resumed) {
549
+ function buildTrainingPair(slug, stage, ts, inputRaw, outputRaw, evaluation, provenance, budgetModeRaw, captureMode, resumed) {
530
550
  // Redaction FIRST, before the oversize guard — same order as the core builder.
531
551
  let input = tpRedact(tpText(inputRaw))
532
552
  let output = tpRedact(tpText(outputRaw))
@@ -559,6 +579,7 @@ function buildTrainingPair(slug, stage, ts, inputRaw, outputRaw, evaluation, pro
559
579
  tokens: (typeof pv.tokens === 'number' && isFinite(pv.tokens)) ? pv.tokens : null,
560
580
  minutes: (typeof pv.minutes === 'number' && isFinite(pv.minutes)) ? pv.minutes : null,
561
581
  },
582
+ budgetMode: tpBudget(budgetModeRaw),
562
583
  truncated: truncated,
563
584
  captureMode: captureMode === 'backfill' ? 'backfill' : 'capture',
564
585
  resumed: resumed === true,
@@ -582,8 +603,9 @@ async function capturePairs(stage, phaseName, records, resumeGuardStage) {
582
603
  mode = decideCaptureMode({ enabled: CAPTURE_PAIRS, resumed: resumed, recordCount: filteredRecords.length })
583
604
  if (mode === 'skip-disabled' || mode === 'skip-empty') return
584
605
  const lines = []
606
+ const budgetModeInput = { primary: PRIMARY, claude: BUDGET_MODE.claude, codex: BUDGET_MODE.codex, preset: A.budget === undefined ? 'unset' : undefined }
585
607
  for (const r of filteredRecords) {
586
- lines.push(JSON.stringify(buildTrainingPair(SLUG, stage, null, r.input, r.output, r.evaluation, r.provenance, mode === 'backfill' ? 'backfill' : 'capture', resumed === true)))
608
+ lines.push(JSON.stringify(buildTrainingPair(SLUG, stage, null, r.input, r.output, r.evaluation, r.provenance, budgetModeInput, mode === 'backfill' ? 'backfill' : 'capture', resumed === true)))
587
609
  }
588
610
  const file = TP_DIR + '/' + stage + '.jsonl'
589
611
  const markStage = String(stage).replace(/\.\./g, '_').replace(/\//g, '_')
@@ -715,8 +737,8 @@ const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
715
737
  // LOAD-BEARING: when args.models.qe is unset the QE stage is auto-routed to the OTHER family than the
716
738
  // coder (a model that codes must not also self-QE). BACKWARD-COMPATIBLE: omitting args.models AND the
717
739
  // legacy knobs ⇒ routingRequested is false ⇒ every stage resolves to {} ⇒ byte-identical to today.
718
- // Precedence: args.models[stage] > legacy planner/coder/qeReviewer/codexModel knobs > DEFAULT_MODELS.
719
- // gpt-5.6-ready: adding a codex id is a DATA-ONLY edit to KNOWN_CODEX. This block is the parser-safe
740
+ // Precedence: args.models[stage] > legacy planner/coder/qeReviewer/codexModel knobs > primary/budget defaults.
741
+ // KNOWN_CODEX is spellability only; capability selection uses explicit CODEX_TIERS and a live pre-run probe.
720
742
  // (string concat, explicit if/return, object-literal tables — NO template literals, NO inline ?:agent())
721
743
  // mirror of src/feature-adr-routing.ts; keep the two in lock-step (a drift test asserts it).
722
744
  // stage 2 pending (loop-designer M10/ADR-004 D4): still a hand-maintained mirror — the routing
@@ -724,16 +746,24 @@ const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
724
746
  // regeneration via model-resolver/usage-probes/codex-dispatch blobs is the tracked Stage-B item.
725
747
  const MODELS = (A.models && typeof A.models === 'object') ? A.models : {}
726
748
  const KNOWN_CODEX = { 'auto': 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 }
749
+ // The allowlist is not an availability check — probe every id before every run; ids drift in both directions.
750
+ const CODEX_TIERS = { flagship: 'gpt-5.6-sol', workhorse: 'gpt-5.6-terra', 'high-volume': 'gpt-5.6-luna' }
727
751
  const CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 }
728
- const VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1 }
752
+ const VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1, max: 1 }
729
753
  const DEFAULT_MODELS = { router: 'fable', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: null, qe: null, fleet: 'sonnet' }
730
- const routingRequested = (Object.keys(MODELS).length > 0) || (PLANNER === 'codex') || (CODER === 'codex' || CODER === 'codex-fallback') || (QE_REVIEWER === 'codex' || QE_REVIEWER === 'codex-fallback') || (A.usageAdaptive === true)
754
+ const BUDGET_PRESETS = { normal: { claude: 'normal', codex: 'normal' }, eco: { claude: 'eco', codex: 'eco' }, hybrid: { claude: 'eco', codex: 'normal' } }
755
+ const ROUTING_TABLES = { claude: { claude: { normal: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'fable', ideation: 'sonnet', ddd: 'fable', architecture: 'fable', plan: 'opus', code: 'sonnet', fleet: 'sonnet' }, eco: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: 'sonnet', fleet: 'sonnet' } }, codex: { normal: {}, eco: {} } }, codex: { claude: { normal: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' }, eco: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' } }, codex: { normal: {}, eco: {} } } }
756
+ const STAGE_EFFORT = { override: { router: 'medium', requirements: 'medium', research: 'medium', adr: 'high', ideation: 'medium', ddd: 'high', architecture: 'high', plan: 'high', code: 'medium', qe: 'high', fleet: 'medium' } }
757
+ const BUDGET_MODE = resolveBudgetMode(A.budget)
758
+ const PRIMARY = (A.primary === 'codex') ? 'codex' : 'claude'
759
+ const routingRequested = (Object.keys(MODELS).length > 0) || (A.primary !== undefined) || (A.budget !== undefined) || (PLANNER === 'codex') || (CODER === 'codex' || CODER === 'codex-fallback') || (QE_REVIEWER === 'codex' || QE_REVIEWER === 'codex-fallback') || (A.usageAdaptive === true)
731
760
  const modelsUsed = {}
732
761
 
733
762
  // ── USAGE-ADAPTIVE ROUTING (pre-emptive codex switch at >= usageThreshold, default 70%) ──
734
763
  // At every phase boundary a minimal haiku probe runs 'dz usage --json'; when SESSION or WEEKLY
735
764
  // usage crosses the threshold BEFORE a phase launches, ALL remaining stages switch to
736
- // codex:<topCodexId> (design/code/plan at xhigh, router/qe/fleet at high). When a later probe reads
765
+ // codex:<topCodexId> using STAGE_EFFORT (official medium default; high only where the table names it).
766
+ // When a later probe reads
737
767
  // BOTH metrics below threshold (positive numbers, not nulls) the normal mix is RESTORED. The
738
768
  // load-bearing asymmetry: an agent-null probe (dispatch died — often MEANS limits) fail-safe-switches
739
769
  // TO codex; a value-null (unconfigured limits) flips NOTHING. All additive behind USAGE_ADAPTIVE:
@@ -742,7 +772,7 @@ const modelsUsed = {}
742
772
  // the pure library mirror (feature-adr-routing.ts) threads it via RoutingEnv — never a global.
743
773
  const USAGE_THRESHOLD = Number(A.usageThreshold) > 0 ? Number(A.usageThreshold) : 70
744
774
  const USAGE_ADAPTIVE = (A.usageAdaptive !== false) && (routingRequested || A.usageAdaptive === true)
745
- const OVERRIDE_REASONING = mergeOpts({ router: 'high', requirements: 'xhigh', research: 'xhigh', adr: 'xhigh', ideation: 'xhigh', ddd: 'xhigh', architecture: 'xhigh', plan: 'xhigh', code: 'xhigh', qe: 'high', fleet: 'high' }, (A.usageReasoning && typeof A.usageReasoning === 'object') ? A.usageReasoning : {})
775
+ const OVERRIDE_REASONING = mergeOpts(STAGE_EFFORT.override, (A.usageReasoning && typeof A.usageReasoning === 'object') ? A.usageReasoning : {})
746
776
  const usageReasoning = OVERRIDE_REASONING
747
777
  let usageOverride = false
748
778
  const usageEvents = []
@@ -804,7 +834,7 @@ function specToOpts(spec) {
804
834
  let id = parts[1] || CODEX_MODEL
805
835
  if (id !== 'auto' && !KNOWN_CODEX[id]) { log('models: unknown codex id ' + id + ' — using ' + CODEX_MODEL); id = CODEX_MODEL }
806
836
  let reasoning = parts[2] || 'high'
807
- if (!VALID_REASONING[reasoning]) { log('models: unknown reasoning ' + reasoning + ' — using high'); reasoning = 'high' }
837
+ if (!VALID_REASONING[reasoning]) throw new RangeError('models: invalid reasoning "' + reasoning + '"valid: ' + Object.keys(VALID_REASONING).join('|'))
808
838
  return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning }
809
839
  }
810
840
  if (CLAUDE_NAMES[parts[0]]) return { model: parts[0] }
@@ -812,32 +842,94 @@ function specToOpts(spec) {
812
842
  return {}
813
843
  }
814
844
 
845
+ function codexIdForTier(tier) {
846
+ return CODEX_MODEL !== 'auto' ? CODEX_MODEL : CODEX_TIERS[tier]
847
+ }
848
+
849
+ function resolveBudgetMode(raw) {
850
+ if (raw === undefined) return BUDGET_PRESETS.normal
851
+ if (typeof raw === 'string') {
852
+ const preset = BUDGET_PRESETS[raw]
853
+ if (!preset) throw new RangeError('budget: unknown preset "' + raw + '" — valid: normal|eco|hybrid')
854
+ return preset
855
+ }
856
+ if (raw && typeof raw === 'object') {
857
+ for (const key of Object.keys(raw)) {
858
+ if (key !== 'claude' && key !== 'codex') throw new RangeError('budget: unknown family key "' + key + '" — valid: claude|codex')
859
+ }
860
+ for (const key of ['claude', 'codex']) {
861
+ const level = raw[key]
862
+ if (level !== undefined && level !== 'normal' && level !== 'eco') throw new RangeError('budget.' + key + ': unknown level "' + level + '" — valid: normal|eco')
863
+ }
864
+ return { claude: raw.claude || 'normal', codex: raw.codex || 'normal' }
865
+ }
866
+ throw new RangeError('budget: expected a preset name or {claude,codex} object, got ' + typeof raw)
867
+ }
868
+
869
+ function budgetPresetName(axis) {
870
+ for (const name of ['normal', 'eco', 'hybrid']) {
871
+ const preset = BUDGET_PRESETS[name]
872
+ if (preset.claude === axis.claude && preset.codex === axis.codex) return name
873
+ }
874
+ return null
875
+ }
876
+
877
+ function budgetTable(primary, mode) {
878
+ const claudeHalf = ROUTING_TABLES[primary].claude[mode.claude]
879
+ let codexHalf
880
+ if (primary === 'claude') {
881
+ const qeSpec = mode.codex === 'normal' ? 'codex:' + codexIdForTier('flagship') + ':high' : 'codex:' + codexIdForTier('workhorse') + ':medium'
882
+ codexHalf = { ...ROUTING_TABLES.claude.codex[mode.codex], qe: A.codexAvailable === false ? 'opus' : qeSpec }
883
+ } else {
884
+ const normal = mode.codex === 'normal'
885
+ const id = codexIdForTier(normal ? 'flagship' : 'workhorse')
886
+ const design = 'codex:' + id + ':' + (normal ? 'high' : 'medium')
887
+ codexHalf = { requirements: design, research: design, adr: design, ideation: design, ddd: design, architecture: design, plan: 'codex:' + id + ':' + (normal ? 'high' : 'low'), code: 'codex:' + id + ':medium' }
888
+ }
889
+ return { ...claudeHalf, ...codexHalf }
890
+ }
891
+
892
+ // qe2 is outside the 11 canonical stages: A-normal adds an independent Opus precision pass for L/XL.
893
+ function qePrecisionPassSpec(primary, budget, tier) {
894
+ if (primary !== 'codex') return null
895
+ if (budget.claude !== 'normal') return null
896
+ if (tier !== 'L' && tier !== 'XL') return null
897
+ return 'opus'
898
+ }
899
+
900
+ function assertCrossFamilyQe(codeSpec, qeSpec) {
901
+ const fam = function (s) { return (s && String(s).split(':')[0] === 'codex') ? 'codex' : 'claude' }
902
+ if (fam(codeSpec) === fam(qeSpec)) throw new Error('cross-family QE violated: code=' + codeSpec + ' qe=' + qeSpec)
903
+ }
904
+
815
905
  function resolveCoderSpec() {
816
906
  if (CODER === 'codex' || CODER === 'codex-fallback') return 'codex:' + CODEX_MODEL + ':high'
817
907
  return 'opus'
818
908
  }
819
909
 
820
910
  function coderIsCodex() {
821
- if (CODER === 'codex' || CODER === 'codex-fallback') return true
822
911
  const codeSpec = MODELS.code
823
- if (codeSpec && String(codeSpec).split(':')[0] === 'codex') return true
824
- return false
912
+ if (codeSpec !== undefined && codeSpec !== null) return String(codeSpec).split(':')[0] === 'codex'
913
+ if (CODER === 'codex' || CODER === 'codex-fallback') return true
914
+ return A.primary === 'codex'
825
915
  }
826
916
 
827
917
  function topCodexId() {
828
- let top = CODEX_MODEL
829
- if (top === 'auto') {
830
- const ids = Object.keys(KNOWN_CODEX)
831
- for (let i = 0; i < ids.length; i++) { if (ids[i] !== 'auto') top = ids[i] || top }
832
- }
833
- return top
918
+ return CODEX_MODEL !== 'auto' ? CODEX_MODEL : CODEX_TIERS.flagship
834
919
  }
835
920
 
836
- function resolveQeSpec() {
837
- if (coderIsCodex()) return 'opus'
921
+ function resolveQeSpecForCoder(coderCodex) {
922
+ if (coderCodex) return 'sonnet'
838
923
  const CODEX_AVAILABLE = A.codexAvailable !== false
839
924
  if (!CODEX_AVAILABLE) return 'opus'
840
- return 'codex:' + topCodexId() + ':high'
925
+ const budget = BUDGET_MODE
926
+ return budget.codex === 'eco'
927
+ ? 'codex:' + codexIdForTier('workhorse') + ':medium'
928
+ : 'codex:' + codexIdForTier('flagship') + ':high'
929
+ }
930
+
931
+ function resolveQeSpec() {
932
+ return resolveQeSpecForCoder(coderIsCodex())
841
933
  }
842
934
 
843
935
  // qeShouldUseCodex: the load-bearing cross-model gate — the model that wrote the code must NEVER self-QE.
@@ -854,7 +946,7 @@ function qeShouldUseCodex() {
854
946
 
855
947
  function resolveStageModel(stage) {
856
948
  if (usageOverride) {
857
- const r = (usageReasoning && usageReasoning[stage]) || OVERRIDE_REASONING[stage] || 'high'
949
+ const r = (usageReasoning && usageReasoning[stage]) || STAGE_EFFORT.override[stage] || 'medium'
858
950
  const o = specToOpts('codex:' + topCodexId() + ':' + r)
859
951
  o._usageSwitched = true
860
952
  return o
@@ -862,7 +954,17 @@ function resolveStageModel(stage) {
862
954
  let spec = MODELS[stage]
863
955
  if (spec === undefined) {
864
956
  if (!routingRequested) return {}
865
- spec = DEFAULT_MODELS[stage]
957
+ if (stage === 'code' && (CODER === 'codex' || CODER === 'codex-fallback')) {
958
+ return specToOpts(resolveCoderSpec())
959
+ }
960
+ if (stage === 'plan' && PLANNER === 'codex') {
961
+ return specToOpts('codex:' + CODEX_MODEL + ':high')
962
+ }
963
+ if (stage === 'qe') {
964
+ return specToOpts(resolveQeSpec())
965
+ }
966
+ const cell = budgetTable(PRIMARY, BUDGET_MODE)[stage]
967
+ spec = cell !== undefined ? cell : DEFAULT_MODELS[stage]
866
968
  }
867
969
  if (stage === 'code' && (spec === null || spec === undefined)) return specToOpts(resolveCoderSpec())
868
970
  if (stage === 'qe' && (spec === null || spec === undefined)) return specToOpts(resolveQeSpec())
@@ -1020,11 +1122,13 @@ function codexExecCommand(input) {
1020
1122
  const o = input || {}
1021
1123
  const modelId = String(o.modelId === undefined || o.modelId === null ? '' : o.modelId)
1022
1124
  if (!isSafeCodexId(modelId)) return null
1125
+ const reasoning = (o.reasoning === undefined || o.reasoning === null || o.reasoning === '') ? 'high' : String(o.reasoning)
1126
+ if (!VALID_REASONING[reasoning]) return null
1023
1127
  const raw = Number(o.timeoutSeconds)
1024
1128
  const seconds = (raw === raw && raw !== Infinity && raw > 0) ? Math.floor(raw) : CODEX_EXEC_TIMEOUT_SECONDS
1025
1129
  const repo = String(o.repo === undefined || o.repo === null ? '' : o.repo)
1026
1130
  const cd = repo === '' ? '' : ' -C ' + codexSq(repo)
1027
- return timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + ' codex exec' + cd + ' -m ' + codexSq(modelId) + ' ' + codexSq(String(o.prompt === undefined || o.prompt === null ? '' : o.prompt)) + ' < /dev/null'
1131
+ return timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + ' codex exec' + cd + ' -m ' + codexSq(modelId) + ' -c model_reasoning_effort=' + codexSq(reasoning) + ' ' + codexSq(String(o.prompt === undefined || o.prompt === null ? '' : o.prompt)) + ' < /dev/null'
1028
1132
  }
1029
1133
 
1030
1134
  function codexReviewCommand(input) {
@@ -1268,7 +1372,14 @@ function isAgentTypeMissingError(err) {
1268
1372
  // CX-3: a workflow naming an agent type the harness lacks must FALL BACK, not die. Any other error
1269
1373
  // still propagates — we do not hide real bugs behind a fallback.
1270
1374
  async function safeCodexAgent(prompt, opts) {
1271
- try { return await agent(prompt, opts) }
1375
+ let dispatchOpts = opts
1376
+ if (opts && opts.agentType === 'codex:codex-rescue') {
1377
+ const requestedId = opts.codexModel !== 'auto' ? opts.codexModel : null
1378
+ const probed = await probeCodexId(requestedId)
1379
+ if (!probed) return null
1380
+ dispatchOpts = mergeOpts(opts, { codexModel: probed })
1381
+ }
1382
+ try { return await agent(prompt, dispatchOpts) }
1272
1383
  catch (err) {
1273
1384
  if (isAgentTypeMissingError(err)) { log('codex: agent type unavailable — falling back to Claude (' + String(err) + ')'); return null }
1274
1385
  throw err
@@ -1276,7 +1387,7 @@ async function safeCodexAgent(prompt, opts) {
1276
1387
  }
1277
1388
 
1278
1389
  // CX-1: the allowlist says a name is spellable; only a probe says it answers. One probe per run.
1279
- let _probedCodexId
1390
+ const _probedCodexIds = {}
1280
1391
  // Which timeout binary this machine actually has. ONE cached shell probe, run before either
1281
1392
  // dispatch mode spends a model call.
1282
1393
  //
@@ -1296,24 +1407,26 @@ async function probeTimeoutBin() {
1296
1407
  return _timeoutBin
1297
1408
  }
1298
1409
 
1299
- async function probeCodexId() {
1300
- if (_probedCodexId !== undefined) return _probedCodexId
1301
- const raw = (CODEX_MODEL && CODEX_MODEL !== 'auto') ? [CODEX_MODEL, 'gpt-5.6-sol', 'gpt-5.5'] : ['gpt-5.6-sol', 'gpt-5.5']
1410
+ async function probeCodexId(requestedId) {
1411
+ const requested = isSafeCodexId(requestedId) ? String(requestedId) : null
1412
+ const raw = requested ? [requested] : ((CODEX_MODEL && CODEX_MODEL !== 'auto') ? [CODEX_MODEL, CODEX_TIERS.flagship, 'gpt-5.5'] : [CODEX_TIERS.flagship, 'gpt-5.5'])
1413
+ const cacheKey = raw.join('|')
1414
+ if (Object.prototype.hasOwnProperty.call(_probedCodexIds, cacheKey)) return _probedCodexIds[cacheKey]
1302
1415
  const ids = raw.filter(isSafeCodexId)
1303
1416
  for (const id of ids) {
1304
1417
  // The probe is built with the SAME binary the real dispatch will use, so a machine that cannot
1305
1418
  // bound a run fails here rather than mid-QE.
1306
1419
  const tbin = await probeTimeoutBin()
1307
- if (tbin === null) { _probedCodexId = null; return null }
1420
+ if (tbin === null) { _probedCodexIds[cacheKey] = null; return null }
1308
1421
  const cmd = codexProbeCommand(id, tbin)
1309
1422
  if (!cmd) { log('codex: refusing unsafe model id ' + id); continue }
1310
1423
  const out = await agent('Run EXACTLY this via Bash and reply with its stdout only: ' + cmd + ' — if it fails or times out reply with exactly ' + CODEX_UNAVAILABLE, { label: 'probe:' + id, phase: 'Route', model: 'haiku', effort: 'low' })
1311
1424
  if (out && /\bOK\b/.test(String(out)) && String(out).indexOf(CODEX_UNAVAILABLE) === -1) {
1312
- log('codex: probed ' + id + ' — available'); _probedCodexId = id; return id
1425
+ log('codex: probed ' + id + ' — available'); _probedCodexIds[cacheKey] = id; return id
1313
1426
  }
1314
1427
  log('codex: probed ' + id + ' — NOT available')
1315
1428
  }
1316
- _probedCodexId = null
1429
+ _probedCodexIds[cacheKey] = null
1317
1430
  return null
1318
1431
  }
1319
1432
 
@@ -1350,9 +1463,9 @@ function codexQeSignalCommand(inner, outPath) {
1350
1463
  // Shared tail of both dispatch modes: run the signal-wrapped command through a shell agent and
1351
1464
  // CLASSIFY what came back. signalExpected is true here — on the pipeline path a swallowed sentinel
1352
1465
  // means the command did not demonstrably run, which is a tool-error, never a pass.
1353
- async function runCodexQeCommand(stage, cmd, phaseName, label, probed, mode, scopeRef, files, allowStatedGrade) {
1466
+ async function runCodexQeCommand(stage, cmd, phaseName, label, probed, mode, scopeRef, files, allowStatedGrade, requestedReasoning) {
1354
1467
  const wrapped = 'Run EXACTLY this via Bash and reply with its stdout VERBATIM and nothing else, INCLUDING the final ' + CODEX_QE_SIGNAL_PREFIX + ' line (it is a machine signal, not prose — do not summarise, reformat or omit it). Only if you cannot run the command AT ALL (no shell, command not found) reply with exactly ' + CODEX_UNAVAILABLE + '; a timeout is NOT that case, it reports itself in the signal line.\n\n' + codexQeSignalCommand(cmd, '/tmp/dz-codex-qe-' + SLUG + '-' + stage + '-' + mode + '.out')
1355
- const raw = await agent(wrapped, { label: stageLabel(label, { agentType: 'codex:codex-rescue', codexModel: probed, _reasoning: 'high' }), phase: phaseName, model: 'haiku', effort: 'low' })
1468
+ const raw = await agent(wrapped, { label: stageLabel(label, { agentType: 'codex:codex-rescue', codexModel: probed, _reasoning: requestedReasoning || 'high' }), phase: phaseName, model: 'haiku', effort: 'low' })
1356
1469
  const sig = parseCodexReviewSignal(raw === null ? '' : String(raw))
1357
1470
  const findings = parseCodexReviewFindings(sig.body)
1358
1471
  // Mode A NEVER asked for a letter (every scope flag rejects a prompt), so any "Grade: X" in its
@@ -1373,28 +1486,32 @@ async function runCodexQeCommand(stage, cmd, phaseName, label, probed, mode, sco
1373
1486
  // MODE A — the primary pass. codex review derives the review scope FROM THE DIFF, which is exactly
1374
1487
  // the reconnaissance we were paying a model to do badly (MEASURED: 146s with a verdict, against 280s
1375
1488
  // and exit 124 without one). It cannot carry our questions: every scope flag refuses [PROMPT].
1376
- async function codexReviewAgent(stage, scope, scopeRef, phaseName) {
1489
+ async function codexReviewAgent(stage, scope, scopeRef, phaseName, requestedOpts) {
1377
1490
  lastCodexDecline = null
1378
- const probed = await probeCodexId()
1491
+ const requestedId = requestedOpts && requestedOpts.codexModel !== 'auto' ? requestedOpts.codexModel : null
1492
+ const requestedReasoning = (requestedOpts && requestedOpts._reasoning) || 'high'
1493
+ const probed = await probeCodexId(requestedId)
1379
1494
  if (!probed) return noteCodexDecline(stage, 'unavailable', { reason: 'no codex model id answered the probe' })
1380
- const built = codexReviewCommand({ scope: scope, ref: scopeRef, modelId: probed, reasoning: 'high', timeoutSeconds: CODEX_REVIEW_TIMEOUT_SECONDS, timeoutBin: await probeTimeoutBin(), repo: REPO })
1495
+ const built = codexReviewCommand({ scope: scope, ref: scopeRef, modelId: probed, reasoning: requestedReasoning, timeoutSeconds: CODEX_REVIEW_TIMEOUT_SECONDS, timeoutBin: await probeTimeoutBin(), repo: REPO })
1381
1496
  if (built.cmd === null) return noteCodexDecline(stage, 'tool-error', { exit: 2, detail: built.reason })
1382
- return await runCodexQeCommand(stage, built.cmd, phaseName, stage + ':codex-review', probed, 'A', built.scope + (scopeRef ? ' ' + scopeRef : ''), [], false)
1497
+ return await runCodexQeCommand(stage, built.cmd, phaseName, stage + ':codex-review', probed, 'A', built.scope + (scopeRef ? ' ' + scopeRef : ''), [], false, requestedReasoning)
1383
1498
  }
1384
1499
 
1385
1500
  // MODE B — the narrowed follow-up. Carries OUR questions over files we name, and is refused outright
1386
1501
  // when the prompt was not built by scopedQePrompt (see codexExecPlan).
1387
- async function codexExecAgent(stage, prompt, phaseName, scoped, files) {
1502
+ async function codexExecAgent(stage, prompt, phaseName, scoped, files, requestedOpts) {
1388
1503
  lastCodexDecline = null
1389
- const probed = await probeCodexId()
1504
+ const requestedId = requestedOpts && requestedOpts.codexModel !== 'auto' ? requestedOpts.codexModel : null
1505
+ const requestedReasoning = (requestedOpts && requestedOpts._reasoning) || 'high'
1506
+ const probed = await probeCodexId(requestedId)
1390
1507
  const plan = codexExecPlan(stage, prompt.length, probed, scoped)
1391
1508
  if (plan.mode !== 'exec') return noteCodexDecline(stage, 'unavailable', { reason: plan.reason })
1392
1509
  // Was JSON.stringify(...) — DOUBLE quotes, in which the shell still expands a command substitution, and the
1393
1510
  // prompt carries the user's own feature description. Single-quoted through codexSq closes that
1394
1511
  // as a side effect of pinning the working directory.
1395
- const inner = codexExecCommand({ modelId: probed, prompt: prompt, timeoutBin: await probeTimeoutBin(), timeoutSeconds: CODEX_EXEC_TIMEOUT_SECONDS, repo: REPO })
1512
+ const inner = codexExecCommand({ modelId: probed, reasoning: requestedReasoning, prompt: prompt, timeoutBin: await probeTimeoutBin(), timeoutSeconds: CODEX_EXEC_TIMEOUT_SECONDS, repo: REPO })
1396
1513
  if (inner === null) return noteCodexDecline(stage, 'unavailable', { reason: 'unsafe codex id ' + String(probed) })
1397
- return await runCodexQeCommand(stage, inner, phaseName, stage + ':codex-exec', probed, 'B', 'declared-targets(' + (files || []).length + ' declared, <=' + SCOPED_QE_MAX_FILES + ' reviewed)', files || [], true)
1514
+ return await runCodexQeCommand(stage, inner, phaseName, stage + ':codex-exec', probed, 'B', 'declared-targets(' + (files || []).length + ' declared, <=' + SCOPED_QE_MAX_FILES + ' reviewed)', files || [], true, requestedReasoning)
1398
1515
  }
1399
1516
 
1400
1517
  // Widened 2026-08-28 (MEASURED — see feature-adr-routing.ts for the reproducer): slop-lint's coder
@@ -2053,7 +2170,7 @@ async function designStage(promptText, opts, artifactPath, baseLabel) {
2053
2170
  if (!needsLandedBarrier(opts)) return await agent(promptText, opts)
2054
2171
  const codexOpts = {}
2055
2172
  for (const k in opts) if (k !== 'schema') codexOpts[k] = opts[k]
2056
- const res = await agent(promptText + codexEffortHint(codexOpts) + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until the file is fully written to disk.', codexOpts)
2173
+ const res = await safeCodexAgent(promptText + codexEffortHint(codexOpts) + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until the file is fully written to disk.', codexOpts)
2057
2174
  const probe = await agent('Confirm a Codex OUT-OF-BAND artifact write has LANDED before the next stage reads it. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + landedProbeCmd(artifactPath), { label: 'design:confirm-landed', phase: 'Design', effort: 'low' })
2058
2175
  if (res && probe && /landed=/.test(String(probe))) return { wrote: [artifactPath], summary: String(res).slice(0, 300) }
2059
2176
  log('design artifact did not land on codex (' + artifactPath + ') — falling back to Claude')
@@ -2138,7 +2255,7 @@ modelsUsed.router = modelLabel(routerOpts)
2138
2255
  // router checkpoint: its result (tier + recalled-pattern rationale) seeds every downstream hash.
2139
2256
  // The Step-0 RECALL is part of the stage — a resumed router restores the SAME recalled patterns the
2140
2257
  // original run applied (fresh lessons taught since then enter on the next live run, not mid-resume).
2141
- const routerHash = ckptHash('router', [DESC, CODE_HINT, MODE, A.tier === undefined ? null : A.tier, BRAIN, MODELS.router === undefined ? null : MODELS.router, CODEX_MODEL, usageOverride, ROUTER_CONTRACT_TOKEN])
2258
+ const routerHash = ckptHash('router', [DESC, CODE_HINT, MODE, A.tier === undefined ? null : A.tier, BRAIN, MODELS.router === undefined ? null : MODELS.router, CODEX_MODEL, PRIMARY, BUDGET_MODE, usageOverride, ROUTER_CONTRACT_TOKEN])
2142
2259
  const router = await withCheckpoint('router', 'Router', routerHash, async () => agent(routerPrompt + codexEffortHint(routerOpts), routerOpts))
2143
2260
  if (resumedStages.indexOf('router') !== -1) modelsUsed.router = modelsUsed.router + ' (resumed)'
2144
2261
  let tier = A.tier || (router ? router.tier : 'M')
@@ -2283,7 +2400,7 @@ if (isMplus) {
2283
2400
  // patterns, the project-skills manifest state, the usage override, and CODEX_MODEL (a bare 'codex'
2284
2401
  // model spec resolves through it). The artifact probe requires EVERY tier-active design artifact
2285
2402
  // (Codex QE #2 — a one-file probe accepted a design missing its ADR/architecture).
2286
- const designHash = ckptHash('design', [tier, DESC, CODE_HINT, fnv1a64(String(LEARNED)), MODELS.requirements === undefined ? null : MODELS.requirements, MODELS.adr === undefined ? null : MODELS.adr, MODELS.ideation === undefined ? null : MODELS.ideation, MODELS.architecture === undefined ? null : MODELS.architecture, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, CODEX_MODEL])
2403
+ const designHash = ckptHash('design', [tier, DESC, CODE_HINT, fnv1a64(String(LEARNED)), MODELS.requirements === undefined ? null : MODELS.requirements, MODELS.adr === undefined ? null : MODELS.adr, MODELS.ideation === undefined ? null : MODELS.ideation, MODELS.architecture === undefined ? null : MODELS.architecture, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, CODEX_MODEL, PRIMARY, BUDGET_MODE])
2287
2404
  const designArtifacts = ['01_requirements.md']
2288
2405
  if (isMplus) designArtifacts.push('03_adr/', '03.5_ideation_report.md', '05_architecture.md')
2289
2406
  if (isLplus) designArtifacts.push('02_research.md', '04_domain_model.md')
@@ -2441,7 +2558,7 @@ const planIsCodex = (planModel.agentType === 'codex:codex-rescue') || (MODELS.pl
2441
2558
  // plan checkpoint: keyed on the design fan's RESULT (a stale design invalidates the plan) + the
2442
2559
  // planner spec. Covers the standard L/XL two-phase flow: the stop-after-plan re-invoke resumes
2443
2560
  // router+design+plan instead of re-running them.
2444
- const planHash = ckptHash('plan', [tier, DESC, fnv1a64(JSON.stringify(design === undefined ? null : design)), PLANNER, MODELS.plan === undefined ? null : MODELS.plan, CODEX_MODEL, usageOverride])
2561
+ const planHash = ckptHash('plan', [tier, DESC, fnv1a64(JSON.stringify(design === undefined ? null : design)), PLANNER, MODELS.plan === undefined ? null : MODELS.plan, CODEX_MODEL, PRIMARY, BUDGET_MODE, usageOverride])
2445
2562
  let plan = await withCheckpoint('plan', 'Plan', planHash, async () => {
2446
2563
  let plan = null
2447
2564
  if (planIsCodex) {
@@ -2653,7 +2770,7 @@ const codePrompt = 'Step 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG +
2653
2770
  // code:null sentinel → resolveCoderSpec(). A Claude resolution merges {model} onto the Claude branch;
2654
2771
  // under the BC omit-path it is {} (byte-identical).
2655
2772
  const codeModel = resolveStageModel('code')
2656
- const codeIsCodexFirst = (MODELS.code !== undefined) ? (codeModel.agentType === 'codex:codex-rescue') : (CODER === 'codex')
2773
+ const codeIsCodexFirst = (codeModel.agentType === 'codex:codex-rescue') && (MODELS.code !== undefined || CODER !== 'codex-fallback')
2657
2774
  const codeClaudeModel = codeIsCodexFirst ? {} : (codeModel.agentType ? {} : codeModel)
2658
2775
  const codeClaudeOpts = mergeOpts({ label: stageLabel('code', codeClaudeModel), phase: 'Code', schema: ARTIFACT, effort: 'high' }, codeClaudeModel)
2659
2776
  // code checkpoint: COMPOSITE — the branchy claude/codex/fallback block sets three interdependent
@@ -2662,7 +2779,7 @@ const codeClaudeOpts = mergeOpts({ label: stageLabel('code', codeClaudeModel), p
2662
2779
  // the checkpoint (it only feeds the expected-targets parse, already consumed by the original run).
2663
2780
  // R6: the landing token is salted into the code stage's PARTS (not CKPT_SCHEMA_VERSION, which
2664
2781
  // stays 'fa-ckpt-2' deliberately) so ONLY this stage's pre-protocol checkpoints hash stale.
2665
- const codeHash = ckptHash('code', [tier, DESC, fnv1a64(JSON.stringify(plan === undefined ? null : plan)), CODER, MODELS.code === undefined ? null : MODELS.code, CODEX_MODEL, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, LANDING_HASH_TOKEN])
2782
+ const codeHash = ckptHash('code', [tier, DESC, fnv1a64(JSON.stringify(plan === undefined ? null : plan)), CODER, MODELS.code === undefined ? null : MODELS.code, CODEX_MODEL, PRIMARY, BUDGET_MODE, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, LANDING_HASH_TOKEN])
2666
2783
  const codeStage = await withCheckpoint('code', 'Code', codeHash, async () => {
2667
2784
  let code = null
2668
2785
  let coderUsed = 'claude'
@@ -2818,13 +2935,16 @@ const wqNote = writerQuiescence.verdict === 'quiet'
2818
2935
  ? ' WRITER-QUIESCENCE: quiet (' + writerQuiescence.note + ').'
2819
2936
  : ' WRITER-QUIESCENCE GATE (MANDATORY to acknowledge): ' + writerQuiescence.note + ' State this standing explicitly in 08_qe_report.md next to the grade.'
2820
2937
  await usageProbe('QE')
2821
- const qePrompt = 'Step 8 (QE - brutal-honesty review, agentic-qe) of /feature-adr for "' + DESC + '" (' + SLUG + '). Adversarially review the SHIPPED code (read it): correctness, edge cases, error handling, and the LOAD-BEARING property the ADR named (ASSERT it has a test that DISCRIMINATES - the recurring lesson: a test that would still pass with the protection deleted is documentation, not a gate). Run this ADR gate before final grading: ' + ADR_FITNESS_CHECKLIST + ' ' + DISCRIMINATION_GATE + ' ' + MUTATION_GATE + ' ' + NO_STUBS_GATE + ' ' + AMENDMENT_GATE + ' Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md with an ADR Fitness Checklist section showing PASS/FAIL per ADR and evidence for the Confirmation-linked test. MANDATORY SELF-LEARNING STORE (close the loop, never skip): compare every candidate lesson against the Step-0 recalled LEARNED patterns above. Teach ONLY lessons NOT covered by Step-0 recall. On overlap, run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate; if --reinforce is unavailable, skip the duplicate teach and report `reinforced existing pattern <id>` in the QE report. Store every genuinely new lesson in the CANONICAL BRAIN store at `' + BRAIN + '` so it is NOT lost to a target repo you may have cd`d into. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal) — the `cd ' + BRAIN + ' &&` prefix + `--project ' + BRAIN + '` pin guarantee the lesson lands in the brain regardless of your CWD. Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled auto --run fa:' + SLUG + ' --count-project ' + BRAIN + ' --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. AUTHORING-TIME CLAIM-CHECK (Deliverable of claim-check-authoring-time): after writing ' + FDIR + '/08_qe_report.md, run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash, parse the {ok, findings, scanned} JSON, and report claimCheck: {findings: N, high: N, medium: N} (counts by severity) in your return object. TAG EVERY QUANTITATIVE CLAIM you write in the report using the convention the checker recognizes as honest — write "1131 tests pass (MEASURED — `npx vitest run`)", never a bare "1131 tests pass" — and where you QUOTE a forbidden phrase as an example (e.g. the retracted "100% passing" framing), backtick the literal so it reads as code, not an assertion, so your own compliant report scans clean. Return {grade, gaps, codeTestsAdequate, docTestsPresent, claimCheck}.' + ABSOLUTE_PATH_NOTE + landedNote + wqNote + PS_GUIDANCE('qe')
2938
+ const qePrompt = 'Step 8 (QE - brutal-honesty review, agentic-qe) of /feature-adr for "' + DESC + '" (' + SLUG + '). Adversarially review the SHIPPED code (read it): correctness, edge cases, error handling, and the LOAD-BEARING property the ADR named (ASSERT it has a test that DISCRIMINATES - the recurring lesson: a test that would still pass with the protection deleted is documentation, not a gate). Run this ADR gate before final grading: ' + ADR_FITNESS_CHECKLIST + ' ' + DISCRIMINATION_GATE + ' ' + MUTATION_GATE + ' ' + NO_STUBS_GATE + ' ' + AMENDMENT_GATE + ' Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md with the primary findings under the exact heading `## Primary QE pass` and an ADR Fitness Checklist section showing PASS/FAIL per ADR and evidence for the Confirmation-linked test. MANDATORY SELF-LEARNING STORE (close the loop, never skip): compare every candidate lesson against the Step-0 recalled LEARNED patterns above. Teach ONLY lessons NOT covered by Step-0 recall. On overlap, run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate; if --reinforce is unavailable, skip the duplicate teach and report `reinforced existing pattern <id>` in the QE report. Store every genuinely new lesson in the CANONICAL BRAIN store at `' + BRAIN + '` so it is NOT lost to a target repo you may have cd`d into. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal) — the `cd ' + BRAIN + ' &&` prefix + `--project ' + BRAIN + '` pin guarantee the lesson lands in the brain regardless of your CWD. Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled auto --run fa:' + SLUG + ' --count-project ' + BRAIN + ' --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. AUTHORING-TIME CLAIM-CHECK (Deliverable of claim-check-authoring-time): after writing ' + FDIR + '/08_qe_report.md, run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash, parse the {ok, findings, scanned} JSON, and report claimCheck: {findings: N, high: N, medium: N} (counts by severity) in your return object. TAG EVERY QUANTITATIVE CLAIM you write in the report using the convention the checker recognizes as honest — write "1131 tests pass (MEASURED — `npx vitest run`)", never a bare "1131 tests pass" — and where you QUOTE a forbidden phrase as an example (e.g. the retracted "100% passing" framing), backtick the literal so it reads as code, not an assertion, so your own compliant report scans clean. Return {grade, gaps, codeTestsAdequate, docTestsPresent, claimCheck}.' + ABSOLUTE_PATH_NOTE + landedNote + wqNote + PS_GUIDANCE('qe')
2822
2939
  // CROSS-MODEL QE (load-bearing): resolveStageModel('qe') derives the OTHER family than the resolved
2823
2940
  // coder when args.models.qe is unset (coder-codex ⇒ opus; coder-Claude ⇒ codex, or opus if codex absent).
2824
2941
  // An explicit args.models.qe wins. A Claude qe spec is merged onto the qe-code-reviewer base (role
2825
2942
  // PRESERVED); a codex qe spec REPLACES agentType with codex:codex-rescue (as today). The codex-null→
2826
2943
  // Claude guard is retained as the runtime belt so codex-unavailable never blocks.
2827
- const qeModel = resolveStageModel('qe')
2944
+ let qeModel = resolveStageModel('qe')
2945
+ // A codex-fallback coder can finish on either family. Default QE follows the ACTUAL runner, not the
2946
+ // pre-code knob; only an explicit models.qe is allowed to opt out of cross-family review.
2947
+ if (MODELS.qe === undefined && routingRequested) qeModel = specToOpts(resolveQeSpecForCoder(tpFamily(coderUsed) === 'codex'))
2828
2948
  // Single tested source of truth (feature-adr-routing.ts:qeShouldUseCodex) — closes the self-QE hole where
2829
2949
  // the legacy qeReviewer='codex' knob used to re-route QE back to codex even when the CODER was codex.
2830
2950
  if (MODELS.qe === undefined && QE_REVIEWER === 'codex' && coderIsCodex()) log('QE: coder is codex — enforcing cross-model Claude QE (ignoring qeReviewer=codex to avoid self-review)')
@@ -2832,15 +2952,16 @@ if (MODELS.qe === undefined && QE_REVIEWER === 'codex' && coderIsCodex()) log('Q
2832
2952
  // (which reads the pre-override config) says claude — dispatching that through the "claude" branch
2833
2953
  // would run the stub wrapper AND mislabel qeReviewerUsed='claude' (a codex-on-codex review recorded
2834
2954
  // as cross-family). The resolved agentType is the truth; either signal routes the codex branch.
2835
- const qeIsCodex = qeShouldUseCodex() || (qeModel && qeModel.agentType === 'codex:codex-rescue')
2955
+ const qeIsCodex = !!(qeModel && qeModel.agentType === 'codex:codex-rescue')
2836
2956
  const qeClaudeModel = qeIsCodex ? {} : qeModel
2837
2957
  const qeClaudeOpts = mergeOpts({ label: stageLabel('qe:brutal', qeClaudeModel), phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, qeClaudeModel)
2958
+ const qe2Spec = qePrecisionPassSpec(PRIMARY, BUDGET_MODE, tier)
2838
2959
  // qe checkpoint: COMPOSITE (verdict + reviewer identity) keyed on the CODE stage's result — a re-coded
2839
2960
  // feature always re-QEs. The teach/fa-record side effects belong to the stage: a resumed QE does not
2840
2961
  // re-teach (the original run already stored its lessons — replaying teach would double-store).
2841
2962
  // R6: the review SCOPE is part of what a QE verdict is about, so it enters the hash — a resume must
2842
2963
  // not present a verdict obtained over one scope as if it had been obtained over another.
2843
- const qeHash = ckptHash('qe', [fnv1a64(JSON.stringify(codeStage === undefined ? null : codeStage)), tier, DESC, QE_REVIEWER, MODELS.qe === undefined ? null : MODELS.qe, CODEX_MODEL, coderUsed, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, QE_SCOPE, QE_SCOPE_REF])
2964
+ const qeHash = ckptHash('qe', [fnv1a64(JSON.stringify(codeStage === undefined ? null : codeStage)), tier, DESC, QE_REVIEWER, MODELS.qe === undefined ? null : MODELS.qe, CODEX_MODEL, coderUsed, PRIMARY, BUDGET_MODE, qe2Spec, POLY.hasManifest, fnv1a64(String(POLY.report || '')), usageOverride, QE_SCOPE, QE_SCOPE_REF])
2844
2965
  let crossFamilyQeReport = null
2845
2966
  const qeStage = await withCheckpoint('qe', 'QE', qeHash, async () => {
2846
2967
  let qe = null
@@ -2898,7 +3019,7 @@ if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
2898
3019
  }
2899
3020
  }
2900
3021
  }
2901
- let codexQe = await codexReviewAgent('qe', QE_SCOPE, QE_SCOPE_REF, 'QE')
3022
+ let codexQe = await codexReviewAgent('qe', QE_SCOPE, QE_SCOPE_REF, 'QE', qeCodexLabelOpts)
2902
3023
  // SCOPE THE VERDICT to this feature. Findings about other dirty work are real and are kept, but they
2903
3024
  // may not decide THIS feature's grade. When the change set is unmeasured the partition is 'unscoped'
2904
3025
  // and the grade stands exactly as the reviewer gave it — attributing nothing is the honest move.
@@ -2937,7 +3058,7 @@ if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
2937
3058
  // mode-B bookkeeping note would hide why the independent review did not happen.
2938
3059
  log('QE: mode B not constructible — no declared changed files to scope it to; keeping mode A reason')
2939
3060
  } else {
2940
- codexQe = await codexExecAgent('qe', modeBPrompt + CODEX_HINT + codexEffortHint(qeCodexLabelOpts), 'QE', true, modeBFiles)
3061
+ codexQe = await codexExecAgent('qe', modeBPrompt + CODEX_HINT + codexEffortHint(qeCodexLabelOpts), 'QE', true, modeBFiles, qeCodexLabelOpts)
2941
3062
  if (codexQe === null) lastCodexDecline = 'mode A: ' + String(modeADecline) + ' | mode B: ' + String(lastCodexDecline)
2942
3063
  }
2943
3064
  }
@@ -2959,7 +3080,7 @@ if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
2959
3080
  // with no 08_qe_report.md at all. Named by cross-family review of b6973199. The verdict itself
2960
3081
  // is real (Codex produced it), so a failed transcription DEGRADES the run rather than voiding
2961
3082
  // it — but it must be visible, and it must never read as a clean QE.
2962
- const scribePrompt = 'Step 8 (QE) of /feature-adr for "' + DESC + '" (' + SLUG + '). The independent cross-family review has ALREADY BEEN DONE, by Codex. You are the SCRIBE, not the reviewer: RECORD it, do NOT re-grade it, do NOT soften it, do NOT add a verdict of your own, and do NOT mark anything resolved that the reviewer flagged. The grade is ' + codexQe.grade + ' and it is FINAL.\n\nWrite ' + FDIR + '/08_qe_report.md with: (1) the grade ' + codexQe.grade + ' stated verbatim; (2) HOW it was obtained — dispatch mode ' + codexQe.mode + ', scope ' + codexQe.scopeRef + ', wall-clock ' + codexQe.elapsedSeconds + 's, gradeSource ' + codexQe.gradeSource + ' (a DERIVED grade means the reviewer could not be asked for a letter and it was computed from the severities it reported — say so plainly); (3) the reviewer text below, verbatim, as the findings section; (4) an ADR Fitness Checklist section with PASS/FAIL per ADR and the evidence pointer for the Confirmation-linked test.\n\nREVIEWER TEXT (verbatim, do not edit or summarise):\n' + String(codexQe.text) + '\n\nMANDATORY SELF-LEARNING STORE (close the loop, never skip): compare candidate lessons against the Step-0 recalled LEARNED patterns. Teach ONLY lessons NOT already covered; on overlap run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal). Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled auto --run fa:' + SLUG + ' --count-project ' + BRAIN + ' --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` verbatim via Bash. Finally run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash and TAG every quantitative claim you write the way the checker recognises as honest.' + ABSOLUTE_PATH_NOTE
3083
+ const scribePrompt = 'Step 8 (QE) of /feature-adr for "' + DESC + '" (' + SLUG + '). The independent cross-family review has ALREADY BEEN DONE, by Codex. You are the SCRIBE, not the reviewer: RECORD it, do NOT re-grade it, do NOT soften it, do NOT add a verdict of your own, and do NOT mark anything resolved that the reviewer flagged. The grade is ' + codexQe.grade + ' and it is FINAL.\n\nWrite ' + FDIR + '/08_qe_report.md with: (1) the grade ' + codexQe.grade + ' stated verbatim; (2) HOW it was obtained — dispatch mode ' + codexQe.mode + ', scope ' + codexQe.scopeRef + ', wall-clock ' + codexQe.elapsedSeconds + 's, gradeSource ' + codexQe.gradeSource + ' (a DERIVED grade means the reviewer could not be asked for a letter and it was computed from the severities it reported — say so plainly); (3) the reviewer text below, verbatim, under the exact heading `## Primary QE pass`; (4) an ADR Fitness Checklist section with PASS/FAIL per ADR and the evidence pointer for the Confirmation-linked test.\n\nREVIEWER TEXT (verbatim, do not edit or summarise):\n' + String(codexQe.text) + '\n\nMANDATORY SELF-LEARNING STORE (close the loop, never skip): compare candidate lessons against the Step-0 recalled LEARNED patterns. Teach ONLY lessons NOT already covered; on overlap run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal). Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled auto --run fa:' + SLUG + ' --count-project ' + BRAIN + ' --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` verbatim via Bash. Finally run EXACTLY `dz claim-check ' + FDIR + '/08_qe_report.md --json --fail-on none` via Bash and TAG every quantitative claim you write the way the checker recognises as honest.' + ABSOLUTE_PATH_NOTE
2963
3084
  // WITNESS THE REWRITE, not the existence. On a re-QE or a resume with the same slug an OLD
2964
3085
  // 08_qe_report.md is already sitting there, and an existence probe reports that stale file as
2965
3086
  // landed — so a scribe that wrote nothing still marked the new verdict recorded, and the stage
@@ -3011,12 +3132,49 @@ if (qe === null && qeIsCodex) {
3011
3132
  crossFamilyQeReport = cfBelt.report
3012
3133
  }
3013
3134
  }
3135
+ // A-normal L/XL only: Sonnet is the recall-oriented primary reviewer; Opus is a SECOND,
3136
+ // independent precision pass. It is advisory but real — never a table-only half-wire — and its
3137
+ // provenance stays separate in both the return object and 08_qe_report.md.
3138
+ let qe2 = null
3139
+ if (qe !== null && qe2Spec !== null) {
3140
+ const qe2Model = specToOpts(qe2Spec)
3141
+ const primaryGrade = String(qe.grade || '').trim().toUpperCase()
3142
+ const qe2ReportState = async function (label) {
3143
+ const report = shq(FDIR + '/08_qe_report.md')
3144
+ const cmd = 'p=' + report + '; h=$(sha256sum -- "$p" 2>/dev/null | awk "{print \\$1}"); [ -n "$h" ] || h=ABSENT; a=$(grep -cFx "## Primary QE pass" "$p" 2>/dev/null || true); b=$(grep -cFx "## Precision QE pass — Claude Opus" "$p" 2>/dev/null || true); c=$(grep -c "Combined Step-8 grade" "$p" 2>/dev/null || true); echo "QE2-REPORT sha=$h primary=$a precision=$b combined=$c"'
3145
+ const out = await agent('Run EXACTLY this via Bash and return only its stdout: ' + cmd, { label: label, phase: 'QE', effort: 'low' })
3146
+ const m = /QE2-REPORT sha=([0-9a-f]{64}|ABSENT) primary=(\d+) precision=(\d+) combined=(\d+)/.exec(String(out || ''))
3147
+ return m ? { sha: m[1], primary: Number(m[2]), precision: Number(m[3]), combined: Number(m[4]) } : null
3148
+ }
3149
+ const qe2Before = await qe2ReportState('qe:precision-before')
3150
+ const qe2Prompt = 'Step 8 precision QE second pass for "' + DESC + '" (' + SLUG + '). This is an INDEPENDENT precision-oriented review after the recall-oriented primary pass. First inspect the shipped code, tests, requirements, architecture, and every ADR and form your own findings WITHOUT consulting 08_qe_report.md. Only after your review is complete, open ' + FDIR + '/08_qe_report.md and APPEND (never replace) your findings under the exact heading `## Precision QE pass — Claude Opus`. Preserve `## Primary QE pass` as a separate provenance section. Grade A/B/C/D honestly. The primary grade was ' + primaryGrade + '; state a `Combined Step-8 grade` equal to the worse of that grade and your precision grade. Return {grade, gaps, codeTestsAdequate, docTestsPresent}.' + ABSOLUTE_PATH_NOTE + landedNote + wqNote
3151
+ const qe2Opts = mergeOpts({ label: stageLabel('qe:precision', qe2Model), phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, qe2Model)
3152
+ qe2 = await agent(qe2Prompt, qe2Opts)
3153
+ if (qe2) {
3154
+ const qe2After = await qe2ReportState('qe:precision-after')
3155
+ const qe2Recorded = !!(qe2Before && qe2After && qe2After.sha !== 'ABSENT' && qe2After.sha !== qe2Before.sha && qe2After.primary > 0 && qe2After.precision > qe2Before.precision && qe2After.combined > 0)
3156
+ const rank = { A: 0, B: 1, C: 2, D: 3 }
3157
+ const precisionGrade = String(qe2.grade || '').trim().toUpperCase()
3158
+ qe2 = mergeOpts(qe2, { reportWritten: qe2Recorded })
3159
+ if (qe2Recorded && rank[precisionGrade] !== undefined && rank[primaryGrade] !== undefined) {
3160
+ const combinedGrade = rank[precisionGrade] > rank[primaryGrade] ? precisionGrade : primaryGrade
3161
+ qe = mergeOpts(qe, { grade: combinedGrade, primaryGrade: primaryGrade, precisionPass: { reviewer: qe2Spec, grade: precisionGrade, gaps: Array.isArray(qe2.gaps) ? qe2.gaps : [] } })
3162
+ modelsUsed.qe2 = modelLabel(qe2Model)
3163
+ } else {
3164
+ log('QE precision pass: report append NOT WITNESSED or grade invalid — primary verdict remains authoritative')
3165
+ modelsUsed.qe2 = modelLabel(qe2Model) + ' (report-unverified)'
3166
+ }
3167
+ } else {
3168
+ log('QE precision pass: Opus returned null — primary QE verdict remains authoritative; qe2 recorded unavailable')
3169
+ }
3170
+ }
3014
3171
  if (qe === null) return null
3015
- return { qe: qe, qeReviewerUsed: qeReviewerUsed, modelUsed: modelsUsed.qe }
3172
+ return { qe: qe, qeReviewerUsed: qeReviewerUsed, modelUsed: modelsUsed.qe, qe2: qe2, qe2ModelUsed: modelsUsed.qe2 || null }
3016
3173
  }, { validate: function (r) { return !!(r && typeof r === 'object' && r.qe && typeof r.qe === 'object' && typeof r.qeReviewerUsed === 'string') } })
3017
3174
  qe = qeStage ? qeStage.qe : null
3018
3175
  let qeReviewerUsed = qeStage ? qeStage.qeReviewerUsed : 'claude'
3019
3176
  if (qeStage && qeStage.modelUsed) modelsUsed.qe = qeStage.modelUsed + (resumedStages.indexOf('qe') !== -1 ? ' (resumed)' : '')
3177
+ if (qeStage && qeStage.qe2ModelUsed) modelsUsed.qe2 = qeStage.qe2ModelUsed + (resumedStages.indexOf('qe') !== -1 ? ' (resumed)' : '')
3020
3178
 
3021
3179
  // Step 8 claim-gate: fold the QE agent's reported claim-check counts into an additive result field.
3022
3180
  const claimGate = step8ClaimGate(qe && qe.claimCheck ? qe.claimCheck : null)
@@ -3135,7 +3293,7 @@ if (isLplus) {
3135
3293
  () => agent(fleetTracePrompt, fleetTraceOpts),
3136
3294
  () => agent(fleetCovPrompt, fleetCovOpts),
3137
3295
  ]
3138
- const fleetHash = ckptHash('fleet', [fnv1a64(JSON.stringify(qeStage === undefined ? null : qeStage)), tier, MODELS.fleet === undefined ? null : MODELS.fleet, CODEX_MODEL, usageOverride])
3296
+ const fleetHash = ckptHash('fleet', [fnv1a64(JSON.stringify(qeStage === undefined ? null : qeStage)), tier, MODELS.fleet === undefined ? null : MODELS.fleet, CODEX_MODEL, PRIMARY, BUDGET_MODE, usageOverride])
3139
3297
  fleet = await withCheckpoint('fleet', 'FleetQE', fleetHash, async () => {
3140
3298
  const fleetRuns = await parallel(fleetThunks)
3141
3299
  return fleetRuns.every(function (x) { return x !== null && x !== undefined }) ? 'run' : null