@dzhechkov/skills-feature-adr 1.3.43 → 1.3.45

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
@@ -124,6 +124,15 @@ writing code. Without this the run stalls; with `coder: 'codex-fallback'` the pi
124
124
  unavailable (limit?) — falling back to Codex auto"* and finishes the code + tests on Codex, no
125
125
  restart and no lost work.
126
126
 
127
+ **Codex writes are out-of-band — the Step-7.5 landed barrier waits for them.** Codex applies edits via
128
+ its own runtime, so a naive pipeline runs QE before the async write flushes and false-grades *"Step 7
129
+ never ran"* on real code. For a Codex-coded run the pipeline polls a **bounded 120s backing-off window**
130
+ (`1,2,2,5,5,10,10,15,20,25,25`s), preferring the code stage's *declared expected files* when known, and
131
+ emits an explicit `changed=0 after 120s — genuinely not landed` only when the window truly expires — so
132
+ QE distinguishes "not implemented" from "not yet flushed". A Claude-coded run is synchronous and skips
133
+ the barrier entirely (zero added wait). Note: if Codex flushes slower than 120s, re-verify after the run
134
+ rather than trusting the end-of-run grade.
135
+
127
136
  ```bash
128
137
  # Headless login on a VPS (no browser):
129
138
  codex login --device-auth # prints a code + URL you approve on another device
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.3.43",
3
+ "version": "1.3.45",
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"
@@ -47,6 +47,12 @@ const DZ_TEACH = (lesson, reward, domain) =>
47
47
  // ~/.codex/config.toml at pre-flight; the hint below only nudges.
48
48
  const CODEX_MODEL = A.codexModel || 'auto'
49
49
  const CODEX_HINT = ' (If you are the Codex runtime, prefer the ' + CODEX_MODEL + ' model.)'
50
+ function codexEffortHint(opts) {
51
+ if (opts && opts.agentType === 'codex:codex-rescue' && opts._reasoning) {
52
+ return ' (If you are the Codex runtime, run at --effort ' + opts._reasoning + '.)'
53
+ }
54
+ return ''
55
+ }
50
56
  const CODER = (A.coder === 'codex' || A.coder === 'codex-fallback') ? A.coder : 'claude'
51
57
  const QE_REVIEWER = (A.qeReviewer === 'codex' || A.qeReviewer === 'codex-fallback') ? A.qeReviewer : 'claude'
52
58
  const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
@@ -66,6 +72,7 @@ const PLANNER = (A.planner === 'codex') ? 'codex' : 'claude'
66
72
  const MODELS = (A.models && typeof A.models === 'object') ? A.models : {}
67
73
  const KNOWN_CODEX = { 'auto': 1, 'gpt-5.5': 1, 'gpt-5.6': 1 }
68
74
  const CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 }
75
+ const VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1 }
69
76
  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
77
  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
78
  const modelsUsed = {}
@@ -143,7 +150,8 @@ function specToOpts(spec) {
143
150
  if (parts[0] === 'codex') {
144
151
  let id = parts[1] || CODEX_MODEL
145
152
  if (id !== 'auto' && !KNOWN_CODEX[id]) { log('models: unknown codex id ' + id + ' — using ' + CODEX_MODEL); id = CODEX_MODEL }
146
- const reasoning = parts[2] || 'high'
153
+ let reasoning = parts[2] || 'high'
154
+ if (!VALID_REASONING[reasoning]) { log('models: unknown reasoning ' + reasoning + ' — using high'); reasoning = 'high' }
147
155
  return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning }
148
156
  }
149
157
  if (CLAUDE_NAMES[parts[0]]) return { model: parts[0] }
@@ -240,6 +248,116 @@ function stageLabel(baseLabel, opts) {
240
248
  // Codex-QE run, do ZERO extra work (byte-identical to today).
241
249
  function needsLandedBarrier(opts) { return !!(opts && opts.agentType === 'codex:codex-rescue') }
242
250
 
251
+ const DEFAULT_CODE_LANDING_MAX_WAIT_MS = 120000
252
+ const DEFAULT_CODE_LANDING_BACKOFF_MS = [1000, 2000, 2000, 5000, 5000, 10000, 10000, 15000, 20000, 25000, 25000]
253
+ const CODE_LANDED_BARRIER_SLEEPS_SECONDS = DEFAULT_CODE_LANDING_BACKOFF_MS.map((ms) => ms / 1000)
254
+ const CODE_LANDING_PIPELINE_PREFIXES = ['features/', '.dz/', '.agentic-qe/', 'roam/']
255
+
256
+ function codeLandingEmptySignal(seconds) {
257
+ return 'changed=0 after ' + seconds + 's — genuinely not landed'
258
+ }
259
+
260
+ function needsCodeLandedBarrier(coderUsed) {
261
+ return coderUsed === 'codex' || coderUsed === 'codex-fallback'
262
+ }
263
+
264
+ function normalizeCodeLandingPath(path) {
265
+ let p = String(path || '').trim().replace(/\\/g, '/')
266
+ while (p.indexOf('./') === 0) p = p.slice(2)
267
+ p = p.replace(/\/+/g, '/')
268
+ if (!p) return ''
269
+ if (p[0] === '/') return ''
270
+ if (p === '..' || p.indexOf('../') === 0 || p.indexOf('/../') >= 0 || p.endsWith('/..')) return ''
271
+ if (/[\0\r\n\t "'\x60$;&|<>*?()[\]{}!]/.test(p)) return ''
272
+ if (p.endsWith('/')) return ''
273
+ for (let i = 0; i < CODE_LANDING_PIPELINE_PREFIXES.length; i++) {
274
+ const prefix = CODE_LANDING_PIPELINE_PREFIXES[i]
275
+ const bare = prefix.slice(0, -1)
276
+ if (p === bare || p.indexOf(prefix) === 0) return ''
277
+ }
278
+ return p
279
+ }
280
+
281
+ function filterPollableCodePaths(paths) {
282
+ const out = []
283
+ const seen = {}
284
+ for (let i = 0; i < (paths || []).length; i++) {
285
+ const normalized = normalizeCodeLandingPath(paths[i])
286
+ if (!normalized || seen[normalized]) continue
287
+ seen[normalized] = 1
288
+ out.push(normalized)
289
+ }
290
+ return out
291
+ }
292
+
293
+ function codeLandedBarrierPlan(coderUsed, expectedPaths) {
294
+ const enabled = needsCodeLandedBarrier(coderUsed)
295
+ const pollWindowSeconds = DEFAULT_CODE_LANDING_MAX_WAIT_MS / 1000
296
+ if (!enabled) {
297
+ return { enabled: false, mode: 'any-code-change', sleepsMs: [], sleepsSeconds: [], pollWindowMs: 0, pollWindowSeconds: 0, expectedPaths: [], emptySignal: '' }
298
+ }
299
+ const filteredExpectedPaths = filterPollableCodePaths(expectedPaths || [])
300
+ return {
301
+ enabled: true,
302
+ mode: filteredExpectedPaths.length > 0 ? 'expected-files' : 'any-code-change',
303
+ sleepsMs: DEFAULT_CODE_LANDING_BACKOFF_MS,
304
+ sleepsSeconds: CODE_LANDED_BARRIER_SLEEPS_SECONDS,
305
+ pollWindowMs: DEFAULT_CODE_LANDING_MAX_WAIT_MS,
306
+ pollWindowSeconds: pollWindowSeconds,
307
+ expectedPaths: filteredExpectedPaths,
308
+ emptySignal: codeLandingEmptySignal(pollWindowSeconds),
309
+ }
310
+ }
311
+
312
+ function addExpectedCodeTarget(value, out) {
313
+ if (value === null || value === undefined) return
314
+ if (Array.isArray(value)) {
315
+ for (let i = 0; i < value.length; i++) addExpectedCodeTarget(value[i], out)
316
+ return
317
+ }
318
+ if (typeof value === 'object') {
319
+ if (Array.isArray(value.wrote)) addExpectedCodeTarget(value.wrote, out)
320
+ if (Array.isArray(value.paths)) addExpectedCodeTarget(value.paths, out)
321
+ return
322
+ }
323
+ const lines = String(value).split(/\r?\n/)
324
+ for (let i = 0; i < lines.length; i++) {
325
+ const candidate = lines[i].replace(/^[-*]\s+/, '').replace(/^\x60+|\x60+$/g, '').trim()
326
+ if (candidate) out.push(candidate)
327
+ }
328
+ }
329
+
330
+ function extractExpectedCodeTargetsFromText(text) {
331
+ const out = []
332
+ const lines = String(text || '').split(/\r?\n/)
333
+ let inBlock = false
334
+ for (let i = 0; i < lines.length; i++) {
335
+ const trimmed = lines[i].trim()
336
+ if (/^EXPECTED_CODE_TARGETS:\s*$/i.test(trimmed)) { inBlock = true; continue }
337
+ if (!inBlock) continue
338
+ if (!trimmed) continue
339
+ if (/^[A-Z][A-Z0-9_ -]*:\s*$/.test(trimmed)) break
340
+ out.push(trimmed.replace(/^[-*]\s+/, '').replace(/^\x60+|\x60+$/g, '').trim())
341
+ }
342
+ return out
343
+ }
344
+
345
+ function extractExpectedCodeTargets(argTargets, codexText) {
346
+ const out = []
347
+ addExpectedCodeTarget(argTargets, out)
348
+ addExpectedCodeTarget(extractExpectedCodeTargetsFromText(codexText), out)
349
+ return filterPollableCodePaths(out)
350
+ }
351
+
352
+ function codeLandingShellQuote(value) {
353
+ return "'" + String(value).replace(/'/g, "'\"'\"'") + "'"
354
+ }
355
+
356
+ function codeLandingProbeCmd(repo, plan) {
357
+ const expectedList = plan.expectedPaths.length > 0 ? plan.expectedPaths.map(codeLandingShellQuote).join(' ') : "''"
358
+ return 'repo=' + codeLandingShellQuote(repo) + '; sleeps="' + plan.sleepsSeconds.join(' ') + '"; expected_count=' + plan.expectedPaths.length + '; elapsed=0; poll(){ paths=$(git -C "$repo" status --porcelain 2>/dev/null | sed -E "s/^...//" | sed -E "s/.* -> //" | grep -vE "^(features/|[.]dz/|[.]agentic-qe/|roam/)" | sed "/^$/d" | head -200); n=$(printf "%s\\n" "$paths" | sed "/^$/d" | wc -l | tr -d " "); if [ "$expected_count" -gt 0 ]; then matched=""; for p in ' + expectedList + '; do [ -z "$p" ] && continue; if printf "%s\\n" "$paths" | grep -Fx -- "$p" >/dev/null; then matched="$p"; break; fi; done; if [ -n "$matched" ]; then echo "CODEX-LANDING-SIGNAL status=landed changed=1 after=${elapsed}s predicate=expected-path"; echo "matched=$matched"; echo "files:"; printf "%s\\n" "$paths" | head -40; exit 0; fi; else if [ "$n" -gt 0 ]; then echo "CODEX-LANDING-SIGNAL status=landed changed=$n after=${elapsed}s predicate=any-code-change"; echo "files:"; printf "%s\\n" "$paths" | head -40; exit 0; fi; fi; }; poll; for wait in $sleeps; do sleep "$wait"; elapsed=$((elapsed + wait)); poll; done; echo "CODEX-LANDING-SIGNAL status=genuinely-not-landed ' + plan.emptySignal + '"; if [ "$expected_count" -gt 0 ]; then echo "predicate=expected-path observed=$n"; else echo "predicate=any-code-change"; fi; echo "files:"; if [ -n "$paths" ]; then printf "%s\\n" "$paths" | head -40; else echo "(none)"; fi'
359
+ }
360
+
243
361
  // A Bash one-liner that waits for a Codex OUT-OF-BAND artifact write to LAND: polls up to ~40s until
244
362
  // the file exists, is non-empty, AND its size is stable across two reads (write finished). Assumes a
245
363
  // fresh feature slug (no stale same-path artifact) — true for a normal /feature-adr run.
@@ -257,7 +375,7 @@ async function designStage(promptText, opts, artifactPath, baseLabel) {
257
375
  if (!needsLandedBarrier(opts)) return await agent(promptText, opts)
258
376
  const codexOpts = {}
259
377
  for (const k in opts) if (k !== 'schema') codexOpts[k] = opts[k]
260
- const res = await agent(promptText + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until the file is fully written to disk.', codexOpts)
378
+ const res = await agent(promptText + codexEffortHint(codexOpts) + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until the file is fully written to disk.', codexOpts)
261
379
  const probe = await agent('Confirm a Codex OUT-OF-BAND artifact write has LANDED before the next stage reads it. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + landedProbeCmd(artifactPath), { label: 'design:confirm-landed', phase: 'Design', effort: 'low' })
262
380
  if (res && probe && /landed=/.test(String(probe))) return { wrote: [artifactPath], summary: String(res).slice(0, 300) }
263
381
  log('design artifact did not land on codex (' + artifactPath + ') — falling back to Claude')
@@ -278,7 +396,7 @@ const routerPrompt = 'You are Step 0 (Complexity Router) of the /feature-adr pip
278
396
  const routerModel = resolveStageModel('router')
279
397
  const routerOpts = mergeOpts({ label: stageLabel('router+recall', routerModel), phase: 'Router', schema: ROUTER, effort: 'low' }, routerModel)
280
398
  modelsUsed.router = modelLabel(routerOpts)
281
- const router = await agent(routerPrompt, routerOpts)
399
+ const router = await agent(routerPrompt + codexEffortHint(routerOpts), routerOpts)
282
400
  let tier = A.tier || (router ? router.tier : 'M')
283
401
  const LEARNED = router ? router.rationale : 'none recalled'
284
402
  const isMplus = tier === 'M' || tier === 'L' || tier === 'XL'
@@ -335,8 +453,8 @@ let plan = null
335
453
  if (planIsCodex) {
336
454
  const planCodexLabelOpts = (planModel.agentType === 'codex:codex-rescue') ? planModel : specToOpts('codex:' + CODEX_MODEL + ':high')
337
455
  modelsUsed.plan = modelLabel(planCodexLabelOpts)
338
- const codexPlanOpts = mergeOpts({ label: stageLabel('plan:codex', planCodexLabelOpts), phase: 'Plan', agentType: 'codex:codex-rescue' }, planModel.agentType ? planModel : {})
339
- const codexPlan = await agent(planPrompt + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until 06_implementation_plan.md is fully written to disk.', codexPlanOpts)
456
+ const codexPlanOpts = mergeOpts({ label: stageLabel('plan:codex', planCodexLabelOpts), phase: 'Plan', agentType: 'codex:codex-rescue' }, planCodexLabelOpts)
457
+ const codexPlan = await agent(planPrompt + codexEffortHint(codexPlanOpts) + ' IMPORTANT: run the Codex task in FOREGROUND (synchronous — do NOT pass --background) so this call blocks until 06_implementation_plan.md is fully written to disk.', codexPlanOpts)
340
458
  // Codex-landed barrier for the plan artifact: a stub return is NOT proof the file was written
341
459
  // (codex writes out-of-band). Require the artifact to LAND; otherwise fall through to the Claude planner.
342
460
  const planLanded = codexPlan ? await agent('Confirm the Codex plan write has LANDED. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + landedProbeCmd(FDIR + '/06_implementation_plan.md'), { label: 'plan:confirm-landed', phase: 'Plan', effort: 'low' }) : null
@@ -383,6 +501,7 @@ const codeClaudeModel = codeIsCodexFirst ? {} : (codeModel.agentType ? {} : code
383
501
  const codeClaudeOpts = mergeOpts({ label: stageLabel('code', codeClaudeModel), phase: 'Code', schema: ARTIFACT, effort: 'high' }, codeClaudeModel)
384
502
  let code = null
385
503
  let coderUsed = 'claude'
504
+ let codexCodeText = ''
386
505
  if (!codeIsCodexFirst) {
387
506
  code = await agent(codePrompt, codeClaudeOpts)
388
507
  if (code) { coderUsed = 'claude'; modelsUsed.code = modelLabel(codeClaudeOpts) }
@@ -391,21 +510,23 @@ if (code === null && !codeIsCodexFirst) reactiveBelt('Code')
391
510
  if (code === null && (codeIsCodexFirst || CODER === 'codex-fallback')) {
392
511
  if (CODER === 'codex-fallback' && !codeIsCodexFirst) log('Code: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
393
512
  const codeCodexLabelOpts = codeModel.agentType ? codeModel : specToOpts('codex:' + CODEX_MODEL + ':high')
394
- const codeCodexOpts = mergeOpts({ label: stageLabel('code:codex', codeCodexLabelOpts), phase: 'Code', agentType: 'codex:codex-rescue' }, codeModel.agentType ? codeModel : {})
395
- const codexCode = await agent(codePrompt + CODEX_HINT, codeCodexOpts)
396
- 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(codeCodexLabelOpts) }
513
+ const codeCodexOpts = mergeOpts({ label: stageLabel('code:codex', codeCodexLabelOpts), phase: 'Code', agentType: 'codex:codex-rescue' }, codeCodexLabelOpts)
514
+ const codexExpectedTargetsHint = '\n\nBecause this is running on Codex, include a final EXPECTED_CODE_TARGETS: block listing the repo-relative production/test files you expect to create or modify. List only real code/test/config/docs targets outside features/, .dz/, .agentic-qe/, and roam/. Example:\nEXPECTED_CODE_TARGETS:\n- packages/example/src/file.ts\n- packages/example/test/file.test.ts'
515
+ const codexCode = await agent(codePrompt + CODEX_HINT + codexEffortHint(codeCodexOpts) + codexExpectedTargetsHint, codeCodexOpts)
516
+ if (codexCode) { codexCodeText = String(codexCode); code = { wrote: [FDIR + '/07_code_changes/change_manifest.md'], summary: codexCodeText.slice(0, 500) }; coderUsed = codeIsCodexFirst ? 'codex' : 'codex-fallback'; modelsUsed.code = modelLabel(codeCodexLabelOpts) }
397
517
  }
398
518
 
399
519
  // Step 7.5: Codex-landed barrier. Codex applies edits OUT-OF-BAND via its own runtime; without this,
400
- // Step-8 QE reads the tree before the async write flushes and false-grades "Step 7 never ran" (grade D
401
- // on real, landed code observed on the goap-ed25519 crypto fix). Poll git status (excluding pipeline
402
- // artifacts) up to ~30s until real code changes appear, and hand the confirmed file list to QE so it
403
- // reviews the ACTUAL landed changes. Claude-coded runs are synchronous → this barrier is skipped.
520
+ // Step-8 QE can read the tree before the async write flushes and false-grade "Step 7 never ran" on real
521
+ // completed code. Poll a bounded 120s backing-off window, preferring declared expected code targets when
522
+ // known. Claude-coded runs are synchronous this barrier is skipped with zero target parsing/probe work.
404
523
  let landedNote = ''
405
- if (coderUsed === 'codex' || coderUsed === 'codex-fallback') {
406
- 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'
407
- 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' })
408
- 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)
524
+ if (needsCodeLandedBarrier(coderUsed)) {
525
+ const expectedCodeTargets = extractExpectedCodeTargets(A.expectedCodeTargets, codexCodeText)
526
+ const barrierPlan = codeLandedBarrierPlan(coderUsed, expectedCodeTargets)
527
+ const barrierCmd = codeLandingProbeCmd(REPO, barrierPlan)
528
+ const probe = await agent('Confirm the Codex Step-7 edits have LANDED in the working tree BEFORE QE runs (Codex writes out-of-band). Expected-file mode must be satisfied by one of the declared expected paths; unrelated dirty files do not count in that mode. Run EXACTLY this via Bash and return its stdout verbatim, nothing else:\n' + barrierCmd, { label: 'code:confirm-landed', phase: 'Code' })
529
+ landedNote = '\n\nCODEX-CODED (out-of-band): Step 7.5 landing barrier used mode=' + barrierPlan.mode + ', window=' + barrierPlan.pollWindowSeconds + 's. Review the signal below. If status=landed, read the listed files and do NOT report "Step 7 never ran". Only status=genuinely-not-landed with "' + barrierPlan.emptySignal + '" means the bounded barrier found no intended code after the full window.\nExpected code targets: ' + (barrierPlan.expectedPaths.length ? barrierPlan.expectedPaths.join(', ') : '(none declared; fallback accepts any non-pipeline code change)') + '\n' + String(probe || '(landed-probe failed)').slice(0, 1500)
409
530
  }
410
531
 
411
532
  // Step 8: QE (brutal-honesty, agentic-qe) + MANDATORY teach
@@ -434,8 +555,8 @@ if (qe === null && !qeIsCodex) reactiveBelt('QE')
434
555
  if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
435
556
  if (QE_REVIEWER === 'codex-fallback' && !qeIsCodex) log('QE: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
436
557
  const qeCodexLabelOpts = qeModel.agentType ? qeModel : specToOpts('codex:' + CODEX_MODEL + ':high')
437
- const qeCodexOpts = mergeOpts({ label: stageLabel('qe:codex', qeCodexLabelOpts), phase: 'QE', agentType: 'codex:codex-rescue' }, qeModel.agentType ? qeModel : {})
438
- const codexQe = await agent(qePrompt + CODEX_HINT, qeCodexOpts)
558
+ const qeCodexOpts = mergeOpts({ label: stageLabel('qe:codex', qeCodexLabelOpts), phase: 'QE', agentType: 'codex:codex-rescue' }, qeCodexLabelOpts)
559
+ const codexQe = await agent(qePrompt + CODEX_HINT + codexEffortHint(qeCodexOpts), qeCodexOpts)
439
560
  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(qeCodexLabelOpts) }
440
561
  }
441
562
  // Belt: if a codex-first QE returned null (codex unavailable), fall back to a Claude reviewer — never block.
@@ -457,8 +578,8 @@ if (isLplus) {
457
578
  const fleetTraceOpts = mergeOpts({ label: stageLabel('fleet:trace', fleetModel), phase: 'FleetQE', agentType: 'qe-requirements-validator' }, fleetModel)
458
579
  const fleetCovOpts = mergeOpts({ label: stageLabel('fleet:cov', fleetModel), phase: 'FleetQE', agentType: 'qe-coverage-specialist' }, fleetModel)
459
580
  const fleetThunks = [
460
- () => 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),
461
- () => 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),
581
+ () => 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.' + codexEffortHint(fleetTraceOpts), fleetTraceOpts),
582
+ () => 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.' + codexEffortHint(fleetCovOpts), fleetCovOpts),
462
583
  ]
463
584
  await parallel(fleetThunks)
464
585
  fleet = 'run'