@dzhechkov/skills-feature-adr 1.3.36 → 1.3.40

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/README.md CHANGED
@@ -139,7 +139,41 @@ Workflow({ scriptPath: '.claude/workflows/feature-adr.js',
139
139
  ```
140
140
 
141
141
  Omit the Codex knobs entirely for today's all-Claude behavior. The run result reports
142
- `plannerUsed` / `coderUsed` / `qeReviewerUsed` / `codexModel` so you can see who did what.
142
+ `plannerUsed` / `coderUsed` / `qeReviewerUsed` / `codexModel` / `modelsUsed` so you can see who did what.
143
+
144
+ ### Per-stage model routing — `args.models`
145
+
146
+ The three Codex knobs above are shortcuts. `args.models` is the **general dial**: an optional map that
147
+ routes each of the 11 pipeline stages to an optimal model. Keys: `{router, requirements, research, adr,
148
+ ideation, ddd, architecture, plan, code, qe, fleet}`. Each value is a **spec** — Claude
149
+ `fable|opus|sonnet|haiku`, or Codex `codex` / `codex:<id>` / `codex:<id>:<reasoning>`
150
+ (`reasoning ∈ low|medium|high|xhigh`; ids incl. `gpt-5.5`, `gpt-5.6`).
151
+
152
+ **Recommended DEFAULT TABLE** (applied only when you opt in — one `args.models` key or any Codex knob
153
+ turns it on; otherwise every stage is session-inherited, byte-identical to today):
154
+
155
+ | router | requirements | adr | ideation | architecture | plan | code | qe | fleet |
156
+ |---|---|---|---|---|---|---|---|---|
157
+ | `fable` | `sonnet` | `opus` | `sonnet` | `opus` | `sonnet` | coder (default `opus`) | cross-model | `sonnet` |
158
+
159
+ (`research` folds into `requirements`, `ddd` into `architecture` — recorded in `modelsUsed`, not a
160
+ separate call.)
161
+
162
+ **Cross-model QE default (load-bearing):** when `args.models.qe` is unset, QE auto-routes to the **other
163
+ family than the coder** — a model that writes code must not also self-QE; independent cross-model review
164
+ catches what self-review misses. coder=Codex ⇒ QE=Claude (`opus`); coder=Claude ⇒ QE=Codex
165
+ (`codex:<top>:high`), or a Claude reviewer if Codex is unavailable (never blocks).
166
+
167
+ **Precedence:** `args.models[stage]` wins; the legacy `planner`/`coder`/`qeReviewer`/`codexModel` knobs
168
+ fill only unspecified stages. `codexModel` seeds the id for a bare `'codex'` spec. gpt-5.6-ready: a new
169
+ Codex id is a data-only allowlist edit.
170
+
171
+ ```js
172
+ // Claude writes the code; Codex independently QEs it (cross-model by construction):
173
+ Workflow({ scriptPath: '.claude/workflows/feature-adr.js',
174
+ args: { slug: 'add-oauth', description: '…', tier: 'L',
175
+ models: { code: 'opus', qe: 'codex:gpt-5.6:high', architecture: 'opus', router: 'fable' } } })
176
+ ```
143
177
 
144
178
  ---
145
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.3.36",
3
+ "version": "1.3.40",
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"
@@ -375,6 +375,69 @@ Pre-flight, if Codex is `ready` (`codex-companion setup --json`), the orchestrat
375
375
  before launching; plain `/feature-adr` offers the same at the planning checkpoint. Omit the Codex knobs
376
376
  for today's all-Claude behavior. See `.claude/rules/feature-adr-ultracode.md`.
377
377
 
378
+ **Per-stage model routing — `args.models` (one dial, 11 stages).** `args.models` is an optional map that
379
+ routes each pipeline stage to an optimal model. Keys: `{router, requirements, research, adr, ideation,
380
+ ddd, architecture, plan, code, qe, fleet}`. Each value is a **spec**:
381
+
382
+ - **Claude** — `'fable' | 'opus' | 'sonnet' | 'haiku'` → adds `model` to that stage's `agent()` call
383
+ (any role `agentType` like `qe-code-reviewer` is PRESERVED).
384
+ - **Codex** — `'codex'` / `'codex:<id>'` / `'codex:<id>:<reasoning>'` (e.g. `'codex:gpt-5.6:xhigh'`;
385
+ `reasoning ∈ low|medium|high|xhigh`; ids incl. `gpt-5.5`, `gpt-5.6`) → routes the stage to the
386
+ `codex:codex-rescue` runtime with that `codexModel` + reasoning hint.
387
+
388
+ **Recommended DEFAULT TABLE** (applied only when you opt into routing — any one `args.models` key or any
389
+ Codex knob flips it on; otherwise every stage stays session-inherited, byte-identical to today):
390
+
391
+ | Stage | Default | | Stage | Default |
392
+ |---|---|---|---|---|
393
+ | router | `fable` | | architecture | `opus` |
394
+ | requirements | `sonnet` | | plan | `sonnet` |
395
+ | research | `sonnet` (folds into requirements) | | code | *the coder knob* (default Claude `opus`) |
396
+ | adr | `opus` | | qe | *CROSS-MODEL of the coder* (see below) |
397
+ | ideation | `sonnet` | | fleet | `sonnet` |
398
+ | ddd | `opus` (folds into architecture) | | | |
399
+
400
+ **Cross-model QE default (load-bearing).** When `args.models.qe` is UNSET, QE is auto-routed to the
401
+ **other model family than the coder** — a model that WRITES code must not also SELF-QE; independent
402
+ cross-model review catches what self-review misses. coder=Codex ⇒ QE=Claude (`opus`); coder=Claude ⇒
403
+ QE=Codex (`codex:<top>:high`), or a Claude reviewer if Codex is unavailable (**never blocks**).
404
+
405
+ **Precedence.** `args.models[stage]` is the general mechanism and WINS on conflict; the legacy
406
+ `planner`/`coder`/`qeReviewer`/`codexModel` knobs are shortcuts that fill a stage only when `args.models`
407
+ does not. `codexModel` sets the default id for a bare `'codex'` spec. Codex-fallback (Claude-first, then
408
+ Codex on limit-exhaustion) stays a knob-only behavior; a direct `models.code='codex'` means codex-first.
409
+
410
+ **gpt-5.6-ready** — a new Codex id is a DATA-ONLY addition to the `KNOWN_CODEX` allowlist (no control flow).
411
+ **Reporting** — the run result includes `modelsUsed` (the resolved per-stage model) so you see who did what.
412
+
413
+ *Example:* `Workflow({ scriptPath: '.claude/workflows/feature-adr.js', args: { slug, description, tier:
414
+ 'L', models: { code: 'opus', qe: 'codex:gpt-5.6:high', architecture: 'opus', router: 'fable' } } })` —
415
+ Claude writes the code, Codex independently QEs it.
416
+
417
+ **Usage-adaptive routing (pre-emptive Codex switch under limit pressure).** When routing is opted into,
418
+ the workflow probes Claude SESSION (active 5h-block) and WEEKLY (rolling 7d) usage at EACH phase boundary
419
+ via a minimal `dz usage --json` agent. When either metric is `>= usageThreshold` (default `70`) BEFORE a
420
+ phase launches — OR the probe output is missing (agent-null, which often MEANS the limit was hit) — ALL
421
+ remaining stages switch to `codex:<top>` (design/code/plan at `xhigh`, router/qe/fleet at `high`). When a
422
+ later probe reads BOTH metrics below the threshold (positive numbers, not nulls), the normal Claude+Codex
423
+ mix is RESTORED. Null percentages (unconfigured limits) change NOTHING in either direction. Switched
424
+ stages are tagged ` (usage-switched)` in `modelsUsed`, and every flip is recorded in the result's
425
+ `usageEvents` array (the audit trail — never trust promiseTags for this). Args:
426
+
427
+ | `args.*` | Default | Effect |
428
+ |---|---|---|
429
+ | `usageAdaptive` | `true` when routing is requested (any `args.models` key or Codex knob); `false` otherwise | `true` forces it on even without other routing; `false` disables all probes (byte-identical to today) |
430
+ | `usageThreshold` | `70` | the `>=` percent (either metric) that triggers the pre-emptive switch |
431
+ | `usageReasoning` | the `OVERRIDE_REASONING` map | per-stage reasoning under the override (merge over the default: design/code/plan → `xhigh`, router/qe/fleet → `high`) |
432
+
433
+ Configure the limits the probe measures against in `.dz/config.json` — `memory.usage.sessionTokenLimit`
434
+ and `memory.usage.weeklyTokenLimit` (OPTIONAL; absent ⇒ `pct` is `null` and no switch fires). The
435
+ percentages are **ESTIMATES** from local transcript aggregation (there is no official usage API);
436
+ **calibrate** by scaling a limit by `X/100` when a real limit-hit lands at an estimated `X%`.
437
+ **Honest caveat (the wrapper lesson):** at TRUE exhaustion even the Codex dispatch dies because
438
+ `codex:codex-rescue` is a Claude wrapper subagent — so the switch MUST happen BEFORE, which is why the
439
+ 70% pre-emptive probe (not just reactive null-detection) is the real defense.
440
+
378
441
  ### Pattern memory loop (self-learning — runs in ALL modes)
379
442
 
380
443
  **Self-learning is MANDATORY on EVERY `/feature-adr` run — including plain `/feature-adr` without any
@@ -408,6 +471,15 @@ non-blocking — an error or empty result never stalls the pipeline. This layer
408
471
  **distinct from the Keysarium reward layer** installed by `--with-learning`: it uses agentic-qe MCP
409
472
  memory + the dz store, is UNCONDITIONAL for the dz half (recall/teach/fa-record run in every mode) and Direct-mode-only for the aqe-MCP half, and never touches `.keysarium/memory/`.
410
473
 
474
+ **Canonical brain store — `args.brain` (never fragment the loop).** The dz durable loop only compounds if
475
+ Step-0 recall and Step-8 teach hit the SAME store. The workflow pins both to a canonical **brain** —
476
+ `args.brain`, default = the workspace root — via `cd <brain> && dz recall/teach … --project <brain>`, so a
477
+ Step-8 teach issued from a coder that `cd`'d into a target repo still lands in the brain, not that repo's
478
+ `.dz`. Omitting `args.brain` is behaviorally inert for a workspace-CWD run (`brain === repo`). **Share** a
479
+ brain: `dz recall --all --json > patterns.json` → `dz teach --from-json patterns.json --project <brain>`
480
+ (exact-text dedup, idempotent). **Recover** a fragmented store: `cd <stray-repo> && dz recall --all --json >
481
+ /tmp/stray.json` → `dz teach --from-json /tmp/stray.json --project <brain>` to merge it back into the brain.
482
+
411
483
  **Live learning panel (`dz statusline`).** The pipeline **drives** the panel: at each pattern-memory-loop
412
484
  step above it records its live state via `dz statusline --fa-record …`, so `dz statusline` can surface
413
485
  per-run learning (which feature, which step, how many patterns recalled vs. newly stored). Each of the
@@ -1,7 +1,7 @@
1
1
  export const meta = {
2
2
  name: 'feature-adr',
3
3
  description: 'Canonical /feature-adr --full-qe-extended pipeline as a reusable workflow: router+RECALL then design(ADR, applies learned patterns) then plan then code then agentic-qe QE+TEACH, producing features/<slug>/00-09 artifacts. MANDATORY in-process self-learning loop (Step-0 recall, apply, Step-8 teach). OPTIONAL Codex routing: args.planner=codex (Step-6), args.coder/qeReviewer=codex-fallback (Step-7/8 fall back to Codex when Claude limits exhaust; args.codexModel default auto, Codex self-selects top). Hybrid checkpoints (S/M autonomous; L/XL stop-after-plan).',
4
- whenToUse: 'ultracode + a feature implementation. Invoke via Workflow({scriptPath:".claude/workflows/feature-adr.js", args:{slug, description, code, tier, stopAfter, planner, coder, qeReviewer, codexModel}}) instead of an ad-hoc orchestration, so every feature ships with an ADR + inline agentic-qe QE + self-learning.',
4
+ whenToUse: 'ultracode + a feature implementation. Invoke via Workflow({scriptPath:".claude/workflows/feature-adr.js", args:{slug, description, code, tier, stopAfter, planner, coder, qeReviewer, codexModel, brain}}) instead of an ad-hoc orchestration, so every feature ships with an ADR + inline agentic-qe QE + self-learning. args.brain pins the self-learning loop (recall/teach) to ONE canonical brain store (default = the workspace root) so lessons never fragment into a target repo when the coder cd`s away.',
5
5
  phases: [
6
6
  { title: 'Router', detail: 'Step 0 - classify + self-learning recall' },
7
7
  { title: 'Design', detail: 'Steps 1-5 - requirements, ADR, QCSD, architecture (tier-gated)' },
@@ -25,6 +25,206 @@ const REPO = (A.repo || '.').replace(/\/+$/, '')
25
25
  const FDIR = REPO + '/features/' + SLUG
26
26
  // The dz CLI: bare `dz` (on PATH for installed users) unless the caller overrides with a bin path.
27
27
  const DZ = A.dzBin || 'dz'
28
+ // CANONICAL BRAIN store: the self-learning loop (Step-0 recall → Step-8 teach) MUST read+write ONE
29
+ // shared pattern store so lessons never fragment into a target repo's .dz when the Step-7 coder cd's
30
+ // away. BRAIN defaults to the workspace root (REPO) — so an OMITTED args.brain is behaviorally inert
31
+ // for a workspace-CWD run (BRAIN===REPO, same store as today's bare recall/teach). Override args.brain
32
+ // with a stable absolute path to keep ONE brain across several target checkouts.
33
+ const BRAIN = (A.brain || REPO).replace(/\/+$/, '')
34
+ // Helpers PIN every learn-loop command to the canonical brain: `cd <BRAIN> &&` survives a cd'd agent
35
+ // (belt); `--project <BRAIN>` is explicit (suspenders). Either alone fixes it; together they also
36
+ // survive the relative-vs-absolute --project resolution asymmetry between recall and teach.
37
+ const DZ_RECALL = (terms) => 'cd ' + BRAIN + ' && ' + DZ + ' recall "' + terms + '" --project ' + BRAIN
38
+ const DZ_TEACH = (lesson, reward, domain) =>
39
+ 'cd ' + BRAIN + ' && ' + DZ + ' teach "' + lesson + '" --reward ' + reward + ' --domain ' + domain + ' --project ' + BRAIN
40
+
41
+ // ── Codex-routing knobs (hoisted so the routing block below can fold them) ──
42
+ // CODER/QE_REVIEWER ∈ 'claude'|'codex'|'codex-fallback'. On 'codex-fallback' the Claude agent runs
43
+ // FIRST; if it returns null (e.g. the Claude Code session limit is exhausted mid-code/mid-QE), the SAME
44
+ // task is retried on the codex:codex-rescue runtime. args.codexModel is DEFAULT 'auto' (Codex self-selects
45
+ // the top model available to the account — ids are account/version-specific and move ahead of any static
46
+ // default, so 'auto' is the portable choice). To hard-pin a specific id the orchestrator writes it into
47
+ // ~/.codex/config.toml at pre-flight; the hint below only nudges.
48
+ const CODEX_MODEL = A.codexModel || 'auto'
49
+ const CODEX_HINT = ' (If you are the Codex runtime, prefer the ' + CODEX_MODEL + ' model.)'
50
+ const CODER = (A.coder === 'codex' || A.coder === 'codex-fallback') ? A.coder : 'claude'
51
+ const QE_REVIEWER = (A.qeReviewer === 'codex' || A.qeReviewer === 'codex-fallback') ? A.qeReviewer : 'claude'
52
+ const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
53
+
54
+ // ── PER-STAGE MODEL ROUTING (args.models) ───────────────────────────────────
55
+ // One dial routes each pipeline stage to an optimal model. `args.models` is an optional per-stage map
56
+ // over {router, requirements, research, adr, ideation, ddd, architecture, plan, code, qe, fleet}; each
57
+ // value a SPEC — Claude 'fable'|'opus'|'sonnet'|'haiku', or Codex 'codex' / 'codex:<id>' /
58
+ // 'codex:<id>:<reasoning>' (reasoning ∈ low|medium|high|xhigh; ids incl gpt-5.5, gpt-5.6).
59
+ // LOAD-BEARING: when args.models.qe is unset the QE stage is auto-routed to the OTHER family than the
60
+ // coder (a model that codes must not also self-QE). BACKWARD-COMPATIBLE: omitting args.models AND the
61
+ // legacy knobs ⇒ routingRequested is false ⇒ every stage resolves to {} ⇒ byte-identical to today.
62
+ // Precedence: args.models[stage] > legacy planner/coder/qeReviewer/codexModel knobs > DEFAULT_MODELS.
63
+ // gpt-5.6-ready: adding a codex id is a DATA-ONLY edit to KNOWN_CODEX. This block is the parser-safe
64
+ // (string concat, explicit if/return, object-literal tables — NO template literals, NO inline ?:agent())
65
+ // mirror of src/feature-adr-routing.ts; keep the two in lock-step (a drift test asserts it).
66
+ const MODELS = (A.models && typeof A.models === 'object') ? A.models : {}
67
+ const KNOWN_CODEX = { 'auto': 1, 'gpt-5.5': 1, 'gpt-5.6': 1 }
68
+ const CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 }
69
+ 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' }
70
+ const routingRequested = (Object.keys(MODELS).length > 0) || (PLANNER === 'codex') || (CODER === 'codex' || CODER === 'codex-fallback') || (QE_REVIEWER === 'codex' || QE_REVIEWER === 'codex-fallback') || (A.usageAdaptive === true)
71
+ const modelsUsed = {}
72
+
73
+ // ── USAGE-ADAPTIVE ROUTING (pre-emptive codex switch at >= usageThreshold, default 70%) ──
74
+ // At every phase boundary a minimal haiku probe runs 'dz usage --json'; when SESSION or WEEKLY
75
+ // usage crosses the threshold BEFORE a phase launches, ALL remaining stages switch to
76
+ // codex:<topCodexId> (design/code/plan at xhigh, router/qe/fleet at high). When a later probe reads
77
+ // BOTH metrics below threshold (positive numbers, not nulls) the normal mix is RESTORED. The
78
+ // load-bearing asymmetry: an agent-null probe (dispatch died — often MEANS limits) fail-safe-switches
79
+ // TO codex; a value-null (unconfigured limits) flips NOTHING. All additive behind USAGE_ADAPTIVE:
80
+ // usageAdaptive:false OR no routing requested ⇒ zero probes, byte-identical to today.
81
+ // The single mutable routing bit usageOverride lives HERE (the workflow is its own environment);
82
+ // the pure library mirror (feature-adr-routing.ts) threads it via RoutingEnv — never a global.
83
+ const USAGE_THRESHOLD = Number(A.usageThreshold) > 0 ? Number(A.usageThreshold) : 70
84
+ const USAGE_ADAPTIVE = (A.usageAdaptive !== false) && (routingRequested || A.usageAdaptive === true)
85
+ 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 : {})
86
+ const usageReasoning = OVERRIDE_REASONING
87
+ let usageOverride = false
88
+ const usageEvents = []
89
+
90
+ // decideUsageAction: the PURE hysteresis core (byte-equivalent to feature-adr-routing.ts). Given the
91
+ // previous override bit, a probe signal (or null when the probe agent DIED), and the threshold,
92
+ // decide the new override bit + the LOCKED 6-value action. Total function.
93
+ function decideUsageAction(prevOverride, signal, threshold) {
94
+ if (signal === null || signal === undefined) {
95
+ if (prevOverride) return { override: true, action: 'keep' }
96
+ return { override: true, action: 'fail-safe-switch' }
97
+ }
98
+ const s = signal.sessionPct
99
+ const w = signal.weeklyPct
100
+ const sKnown = typeof s === 'number' && isFinite(s) && s >= 0
101
+ const wKnown = typeof w === 'number' && isFinite(w) && w >= 0
102
+ if ((sKnown && s >= threshold) || (wKnown && w >= threshold)) {
103
+ return { override: true, action: prevOverride ? 'keep' : 'switch' }
104
+ }
105
+ if (sKnown && wKnown) {
106
+ return { override: false, action: prevOverride ? 'restore' : 'none' }
107
+ }
108
+ return { override: prevOverride, action: prevOverride ? 'keep' : 'none' }
109
+ }
110
+
111
+ const PROBE_SCHEMA = { type: 'object', additionalProperties: false, required: ['sessionPct', 'weeklyPct'], properties: { sessionPct: { type: ['number', 'null'] }, weeklyPct: { type: ['number', 'null'] } } }
112
+
113
+ // usageProbe: at each phase boundary, dispatch a minimal haiku/effort-low agent that runs EXACTLY
114
+ // one shell command ('dz usage --json') — the same guaranteed-single-command shape as fa-record:step0.
115
+ // Feed the reading to decideUsageAction, flip usageOverride, record a usageEvents entry. First
116
+ // statement is the BC guard: when USAGE_ADAPTIVE is off, ZERO probe agents are dispatched (AC-4).
117
+ async function usageProbe(phaseName) {
118
+ if (!USAGE_ADAPTIVE) return
119
+ const probePrompt = 'Run EXACTLY this one shell command via your Bash tool and return ONLY its parsed JSON fields sessionPct and weeklyPct (numbers or null), nothing else, do not summarize: ' + DZ + ' usage --json --project ' + REPO
120
+ const r = await agent(probePrompt, { label: 'usage:probe', phase: phaseName, model: 'haiku', effort: 'low', schema: PROBE_SCHEMA })
121
+ const d = decideUsageAction(usageOverride, r, USAGE_THRESHOLD)
122
+ if (d.action === 'switch') log('usage: session ' + (r ? r.sessionPct : null) + '% / week ' + (r ? r.weeklyPct : null) + '% >= ' + USAGE_THRESHOLD + '% — switching remaining stages to codex:' + topCodexId())
123
+ if (d.action === 'fail-safe-switch') log('usage: probe died (agent-null — often MEANS limits) — fail-safe switching remaining stages to codex:' + topCodexId())
124
+ if (d.action === 'restore') log('usage: back to ' + (r ? r.sessionPct : null) + '%/' + (r ? r.weeklyPct : null) + '% — restoring normal routing')
125
+ usageOverride = d.override
126
+ usageEvents.push({ phase: phaseName, sessionPct: r ? r.sessionPct : null, weeklyPct: r ? r.weeklyPct : null, action: d.action })
127
+ }
128
+
129
+ // reactiveBelt: generalizes today's codex-fallback. When a stage agent returns null while NOT already
130
+ // overridden (a possible limit event), flip the override so the REMAINING stages don't walk into the
131
+ // same wall. The legacy codex-fallback same-stage retry is untouched. Best-effort: at TRUE exhaustion
132
+ // even codex dispatch dies (codex:codex-rescue is a Claude wrapper) — the 70% pre-emptive probe is the real defense.
133
+ function reactiveBelt(phaseName) {
134
+ if (!USAGE_ADAPTIVE || usageOverride) return
135
+ usageOverride = true
136
+ usageEvents.push({ phase: phaseName, sessionPct: null, weeklyPct: null, action: 'reactive-switch' })
137
+ log('usage: stage agent returned null while not overridden — possible limit event, switching remaining stages to codex:' + topCodexId())
138
+ }
139
+
140
+ function specToOpts(spec) {
141
+ if (!spec) return {}
142
+ const parts = String(spec).split(':')
143
+ if (parts[0] === 'codex') {
144
+ let id = parts[1] || CODEX_MODEL
145
+ if (id !== 'auto' && !KNOWN_CODEX[id]) { log('models: unknown codex id ' + id + ' — using ' + CODEX_MODEL); id = CODEX_MODEL }
146
+ const reasoning = parts[2] || 'high'
147
+ return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning }
148
+ }
149
+ if (CLAUDE_NAMES[parts[0]]) return { model: parts[0] }
150
+ log('models: unknown spec ' + spec + ' — session-inherited')
151
+ return {}
152
+ }
153
+
154
+ function resolveCoderSpec() {
155
+ if (CODER === 'codex' || CODER === 'codex-fallback') return 'codex:' + CODEX_MODEL + ':high'
156
+ return 'opus'
157
+ }
158
+
159
+ function coderIsCodex() {
160
+ if (CODER === 'codex' || CODER === 'codex-fallback') return true
161
+ const codeSpec = MODELS.code
162
+ if (codeSpec && String(codeSpec).split(':')[0] === 'codex') return true
163
+ return false
164
+ }
165
+
166
+ function topCodexId() {
167
+ let top = CODEX_MODEL
168
+ if (top === 'auto') {
169
+ const ids = Object.keys(KNOWN_CODEX)
170
+ for (let i = 0; i < ids.length; i++) { if (ids[i] !== 'auto') top = ids[i] || top }
171
+ }
172
+ return top
173
+ }
174
+
175
+ function resolveQeSpec() {
176
+ if (coderIsCodex()) return 'opus'
177
+ const CODEX_AVAILABLE = A.codexAvailable !== false
178
+ if (!CODEX_AVAILABLE) return 'opus'
179
+ return 'codex:' + topCodexId() + ':high'
180
+ }
181
+
182
+ // qeShouldUseCodex: the load-bearing cross-model gate — the model that wrote the code must NEVER self-QE.
183
+ // (1) explicit MODELS.qe wins; (2) legacy QE_REVIEWER==='codex' knob honored ONLY when coder is NOT codex
184
+ // (a codex coder + qeReviewer:'codex' would be codex-self-QE); (3) else the cross-model default decides.
185
+ function qeShouldUseCodex() {
186
+ const explicit = MODELS.qe
187
+ if (explicit !== undefined && explicit !== null) {
188
+ return String(explicit).split(':')[0] === 'codex'
189
+ }
190
+ if (QE_REVIEWER === 'codex') return !coderIsCodex()
191
+ return routingRequested && resolveQeSpec().split(':')[0] === 'codex'
192
+ }
193
+
194
+ function resolveStageModel(stage) {
195
+ if (usageOverride) {
196
+ const r = (usageReasoning && usageReasoning[stage]) || OVERRIDE_REASONING[stage] || 'high'
197
+ const o = specToOpts('codex:' + topCodexId() + ':' + r)
198
+ o._usageSwitched = true
199
+ return o
200
+ }
201
+ let spec = MODELS[stage]
202
+ if (spec === undefined) {
203
+ if (!routingRequested) return {}
204
+ spec = DEFAULT_MODELS[stage]
205
+ }
206
+ if (stage === 'code' && (spec === null || spec === undefined)) return specToOpts(resolveCoderSpec())
207
+ if (stage === 'qe' && (spec === null || spec === undefined)) return specToOpts(resolveQeSpec())
208
+ return specToOpts(spec)
209
+ }
210
+
211
+ function mergeOpts(base, extra) {
212
+ const out = {}
213
+ for (const k in base) out[k] = base[k]
214
+ for (const k in extra) out[k] = extra[k]
215
+ return out
216
+ }
217
+
218
+ // modelLabel: record the resolved spec for a stage in modelsUsed (for the run report / who-did-what).
219
+ function modelLabel(opts) {
220
+ if (opts && opts.agentType === 'codex:codex-rescue') {
221
+ const base = 'codex:' + opts.codexModel + ':' + opts._reasoning
222
+ if (opts._usageSwitched) return base + ' (usage-switched)'
223
+ return base
224
+ }
225
+ if (opts && opts.model) return opts.model
226
+ return 'session'
227
+ }
28
228
 
29
229
  const ROUTER = { type: 'object', additionalProperties: false, required: ['tier', 'activeSteps', 'rationale'], properties: { tier: { type: 'string', enum: ['S', 'M', 'L', 'XL'] }, activeSteps: { type: 'array', items: { type: 'number' } }, rationale: { type: 'string' } } }
30
230
  const ARTIFACT = { type: 'object', additionalProperties: false, required: ['wrote', 'summary'], properties: { wrote: { type: 'array', items: { type: 'string' } }, summary: { type: 'string' } } }
@@ -32,8 +232,11 @@ const QE = { type: 'object', additionalProperties: false, required: ['grade', 'g
32
232
 
33
233
  // Step 0: Router + MANDATORY self-learning recall
34
234
  phase('Router')
35
- const routerPrompt = 'You are Step 0 (Complexity Router) of the /feature-adr pipeline. TWO jobs. (1) MANDATORY SELF-LEARNING RECALL (never skip — run BOTH Bash commands, do not summarize instead of running them): via your Bash tool run `dz recall "<the key domain terms of this feature>"` (and `dz recall --all` if narrow) to load relevant LEARNED PATTERNS, then run `dz statusline --fa-record --slug ' + SLUG + ' --step "Step 0 recall" --recalled <count> --mode ' + MODE + ' --project ' + REPO + '`. Summarize the top 3 applicable patterns in the rationale. (2) Classify S/M/L/XL + active steps. Feature: "' + DESC + '". Code: ' + CODE_HINT + '. S=1-3 files (0,1,6,7,8); M=4-10 (0,1,3,3.5,5,6,7,8); L=11-30 (all+9); XL=30+ (full+9). Return {tier, activeSteps, rationale} with the recalled patterns folded into rationale.'
36
- const router = await agent(routerPrompt, { label: 'router+recall', phase: 'Router', schema: ROUTER, effort: 'low' })
235
+ await usageProbe('Router')
236
+ const routerPrompt = 'You are Step 0 (Complexity Router) of the /feature-adr pipeline. TWO jobs. (1) MANDATORY SELF-LEARNING RECALL (never skip — run BOTH Bash commands VERBATIM, do not summarize instead of running them): the learned patterns live in the CANONICAL BRAIN store at `' + BRAIN + '` — pin every recall to it. Via your Bash tool run EXACTLY `' + DZ_RECALL('<the key domain terms of this feature>') + '` (and `' + DZ_RECALL('<the key domain terms of this feature>') + ' --all` if narrow) to load relevant LEARNED PATTERNS from the brain. Preserve recalled pattern TEXT, reward, domain, and any visible id in the rationale as a concrete list so Step 8 can compare candidate lessons against it. Then run `dz statusline --fa-record --slug ' + SLUG + ' --step "Step 0 recall" --recalled <count> --mode ' + MODE + ' --project ' + REPO + '`. Summarize the top 3 applicable patterns in the rationale. (2) Classify S/M/L/XL + active steps. Feature: "' + DESC + '". Code: ' + CODE_HINT + '. S=1-3 files (0,1,6,7,8); M=4-10 (0,1,3,3.5,5,6,7,8); L=11-30 (all+9); XL=30+ (full+9). Return {tier, activeSteps, rationale} with the recalled patterns folded into rationale.'
237
+ const routerOpts = mergeOpts({ label: 'router+recall', phase: 'Router', schema: ROUTER, effort: 'low' }, resolveStageModel('router'))
238
+ modelsUsed.router = modelLabel(routerOpts)
239
+ const router = await agent(routerPrompt, routerOpts)
37
240
  let tier = A.tier || (router ? router.tier : 'M')
38
241
  const LEARNED = router ? router.rationale : 'none recalled'
39
242
  const isMplus = tier === 'M' || tier === 'L' || tier === 'XL'
@@ -47,14 +250,27 @@ await agent('Run EXACTLY this one shell command via your Bash tool and report it
47
250
 
48
251
  // Steps 1-5: Design (tier-gated thunks built explicitly - no inline ternary-null)
49
252
  phase('Design')
253
+ await usageProbe('Design')
50
254
  const designThunks = []
51
255
  const reqExtra = isLplus ? ' Also write ' + FDIR + '/02_research.md (codebase patterns + external analogues; read the repo for the closest existing implementation to mirror).' : ''
52
- designThunks.push(() => agent('Step 1 (Requirements)' + (isLplus ? ' + Step 2 (Research)' : '') + ' of /feature-adr for "' + DESC + '" (tier ' + tier + ', slug ' + SLUG + '). Code: ' + CODE_HINT + '. APPLY these Step-0 recalled LEARNED PATTERNS (fold the applicable ones into requirements/constraints - the loop paying off): ' + LEARNED + '. Write ' + FDIR + '/01_requirements.md (functional + non-functional requirements, acceptance criteria, constraints, and an "Applied learned patterns" note).' + reqExtra + ' Return wrote[] + a 1-line summary.', { label: 'requirements', phase: 'Design', schema: ARTIFACT }))
256
+ // Resolve per-stage model opts up-front (parser-safe: no inline resolveStageModel inside the thunk arrays).
257
+ // research folds into requirements, ddd folds into architecture (single shared call) — recorded for reporting.
258
+ const reqOpts = mergeOpts({ label: 'requirements', phase: 'Design', schema: ARTIFACT }, resolveStageModel('requirements'))
259
+ const adrOpts = mergeOpts({ label: 'adr', phase: 'Design', schema: ARTIFACT }, resolveStageModel('adr'))
260
+ const qcsdOpts = mergeOpts({ label: 'qcsd', phase: 'Design', schema: ARTIFACT }, resolveStageModel('ideation'))
261
+ const archOpts = mergeOpts({ label: 'architecture', phase: 'Design', schema: ARTIFACT }, resolveStageModel('architecture'))
262
+ modelsUsed.requirements = modelLabel(reqOpts)
263
+ modelsUsed.research = modelLabel(reqOpts)
264
+ modelsUsed.adr = modelLabel(adrOpts)
265
+ modelsUsed.ideation = modelLabel(qcsdOpts)
266
+ modelsUsed.architecture = modelLabel(archOpts)
267
+ modelsUsed.ddd = modelLabel(archOpts)
268
+ designThunks.push(() => agent('Step 1 (Requirements)' + (isLplus ? ' + Step 2 (Research)' : '') + ' of /feature-adr for "' + DESC + '" (tier ' + tier + ', slug ' + SLUG + '). Code: ' + CODE_HINT + '. APPLY these Step-0 recalled LEARNED PATTERNS (fold the applicable ones into requirements/constraints - the loop paying off): ' + LEARNED + '. Write ' + FDIR + '/01_requirements.md (functional + non-functional requirements, acceptance criteria, constraints, and an "Applied learned patterns" note).' + reqExtra + ' Return wrote[] + a 1-line summary.', reqOpts))
53
269
  if (isMplus) {
54
- designThunks.push(() => agent('Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. Write ' + FDIR + '/03_adr/001-' + SLUG + '.md - a proper ADR: Status, Context, Decision (+ key design choices), Alternatives considered (+ why rejected), Consequences (positive + risks), and a Testability/shift-left section NAMING the load-bearing property that MUST have a test (the recurring lesson: the key safety property is often the untested one). Return wrote[] + summary.', { label: 'adr', phase: 'Design', schema: ARTIFACT }))
55
- designThunks.push(() => agent('Step 3.5 (QCSD ideation swarm - HTSM quality criteria + SFDIPOT risk) of /feature-adr for "' + DESC + '" (' + SLUG + '). Assess quality criteria + product-factors risk. Write ' + FDIR + '/03.5_ideation_report.md with a GO/CONDITIONAL/NO-GO verdict + top quality risks for QE. Return wrote[] + summary.', { label: 'qcsd', phase: 'Design', schema: ARTIFACT }))
270
+ designThunks.push(() => agent('Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. Write ' + FDIR + '/03_adr/001-' + SLUG + '.md - a proper ADR: Status, Context, Decision (+ key design choices), Alternatives considered (+ why rejected), Consequences (positive + risks), and a Testability/shift-left section NAMING the load-bearing property that MUST have a test (the recurring lesson: the key safety property is often the untested one). Return wrote[] + summary.', adrOpts))
271
+ designThunks.push(() => agent('Step 3.5 (QCSD ideation swarm - HTSM quality criteria + SFDIPOT risk) of /feature-adr for "' + DESC + '" (' + SLUG + '). Assess quality criteria + product-factors risk. Write ' + FDIR + '/03.5_ideation_report.md with a GO/CONDITIONAL/NO-GO verdict + top quality risks for QE. Return wrote[] + summary.', qcsdOpts))
56
272
  const archExtra = isLplus ? ' Also ' + FDIR + '/04_domain_model.md (DDD).' : ''
57
- designThunks.push(() => agent((isLplus ? 'Step 4 (DDD) + ' : '') + 'Step 5 (Architecture) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the code. Write ' + FDIR + '/05_architecture.md (components, data flow, integration points, the emit/merge/wiring shape).' + archExtra + ' Return wrote[] + summary.', { label: 'architecture', phase: 'Design', schema: ARTIFACT }))
273
+ designThunks.push(() => agent((isLplus ? 'Step 4 (DDD) + ' : '') + 'Step 5 (Architecture) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the code. Write ' + FDIR + '/05_architecture.md (components, data flow, integration points, the emit/merge/wiring shape).' + archExtra + ' Return wrote[] + summary.', archOpts))
58
274
  }
59
275
  const design = await parallel(designThunks)
60
276
 
@@ -63,11 +279,17 @@ const design = await parallel(designThunks)
63
279
  // the codex:codex-rescue runtime and GRACEFULLY FALL BACK to the default (Claude) planner if Codex is
64
280
  // unavailable/errors — the pipeline never blocks on Codex.
65
281
  phase('Plan')
66
- const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
282
+ await usageProbe('Plan')
67
283
  const planPrompt = 'Step 6 (SPARC-GOAP implementation plan) of /feature-adr for "' + DESC + '" (' + SLUG + ', tier ' + tier + '). Given the requirements + ADR + architecture in ' + FDIR + ', decompose into milestones + concrete tasks with success metrics. Write ' + FDIR + '/06_implementation_plan.md. Return wrote[] + summary.'
284
+ // Resolve the plan model. args.models.plan wins; else the planner:'codex' knob (via routingRequested +
285
+ // DEFAULT_MODELS/coder-fold) or the DEFAULT_MODELS.plan ('sonnet') under routing; else {} (BC).
286
+ const planModel = resolveStageModel('plan')
287
+ const planIsCodex = (planModel.agentType === 'codex:codex-rescue') || (MODELS.plan === undefined && PLANNER === 'codex')
68
288
  let plan = null
69
- if (PLANNER === 'codex') {
70
- const codexPlan = await agent(planPrompt, { label: 'plan:codex', phase: 'Plan', agentType: 'codex:codex-rescue' })
289
+ if (planIsCodex) {
290
+ modelsUsed.plan = (planModel.agentType === 'codex:codex-rescue') ? modelLabel(planModel) : ('codex:' + CODEX_MODEL + ':high')
291
+ const codexPlanOpts = mergeOpts({ label: 'plan:codex', phase: 'Plan', agentType: 'codex:codex-rescue' }, planModel.agentType ? planModel : {})
292
+ const codexPlan = await agent(planPrompt, codexPlanOpts)
71
293
  if (codexPlan) {
72
294
  plan = { wrote: [FDIR + '/06_implementation_plan.md'], summary: String(codexPlan).slice(0, 500), planner: 'codex' }
73
295
  log('Plan: Codex (top model)')
@@ -75,42 +297,50 @@ if (PLANNER === 'codex') {
75
297
  log('Plan: Codex unavailable — falling back to the default planner')
76
298
  }
77
299
  }
300
+ if (plan === null && planIsCodex) reactiveBelt('Plan')
78
301
  if (plan === null) {
79
- const claudePlan = await agent(planPrompt, { label: 'plan', phase: 'Plan', schema: ARTIFACT })
80
- plan = claudePlan ? { wrote: claudePlan.wrote, summary: claudePlan.summary, planner: PLANNER === 'codex' ? 'claude-fallback' : 'claude' } : null
302
+ const claudePlanOpts = mergeOpts({ label: 'plan', phase: 'Plan', schema: ARTIFACT }, planIsCodex ? {} : planModel)
303
+ modelsUsed.plan = planIsCodex ? 'claude-fallback' : modelLabel(claudePlanOpts)
304
+ const claudePlan = await agent(planPrompt, claudePlanOpts)
305
+ plan = claudePlan ? { wrote: claudePlan.wrote, summary: claudePlan.summary, planner: planIsCodex ? 'claude-fallback' : 'claude' } : null
81
306
  }
82
307
 
83
308
  // Hybrid checkpoint for L/XL
84
309
  const stopHere = STOP_AFTER === 'plan' || (isLplus && STOP_AFTER !== 'none')
85
310
  if (stopHere) {
86
- return { tier: tier, phase: 'checkpoint-after-plan', artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), note: 'L/XL checkpoint - review the ADR + plan, then re-invoke with args.stopAfter="none" to implement + QE.' }
311
+ // MED-fix: pre-compute the PLANNED code/qe/fleet labels here (resolution is PURE matches the post-
312
+ // checkpoint run), so the reviewer sees the load-bearing cross-model QE decision at the exact point
313
+ // they re-invoke. Marked `(planned)` since the stages haven't executed yet.
314
+ const codePlanned = modelLabel(resolveStageModel('code'))
315
+ const qePlanned = qeShouldUseCodex() ? modelLabel(resolveStageModel('qe')) : modelLabel(mergeOpts({ agentType: 'qe-code-reviewer' }, resolveStageModel('qe')))
316
+ const plannedModels = mergeOpts(modelsUsed, { code: codePlanned + ' (planned)', qe: qePlanned + ' (planned)' })
317
+ if (isLplus) plannedModels.fleet = modelLabel(resolveStageModel('fleet')) + ' (planned)'
318
+ return { tier: tier, phase: 'checkpoint-after-plan', artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: plannedModels, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, note: 'L/XL checkpoint - review the ADR + plan (+ the planned code/qe/fleet models), then re-invoke with args.stopAfter="none" to implement + QE.' }
87
319
  }
88
320
 
89
- // Codex fallback config (opt-in). CODER/QE_REVIEWER ∈ 'claude'|'codex'|'codex-fallback'. On
90
- // 'codex-fallback' the Claude agent runs FIRST; if it returns null (e.g. the Claude Code session limit
91
- // is exhausted mid-code/mid-QE — exactly the failure we hit before), the SAME task is retried on the
92
- // codex:codex-rescue runtime. args.codexModel is DEFAULT 'auto' (Codex self-selects the top model
93
- // available to the account — model ids are account/version-specific and move ahead of any static
94
- // default, so 'auto' is the portable choice). To hard-pin a specific id (e.g. this account's top is
95
- // gpt-5.5), the orchestrator writes it into ~/.codex/config.toml at pre-flight; the hint below only nudges.
96
- const CODEX_MODEL = A.codexModel || 'auto'
97
- const CODEX_HINT = ' (If you are the Codex runtime, prefer the ' + CODEX_MODEL + ' model.)'
98
- const CODER = (A.coder === 'codex' || A.coder === 'codex-fallback') ? A.coder : 'claude'
99
- const QE_REVIEWER = (A.qeReviewer === 'codex' || A.qeReviewer === 'codex-fallback') ? A.qeReviewer : 'claude'
100
-
101
321
  // Step 7: Code (optional Codex fallback on Claude-limit exhaustion)
102
322
  phase('Code')
323
+ await usageProbe('Code')
103
324
  const codePrompt = 'Step 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG + '). Implement the feature per the plan + ADR + architecture in ' + FDIR + '. Write the ACTUAL production code + its tests (mirror the closest existing implementation named in research/architecture). Follow repo conventions; build must pass. Write a change manifest ' + FDIR + '/07_code_changes/change_manifest.md listing every file touched. Return wrote[] (incl. real source files) + summary.'
325
+ // Resolve the coder model. args.models.code wins (a direct 'codex' spec = codex-first); else the legacy
326
+ // CODER knob drives it (with its codex-fallback null-guard). resolveStageModel('code') folds both via the
327
+ // code:null sentinel → resolveCoderSpec(). A Claude resolution merges {model} onto the Claude branch;
328
+ // under the BC omit-path it is {} (byte-identical).
329
+ const codeModel = resolveStageModel('code')
330
+ const codeIsCodexFirst = (MODELS.code !== undefined) ? (codeModel.agentType === 'codex:codex-rescue') : (CODER === 'codex')
331
+ const codeClaudeOpts = mergeOpts({ label: 'code', phase: 'Code', schema: ARTIFACT, effort: 'high' }, codeIsCodexFirst ? {} : (codeModel.agentType ? {} : codeModel))
104
332
  let code = null
105
333
  let coderUsed = 'claude'
106
- if (CODER !== 'codex') {
107
- code = await agent(codePrompt, { label: 'code', phase: 'Code', schema: ARTIFACT, effort: 'high' })
108
- if (code) coderUsed = 'claude'
334
+ if (!codeIsCodexFirst) {
335
+ code = await agent(codePrompt, codeClaudeOpts)
336
+ if (code) { coderUsed = 'claude'; modelsUsed.code = modelLabel(codeClaudeOpts) }
109
337
  }
110
- if (code === null && (CODER === 'codex' || CODER === 'codex-fallback')) {
111
- if (CODER === 'codex-fallback') log('Code: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
112
- const codexCode = await agent(codePrompt + CODEX_HINT, { label: 'code:codex', phase: 'Code', agentType: 'codex:codex-rescue' })
113
- if (codexCode) { code = { wrote: [FDIR + '/07_code_changes/change_manifest.md'], summary: String(codexCode).slice(0, 500) }; coderUsed = CODER === 'codex' ? 'codex' : 'codex-fallback' }
338
+ if (code === null && !codeIsCodexFirst) reactiveBelt('Code')
339
+ if (code === null && (codeIsCodexFirst || CODER === 'codex-fallback')) {
340
+ if (CODER === 'codex-fallback' && !codeIsCodexFirst) log('Code: Claude unavailable (limit?) falling back to Codex ' + CODEX_MODEL)
341
+ const codeCodexOpts = mergeOpts({ label: 'code:codex', phase: 'Code', agentType: 'codex:codex-rescue' }, codeModel.agentType ? codeModel : {})
342
+ const codexCode = await agent(codePrompt + CODEX_HINT, codeCodexOpts)
343
+ if (codexCode) { code = { wrote: [FDIR + '/07_code_changes/change_manifest.md'], summary: String(codexCode).slice(0, 500) }; coderUsed = codeIsCodexFirst ? 'codex' : 'codex-fallback'; modelsUsed.code = modelLabel(codeCodexOpts) }
114
344
  }
115
345
 
116
346
  // Step 7.5: Codex-landed barrier. Codex applies edits OUT-OF-BAND via its own runtime; without this,
@@ -127,26 +357,52 @@ if (coderUsed === 'codex' || coderUsed === 'codex-fallback') {
127
357
 
128
358
  // Step 8: QE (brutal-honesty, agentic-qe) + MANDATORY teach
129
359
  phase('QE')
130
- 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 - the recurring lesson). Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md. MANDATORY SELF-LEARNING STORE (close the loop, never skip): via Bash run `dz teach "<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>" --reward <0.7-0.95> --domain <area>` for each genuine lesson (1-3 max, high-signal), then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled 3 --stored <count taught> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. Return {grade, gaps, codeTestsAdequate, docTestsPresent}.' + landedNote
360
+ await usageProbe('QE')
361
+ 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 - the recurring lesson). Grade A/B/C/D honestly. Assess code-test adequacy + doc-test presence. List CONFIRMED gaps with severity. Write ' + FDIR + '/08_qe_report.md. MANDATORY SELF-LEARNING STORE (close the loop, never skip): compare every candidate lesson against the Step-0 recalled LEARNED patterns above. Teach ONLY lessons NOT covered by Step-0 recall. On overlap, run `dz teach --reinforce "<recalled pattern id or exact text>" --project ' + BRAIN + '` instead of minting a near-duplicate; if --reinforce is unavailable, skip the duplicate teach and report `reinforced existing pattern <id>` in the QE report. Store every genuinely new lesson in the CANONICAL BRAIN store at `' + BRAIN + '` so it is NOT lost to a target repo you may have cd`d into. Via Bash run EXACTLY `' + DZ_TEACH('<a durable reusable lesson from this feature - a rule/pattern/pitfall, NOT a checkpoint echo>', '<0.7-0.95>', '<area>') + '` for each genuine NEW lesson (1-3 max, high-signal) — the `cd ' + BRAIN + ' &&` prefix + `--project ' + BRAIN + '` pin guarantee the lesson lands in the brain regardless of your CWD. Then run `' + DZ + ' statusline --fa-record --slug ' + SLUG + ' --step "Step 8 QE" --recalled 3 --stored <count taught> --reinforced <count reinforced> --mode ' + MODE + ' --project ' + REPO + '` (run it verbatim via Bash, do not skip). Do NOT teach trivia or invent gaps. Return {grade, gaps, codeTestsAdequate, docTestsPresent}.' + landedNote
362
+ // CROSS-MODEL QE (load-bearing): resolveStageModel('qe') derives the OTHER family than the resolved
363
+ // coder when args.models.qe is unset (coder-codex ⇒ opus; coder-Claude ⇒ codex, or opus if codex absent).
364
+ // An explicit args.models.qe wins. A Claude qe spec is merged onto the qe-code-reviewer base (role
365
+ // PRESERVED); a codex qe spec REPLACES agentType with codex:codex-rescue (as today). The codex-null→
366
+ // Claude guard is retained as the runtime belt so codex-unavailable never blocks.
367
+ const qeModel = resolveStageModel('qe')
368
+ // Single tested source of truth (feature-adr-routing.ts:qeShouldUseCodex) — closes the self-QE hole where
369
+ // the legacy qeReviewer='codex' knob used to re-route QE back to codex even when the CODER was codex.
370
+ 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)')
371
+ const qeIsCodex = qeShouldUseCodex()
372
+ const qeClaudeOpts = mergeOpts({ label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, qeIsCodex ? {} : qeModel)
131
373
  let qe = null
132
374
  let qeReviewerUsed = 'claude'
133
- if (QE_REVIEWER !== 'codex') {
134
- qe = await agent(qePrompt, { label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE })
135
- if (qe) qeReviewerUsed = 'claude'
375
+ if (!qeIsCodex) {
376
+ qe = await agent(qePrompt, qeClaudeOpts)
377
+ if (qe) { qeReviewerUsed = 'claude'; modelsUsed.qe = modelLabel(qeClaudeOpts) }
378
+ }
379
+ if (qe === null && !qeIsCodex) reactiveBelt('QE')
380
+ if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
381
+ if (QE_REVIEWER === 'codex-fallback' && !qeIsCodex) log('QE: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
382
+ const qeCodexOpts = mergeOpts({ label: 'qe:codex', phase: 'QE', agentType: 'codex:codex-rescue' }, qeModel.agentType ? qeModel : {})
383
+ const codexQe = await agent(qePrompt + CODEX_HINT, qeCodexOpts)
384
+ if (codexQe) { qe = { grade: 'codex-review', gaps: [], codeTestsAdequate: null, docTestsPresent: null, summary: String(codexQe).slice(0, 500) }; qeReviewerUsed = qeIsCodex ? 'codex' : 'codex-fallback'; modelsUsed.qe = modelLabel(qeCodexOpts) }
136
385
  }
137
- if (qe === null && (QE_REVIEWER === 'codex' || QE_REVIEWER === 'codex-fallback')) {
138
- if (QE_REVIEWER === 'codex-fallback') log('QE: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
139
- const codexQe = await agent(qePrompt + CODEX_HINT, { label: 'qe:codex', phase: 'QE', agentType: 'codex:codex-rescue' })
140
- if (codexQe) { qe = { grade: 'codex-review', gaps: [], codeTestsAdequate: null, docTestsPresent: null, summary: String(codexQe).slice(0, 500) }; qeReviewerUsed = QE_REVIEWER === 'codex' ? 'codex' : 'codex-fallback' }
386
+ // Belt: if a codex-first QE returned null (codex unavailable), fall back to a Claude reviewer — never block.
387
+ if (qe === null && qeIsCodex) {
388
+ log('QE: Codex unavailable falling back to a Claude reviewer (cross-model belt)')
389
+ const qeBeltOpts = mergeOpts({ label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, routingRequested ? { model: 'opus' } : {})
390
+ qe = await agent(qePrompt, qeBeltOpts)
391
+ if (qe) { qeReviewerUsed = 'claude'; modelsUsed.qe = modelLabel(qeBeltOpts) }
141
392
  }
142
393
 
143
394
  // Step 9: Fleet QE (L/XL)
144
395
  let fleet = 'skipped (S/M)'
145
396
  if (isLplus) {
146
397
  phase('FleetQE')
398
+ await usageProbe('FleetQE')
399
+ const fleetModel = resolveStageModel('fleet')
400
+ modelsUsed.fleet = modelLabel(mergeOpts({}, fleetModel))
401
+ const fleetTraceOpts = mergeOpts({ label: 'fleet:trace', phase: 'FleetQE', agentType: 'qe-requirements-validator' }, fleetModel)
402
+ const fleetCovOpts = mergeOpts({ label: 'fleet:cov', phase: 'FleetQE', agentType: 'qe-coverage-specialist' }, fleetModel)
147
403
  const fleetThunks = [
148
- () => agent('Step 9 fleet-QE (requirements traceability + risk) for ' + SLUG + ': map ADR decisions to code to tests; flag orphans + high risk. Write ' + FDIR + '/09_fleet_qe_assessment.md.', { label: 'fleet:trace', phase: 'FleetQE', agentType: 'qe-requirements-validator' }),
149
- () => agent('Step 9 fleet-QE (coverage + regression) for ' + SLUG + ': risk-weighted coverage gaps + regression selection for the changed files. Append to ' + FDIR + '/09_fleet_qe_assessment.md.', { label: 'fleet:cov', phase: 'FleetQE', agentType: 'qe-coverage-specialist' }),
404
+ () => agent('Step 9 fleet-QE (requirements traceability + risk) for ' + SLUG + ': map ADR decisions to code to tests; flag orphans + high risk. Write ' + FDIR + '/09_fleet_qe_assessment.md.', fleetTraceOpts),
405
+ () => agent('Step 9 fleet-QE (coverage + regression) for ' + SLUG + ': risk-weighted coverage gaps + regression selection for the changed files. Append to ' + FDIR + '/09_fleet_qe_assessment.md.', fleetCovOpts),
150
406
  ]
151
407
  await parallel(fleetThunks)
152
408
  fleet = 'run'
@@ -167,6 +423,10 @@ return {
167
423
  coderUsed: coderUsed,
168
424
  qeReviewerUsed: qeReviewerUsed,
169
425
  codexModel: CODEX_MODEL,
426
+ modelsUsed: modelsUsed,
427
+ usageEvents: usageEvents,
428
+ usageThreshold: USAGE_THRESHOLD,
170
429
  selfLearning: 'recall@Step0 + teach@Step8 (mandatory)',
430
+ brain: BRAIN,
171
431
  promiseTags: tags,
172
432
  }