@dzhechkov/skills-feature-adr 1.3.34 → 1.3.38

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.34",
3
+ "version": "1.3.38",
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"
@@ -60,6 +60,12 @@ Without this, the Code agent returns null and the run stalls. With `coder: 'code
60
60
  detects the null, logs *"Claude unavailable (limit?) — falling back to Codex auto"*, and finishes
61
61
  the code + tests on Codex — no restart, no lost work.
62
62
 
63
+ **Codex-landed barrier (Step 7.5):** Codex applies edits OUT-OF-BAND via its own runtime, so a naive
64
+ pipeline runs Step-8 QE before the async write flushes and false-grades *"Step 7 never ran"* (grade D on
65
+ real, landed code — observed on the goap-ed25519 fix). When Codex was the coder, feature-adr now polls
66
+ `git status` (excluding pipeline artifacts) up to ~30s until the code changes appear and hands the
67
+ confirmed file list to QE. Claude-coded runs are synchronous, so the barrier is skipped (zero cost).
68
+
63
69
  **Example invocations:**
64
70
  ```js
65
71
  // Plan on Codex, code+QE fall back to Codex only if Claude runs out, auto:
@@ -366,7 +366,7 @@ to Codex — always ASKING first, always with a Claude fallback (never blocks):
366
366
  - **Model choice** → `args.codexModel` (`auto` default (Codex self-selects) · or an id your account exposes (e.g. `gpt-5.5`)).
367
367
 
368
368
  *Scenario:* an L/XL run hits the session limit during coding → with `coder: 'codex-fallback'` feature-adr
369
- logs *"Claude unavailable (limit?) — falling back to Codex auto"* and finishes on Codex, no restart.
369
+ logs *"Claude unavailable (limit?) — falling back to Codex auto"* and finishes on Codex, no restart. Codex writes out-of-band, so feature-adr confirms the edits LANDED (`git status`, ~30s poll) before QE — no false "Step 7 never ran" grade.
370
370
 
371
371
  *Example:* `Workflow({ scriptPath: '.claude/workflows/feature-adr.js', args: { slug, description, tier:
372
372
  'M', planner: 'codex', coder: 'codex-fallback', qeReviewer: 'codex-fallback', codexModel: 'auto' } }) // 'auto' = Codex picks top; or pin e.g. 'gpt-5.5'`.
@@ -375,6 +375,45 @@ 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
+
378
417
  ### Pattern memory loop (self-learning — runs in ALL modes)
379
418
 
380
419
  **Self-learning is MANDATORY on EVERY `/feature-adr` run — including plain `/feature-adr` without any
@@ -408,6 +447,15 @@ non-blocking — an error or empty result never stalls the pipeline. This layer
408
447
  **distinct from the Keysarium reward layer** installed by `--with-learning`: it uses agentic-qe MCP
409
448
  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
449
 
450
+ **Canonical brain store — `args.brain` (never fragment the loop).** The dz durable loop only compounds if
451
+ Step-0 recall and Step-8 teach hit the SAME store. The workflow pins both to a canonical **brain** —
452
+ `args.brain`, default = the workspace root — via `cd <brain> && dz recall/teach … --project <brain>`, so a
453
+ Step-8 teach issued from a coder that `cd`'d into a target repo still lands in the brain, not that repo's
454
+ `.dz`. Omitting `args.brain` is behaviorally inert for a workspace-CWD run (`brain === repo`). **Share** a
455
+ brain: `dz recall --all --json > patterns.json` → `dz teach --from-json patterns.json --project <brain>`
456
+ (exact-text dedup, idempotent). **Recover** a fragmented store: `cd <stray-repo> && dz recall --all --json >
457
+ /tmp/stray.json` → `dz teach --from-json /tmp/stray.json --project <brain>` to merge it back into the brain.
458
+
411
459
  **Live learning panel (`dz statusline`).** The pipeline **drives** the panel: at each pattern-memory-loop
412
460
  step above it records its live state via `dz statusline --fa-record …`, so `dz statusline` can surface
413
461
  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,125 @@ 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')
71
+ const modelsUsed = {}
72
+
73
+ function specToOpts(spec) {
74
+ if (!spec) return {}
75
+ const parts = String(spec).split(':')
76
+ if (parts[0] === 'codex') {
77
+ let id = parts[1] || CODEX_MODEL
78
+ if (id !== 'auto' && !KNOWN_CODEX[id]) { log('models: unknown codex id ' + id + ' — using ' + CODEX_MODEL); id = CODEX_MODEL }
79
+ const reasoning = parts[2] || 'high'
80
+ return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning }
81
+ }
82
+ if (CLAUDE_NAMES[parts[0]]) return { model: parts[0] }
83
+ log('models: unknown spec ' + spec + ' — session-inherited')
84
+ return {}
85
+ }
86
+
87
+ function resolveCoderSpec() {
88
+ if (CODER === 'codex' || CODER === 'codex-fallback') return 'codex:' + CODEX_MODEL + ':high'
89
+ return 'opus'
90
+ }
91
+
92
+ function coderIsCodex() {
93
+ if (CODER === 'codex' || CODER === 'codex-fallback') return true
94
+ const codeSpec = MODELS.code
95
+ if (codeSpec && String(codeSpec).split(':')[0] === 'codex') return true
96
+ return false
97
+ }
98
+
99
+ function resolveQeSpec() {
100
+ if (coderIsCodex()) return 'opus'
101
+ const CODEX_AVAILABLE = A.codexAvailable !== false
102
+ if (!CODEX_AVAILABLE) return 'opus'
103
+ let top = CODEX_MODEL
104
+ if (top === 'auto') {
105
+ const ids = Object.keys(KNOWN_CODEX)
106
+ for (let i = 0; i < ids.length; i++) { if (ids[i] !== 'auto') top = ids[i] || top }
107
+ }
108
+ return 'codex:' + top + ':high'
109
+ }
110
+
111
+ // qeShouldUseCodex: the load-bearing cross-model gate — the model that wrote the code must NEVER self-QE.
112
+ // (1) explicit MODELS.qe wins; (2) legacy QE_REVIEWER==='codex' knob honored ONLY when coder is NOT codex
113
+ // (a codex coder + qeReviewer:'codex' would be codex-self-QE); (3) else the cross-model default decides.
114
+ function qeShouldUseCodex() {
115
+ const explicit = MODELS.qe
116
+ if (explicit !== undefined && explicit !== null) {
117
+ return String(explicit).split(':')[0] === 'codex'
118
+ }
119
+ if (QE_REVIEWER === 'codex') return !coderIsCodex()
120
+ return routingRequested && resolveQeSpec().split(':')[0] === 'codex'
121
+ }
122
+
123
+ function resolveStageModel(stage) {
124
+ let spec = MODELS[stage]
125
+ if (spec === undefined) {
126
+ if (!routingRequested) return {}
127
+ spec = DEFAULT_MODELS[stage]
128
+ }
129
+ if (stage === 'code' && (spec === null || spec === undefined)) return specToOpts(resolveCoderSpec())
130
+ if (stage === 'qe' && (spec === null || spec === undefined)) return specToOpts(resolveQeSpec())
131
+ return specToOpts(spec)
132
+ }
133
+
134
+ function mergeOpts(base, extra) {
135
+ const out = {}
136
+ for (const k in base) out[k] = base[k]
137
+ for (const k in extra) out[k] = extra[k]
138
+ return out
139
+ }
140
+
141
+ // modelLabel: record the resolved spec for a stage in modelsUsed (for the run report / who-did-what).
142
+ function modelLabel(opts) {
143
+ if (opts && opts.agentType === 'codex:codex-rescue') return 'codex:' + opts.codexModel + ':' + opts._reasoning
144
+ if (opts && opts.model) return opts.model
145
+ return 'session'
146
+ }
28
147
 
29
148
  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
149
  const ARTIFACT = { type: 'object', additionalProperties: false, required: ['wrote', 'summary'], properties: { wrote: { type: 'array', items: { type: 'string' } }, summary: { type: 'string' } } }
@@ -32,8 +151,10 @@ const QE = { type: 'object', additionalProperties: false, required: ['grade', 'g
32
151
 
33
152
  // Step 0: Router + MANDATORY self-learning recall
34
153
  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' })
154
+ 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, 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.'
155
+ const routerOpts = mergeOpts({ label: 'router+recall', phase: 'Router', schema: ROUTER, effort: 'low' }, resolveStageModel('router'))
156
+ modelsUsed.router = modelLabel(routerOpts)
157
+ const router = await agent(routerPrompt, routerOpts)
37
158
  let tier = A.tier || (router ? router.tier : 'M')
38
159
  const LEARNED = router ? router.rationale : 'none recalled'
39
160
  const isMplus = tier === 'M' || tier === 'L' || tier === 'XL'
@@ -49,12 +170,24 @@ await agent('Run EXACTLY this one shell command via your Bash tool and report it
49
170
  phase('Design')
50
171
  const designThunks = []
51
172
  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 }))
173
+ // Resolve per-stage model opts up-front (parser-safe: no inline resolveStageModel inside the thunk arrays).
174
+ // research folds into requirements, ddd folds into architecture (single shared call) — recorded for reporting.
175
+ const reqOpts = mergeOpts({ label: 'requirements', phase: 'Design', schema: ARTIFACT }, resolveStageModel('requirements'))
176
+ const adrOpts = mergeOpts({ label: 'adr', phase: 'Design', schema: ARTIFACT }, resolveStageModel('adr'))
177
+ const qcsdOpts = mergeOpts({ label: 'qcsd', phase: 'Design', schema: ARTIFACT }, resolveStageModel('ideation'))
178
+ const archOpts = mergeOpts({ label: 'architecture', phase: 'Design', schema: ARTIFACT }, resolveStageModel('architecture'))
179
+ modelsUsed.requirements = modelLabel(reqOpts)
180
+ modelsUsed.research = modelLabel(reqOpts)
181
+ modelsUsed.adr = modelLabel(adrOpts)
182
+ modelsUsed.ideation = modelLabel(qcsdOpts)
183
+ modelsUsed.architecture = modelLabel(archOpts)
184
+ modelsUsed.ddd = modelLabel(archOpts)
185
+ 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
186
  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 }))
187
+ 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))
188
+ 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
189
  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 }))
190
+ 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
191
  }
59
192
  const design = await parallel(designThunks)
60
193
 
@@ -63,11 +196,16 @@ const design = await parallel(designThunks)
63
196
  // the codex:codex-rescue runtime and GRACEFULLY FALL BACK to the default (Claude) planner if Codex is
64
197
  // unavailable/errors — the pipeline never blocks on Codex.
65
198
  phase('Plan')
66
- const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
67
199
  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.'
200
+ // Resolve the plan model. args.models.plan wins; else the planner:'codex' knob (via routingRequested +
201
+ // DEFAULT_MODELS/coder-fold) or the DEFAULT_MODELS.plan ('sonnet') under routing; else {} (BC).
202
+ const planModel = resolveStageModel('plan')
203
+ const planIsCodex = (planModel.agentType === 'codex:codex-rescue') || (MODELS.plan === undefined && PLANNER === 'codex')
68
204
  let plan = null
69
- if (PLANNER === 'codex') {
70
- const codexPlan = await agent(planPrompt, { label: 'plan:codex', phase: 'Plan', agentType: 'codex:codex-rescue' })
205
+ if (planIsCodex) {
206
+ modelsUsed.plan = (planModel.agentType === 'codex:codex-rescue') ? modelLabel(planModel) : ('codex:' + CODEX_MODEL + ':high')
207
+ const codexPlanOpts = mergeOpts({ label: 'plan:codex', phase: 'Plan', agentType: 'codex:codex-rescue' }, planModel.agentType ? planModel : {})
208
+ const codexPlan = await agent(planPrompt, codexPlanOpts)
71
209
  if (codexPlan) {
72
210
  plan = { wrote: [FDIR + '/06_implementation_plan.md'], summary: String(codexPlan).slice(0, 500), planner: 'codex' }
73
211
  log('Plan: Codex (top model)')
@@ -76,65 +214,105 @@ if (PLANNER === 'codex') {
76
214
  }
77
215
  }
78
216
  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
217
+ const claudePlanOpts = mergeOpts({ label: 'plan', phase: 'Plan', schema: ARTIFACT }, planIsCodex ? {} : planModel)
218
+ modelsUsed.plan = planIsCodex ? 'claude-fallback' : modelLabel(claudePlanOpts)
219
+ const claudePlan = await agent(planPrompt, claudePlanOpts)
220
+ plan = claudePlan ? { wrote: claudePlan.wrote, summary: claudePlan.summary, planner: planIsCodex ? 'claude-fallback' : 'claude' } : null
81
221
  }
82
222
 
83
223
  // Hybrid checkpoint for L/XL
84
224
  const stopHere = STOP_AFTER === 'plan' || (isLplus && STOP_AFTER !== 'none')
85
225
  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.' }
226
+ // MED-fix: pre-compute the PLANNED code/qe/fleet labels here (resolution is PURE matches the post-
227
+ // checkpoint run), so the reviewer sees the load-bearing cross-model QE decision at the exact point
228
+ // they re-invoke. Marked `(planned)` since the stages haven't executed yet.
229
+ const codePlanned = modelLabel(resolveStageModel('code'))
230
+ const qePlanned = qeShouldUseCodex() ? modelLabel(resolveStageModel('qe')) : modelLabel(mergeOpts({ agentType: 'qe-code-reviewer' }, resolveStageModel('qe')))
231
+ const plannedModels = mergeOpts(modelsUsed, { code: codePlanned + ' (planned)', qe: qePlanned + ' (planned)' })
232
+ if (isLplus) plannedModels.fleet = modelLabel(resolveStageModel('fleet')) + ' (planned)'
233
+ return { tier: tier, phase: 'checkpoint-after-plan', artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: plannedModels, 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
234
  }
88
235
 
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
236
  // Step 7: Code (optional Codex fallback on Claude-limit exhaustion)
102
237
  phase('Code')
103
238
  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.'
239
+ // Resolve the coder model. args.models.code wins (a direct 'codex' spec = codex-first); else the legacy
240
+ // CODER knob drives it (with its codex-fallback null-guard). resolveStageModel('code') folds both via the
241
+ // code:null sentinel → resolveCoderSpec(). A Claude resolution merges {model} onto the Claude branch;
242
+ // under the BC omit-path it is {} (byte-identical).
243
+ const codeModel = resolveStageModel('code')
244
+ const codeIsCodexFirst = (MODELS.code !== undefined) ? (codeModel.agentType === 'codex:codex-rescue') : (CODER === 'codex')
245
+ const codeClaudeOpts = mergeOpts({ label: 'code', phase: 'Code', schema: ARTIFACT, effort: 'high' }, codeIsCodexFirst ? {} : (codeModel.agentType ? {} : codeModel))
104
246
  let code = null
105
247
  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'
248
+ if (!codeIsCodexFirst) {
249
+ code = await agent(codePrompt, codeClaudeOpts)
250
+ if (code) { coderUsed = 'claude'; modelsUsed.code = modelLabel(codeClaudeOpts) }
251
+ }
252
+ if (code === null && (codeIsCodexFirst || CODER === 'codex-fallback')) {
253
+ if (CODER === 'codex-fallback' && !codeIsCodexFirst) log('Code: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
254
+ const codeCodexOpts = mergeOpts({ label: 'code:codex', phase: 'Code', agentType: 'codex:codex-rescue' }, codeModel.agentType ? codeModel : {})
255
+ const codexCode = await agent(codePrompt + CODEX_HINT, codeCodexOpts)
256
+ 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) }
109
257
  }
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' }
258
+
259
+ // Step 7.5: Codex-landed barrier. Codex applies edits OUT-OF-BAND via its own runtime; without this,
260
+ // Step-8 QE reads the tree before the async write flushes and false-grades "Step 7 never ran" (grade D
261
+ // on real, landed code observed on the goap-ed25519 crypto fix). Poll git status (excluding pipeline
262
+ // artifacts) up to ~30s until real code changes appear, and hand the confirmed file list to QE so it
263
+ // reviews the ACTUAL landed changes. Claude-coded runs are synchronous → this barrier is skipped.
264
+ let landedNote = ''
265
+ if (coderUsed === 'codex' || coderUsed === 'codex-fallback') {
266
+ const barrierCmd = 'for i in 1 2 3 4 5 6; do n=$(git -C ' + REPO + ' status --porcelain 2>/dev/null | grep -vE "features/|[.]dz/|[.]agentic-qe/|roam/" | wc -l); [ "$n" -gt 0 ] && break; sleep 5; done; echo "changed=$n"; git -C ' + REPO + ' status --porcelain 2>/dev/null | grep -vE "features/|[.]dz/|[.]agentic-qe/|roam/" | head -40'
267
+ const probe = await agent('Confirm the Codex Step-7 edits have LANDED in the working tree BEFORE QE runs (Codex writes out-of-band). Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + barrierCmd, { label: 'code:confirm-landed', phase: 'Code' })
268
+ landedNote = '\n\nCODEX-CODED (out-of-band): review the CONFIRMED landed working-tree changes below — do NOT report "Step 7 never ran" if files are listed. If changed=0 the implementation genuinely did not land, then grade accordingly.\n' + String(probe || '(landed-probe failed)').slice(0, 1500)
114
269
  }
115
270
 
116
271
  // Step 8: QE (brutal-honesty, agentic-qe) + MANDATORY teach
117
272
  phase('QE')
118
- 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}.'
273
+ 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): store every 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 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> --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
274
+ // CROSS-MODEL QE (load-bearing): resolveStageModel('qe') derives the OTHER family than the resolved
275
+ // coder when args.models.qe is unset (coder-codex ⇒ opus; coder-Claude ⇒ codex, or opus if codex absent).
276
+ // An explicit args.models.qe wins. A Claude qe spec is merged onto the qe-code-reviewer base (role
277
+ // PRESERVED); a codex qe spec REPLACES agentType with codex:codex-rescue (as today). The codex-null→
278
+ // Claude guard is retained as the runtime belt so codex-unavailable never blocks.
279
+ const qeModel = resolveStageModel('qe')
280
+ // Single tested source of truth (feature-adr-routing.ts:qeShouldUseCodex) — closes the self-QE hole where
281
+ // the legacy qeReviewer='codex' knob used to re-route QE back to codex even when the CODER was codex.
282
+ 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)')
283
+ const qeIsCodex = qeShouldUseCodex()
284
+ const qeClaudeOpts = mergeOpts({ label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, qeIsCodex ? {} : qeModel)
119
285
  let qe = null
120
286
  let qeReviewerUsed = 'claude'
121
- if (QE_REVIEWER !== 'codex') {
122
- qe = await agent(qePrompt, { label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE })
123
- if (qe) qeReviewerUsed = 'claude'
287
+ if (!qeIsCodex) {
288
+ qe = await agent(qePrompt, qeClaudeOpts)
289
+ if (qe) { qeReviewerUsed = 'claude'; modelsUsed.qe = modelLabel(qeClaudeOpts) }
290
+ }
291
+ if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
292
+ if (QE_REVIEWER === 'codex-fallback' && !qeIsCodex) log('QE: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
293
+ const qeCodexOpts = mergeOpts({ label: 'qe:codex', phase: 'QE', agentType: 'codex:codex-rescue' }, qeModel.agentType ? qeModel : {})
294
+ const codexQe = await agent(qePrompt + CODEX_HINT, qeCodexOpts)
295
+ 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) }
124
296
  }
125
- if (qe === null && (QE_REVIEWER === 'codex' || QE_REVIEWER === 'codex-fallback')) {
126
- if (QE_REVIEWER === 'codex-fallback') log('QE: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
127
- const codexQe = await agent(qePrompt + CODEX_HINT, { label: 'qe:codex', phase: 'QE', agentType: 'codex:codex-rescue' })
128
- if (codexQe) { qe = { grade: 'codex-review', gaps: [], codeTestsAdequate: null, docTestsPresent: null, summary: String(codexQe).slice(0, 500) }; qeReviewerUsed = QE_REVIEWER === 'codex' ? 'codex' : 'codex-fallback' }
297
+ // Belt: if a codex-first QE returned null (codex unavailable), fall back to a Claude reviewer — never block.
298
+ if (qe === null && qeIsCodex) {
299
+ log('QE: Codex unavailable falling back to a Claude reviewer (cross-model belt)')
300
+ const qeBeltOpts = mergeOpts({ label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, routingRequested ? { model: 'opus' } : {})
301
+ qe = await agent(qePrompt, qeBeltOpts)
302
+ if (qe) { qeReviewerUsed = 'claude'; modelsUsed.qe = modelLabel(qeBeltOpts) }
129
303
  }
130
304
 
131
305
  // Step 9: Fleet QE (L/XL)
132
306
  let fleet = 'skipped (S/M)'
133
307
  if (isLplus) {
134
308
  phase('FleetQE')
309
+ const fleetModel = resolveStageModel('fleet')
310
+ modelsUsed.fleet = modelLabel(mergeOpts({}, fleetModel))
311
+ const fleetTraceOpts = mergeOpts({ label: 'fleet:trace', phase: 'FleetQE', agentType: 'qe-requirements-validator' }, fleetModel)
312
+ const fleetCovOpts = mergeOpts({ label: 'fleet:cov', phase: 'FleetQE', agentType: 'qe-coverage-specialist' }, fleetModel)
135
313
  const fleetThunks = [
136
- () => 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' }),
137
- () => 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' }),
314
+ () => 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),
315
+ () => 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),
138
316
  ]
139
317
  await parallel(fleetThunks)
140
318
  fleet = 'run'
@@ -155,6 +333,8 @@ return {
155
333
  coderUsed: coderUsed,
156
334
  qeReviewerUsed: qeReviewerUsed,
157
335
  codexModel: CODEX_MODEL,
336
+ modelsUsed: modelsUsed,
158
337
  selfLearning: 'recall@Step0 + teach@Step8 (mandatory)',
338
+ brain: BRAIN,
159
339
  promiseTags: tags,
160
340
  }