@dzhechkov/skills-feature-adr 1.3.42 → 1.3.44

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
@@ -141,6 +150,13 @@ Workflow({ scriptPath: '.claude/workflows/feature-adr.js',
141
150
  Omit the Codex knobs entirely for today's all-Claude behavior. The run result reports
142
151
  `plannerUsed` / `coderUsed` / `qeReviewerUsed` / `codexModel` / `modelsUsed` so you can see who did what.
143
152
 
153
+ **Live model visibility in `/workflows`:** each stage's agent label is decorated with its *resolved*
154
+ model — e.g. `adr · codex:gpt-5.5:xhigh`, `plan · opus` — so you see which model actually ran a stage in
155
+ the live progress tree, not just in the final report. This matters because a Codex stage's auto
156
+ model-badge shows the `codex:codex-rescue` Claude wrapper (the session model), never `codex`; the label
157
+ text is the honest signal. A Claude fallback appears as its own distinct node (never mislabeled as Codex),
158
+ and a routing-off run adds no suffix (byte-identical to today).
159
+
144
160
  ### Per-stage model routing — `args.models`
145
161
 
146
162
  The three Codex knobs above are shortcuts. `args.models` is the **general dial**: an optional map that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.3.42",
3
+ "version": "1.3.44",
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"
@@ -226,12 +226,130 @@ function modelLabel(opts) {
226
226
  return 'session'
227
227
  }
228
228
 
229
+ // stageLabel: make resolved model participation visible in LIVE /workflows labels.
230
+ // A session-inherited resolution is the routing-off BC path, so it stays silent.
231
+ function stageLabel(baseLabel, opts) {
232
+ const m = modelLabel(opts)
233
+ if (!m || m === 'session') return baseLabel
234
+ return baseLabel + ' · ' + m
235
+ }
236
+
229
237
  // needsLandedBarrier — mirror of harness-core's pure gate (feature-adr-routing.ts). TRUE only when a
230
238
  // stage resolved to Codex. A Claude stage is synchronous (artifact on disk when agent() returns), so
231
239
  // this is FALSE and the barrier below is never constructed → an all-Claude run, and a Claude-design +
232
240
  // Codex-QE run, do ZERO extra work (byte-identical to today).
233
241
  function needsLandedBarrier(opts) { return !!(opts && opts.agentType === 'codex:codex-rescue') }
234
242
 
243
+ const DEFAULT_CODE_LANDING_MAX_WAIT_MS = 120000
244
+ const DEFAULT_CODE_LANDING_BACKOFF_MS = [1000, 2000, 2000, 5000, 5000, 10000, 10000, 15000, 20000, 25000, 25000]
245
+ const CODE_LANDED_BARRIER_SLEEPS_SECONDS = DEFAULT_CODE_LANDING_BACKOFF_MS.map((ms) => ms / 1000)
246
+ const CODE_LANDING_PIPELINE_PREFIXES = ['features/', '.dz/', '.agentic-qe/', 'roam/']
247
+
248
+ function codeLandingEmptySignal(seconds) {
249
+ return 'changed=0 after ' + seconds + 's — genuinely not landed'
250
+ }
251
+
252
+ function needsCodeLandedBarrier(coderUsed) {
253
+ return coderUsed === 'codex' || coderUsed === 'codex-fallback'
254
+ }
255
+
256
+ function normalizeCodeLandingPath(path) {
257
+ let p = String(path || '').trim().replace(/\\/g, '/')
258
+ while (p.indexOf('./') === 0) p = p.slice(2)
259
+ p = p.replace(/\/+/g, '/')
260
+ if (!p) return ''
261
+ if (p[0] === '/') return ''
262
+ if (p === '..' || p.indexOf('../') === 0 || p.indexOf('/../') >= 0 || p.endsWith('/..')) return ''
263
+ if (/[\0\r\n\t "'\x60$;&|<>*?()[\]{}!]/.test(p)) return ''
264
+ if (p.endsWith('/')) return ''
265
+ for (let i = 0; i < CODE_LANDING_PIPELINE_PREFIXES.length; i++) {
266
+ const prefix = CODE_LANDING_PIPELINE_PREFIXES[i]
267
+ const bare = prefix.slice(0, -1)
268
+ if (p === bare || p.indexOf(prefix) === 0) return ''
269
+ }
270
+ return p
271
+ }
272
+
273
+ function filterPollableCodePaths(paths) {
274
+ const out = []
275
+ const seen = {}
276
+ for (let i = 0; i < (paths || []).length; i++) {
277
+ const normalized = normalizeCodeLandingPath(paths[i])
278
+ if (!normalized || seen[normalized]) continue
279
+ seen[normalized] = 1
280
+ out.push(normalized)
281
+ }
282
+ return out
283
+ }
284
+
285
+ function codeLandedBarrierPlan(coderUsed, expectedPaths) {
286
+ const enabled = needsCodeLandedBarrier(coderUsed)
287
+ const pollWindowSeconds = DEFAULT_CODE_LANDING_MAX_WAIT_MS / 1000
288
+ if (!enabled) {
289
+ return { enabled: false, mode: 'any-code-change', sleepsMs: [], sleepsSeconds: [], pollWindowMs: 0, pollWindowSeconds: 0, expectedPaths: [], emptySignal: '' }
290
+ }
291
+ const filteredExpectedPaths = filterPollableCodePaths(expectedPaths || [])
292
+ return {
293
+ enabled: true,
294
+ mode: filteredExpectedPaths.length > 0 ? 'expected-files' : 'any-code-change',
295
+ sleepsMs: DEFAULT_CODE_LANDING_BACKOFF_MS,
296
+ sleepsSeconds: CODE_LANDED_BARRIER_SLEEPS_SECONDS,
297
+ pollWindowMs: DEFAULT_CODE_LANDING_MAX_WAIT_MS,
298
+ pollWindowSeconds: pollWindowSeconds,
299
+ expectedPaths: filteredExpectedPaths,
300
+ emptySignal: codeLandingEmptySignal(pollWindowSeconds),
301
+ }
302
+ }
303
+
304
+ function addExpectedCodeTarget(value, out) {
305
+ if (value === null || value === undefined) return
306
+ if (Array.isArray(value)) {
307
+ for (let i = 0; i < value.length; i++) addExpectedCodeTarget(value[i], out)
308
+ return
309
+ }
310
+ if (typeof value === 'object') {
311
+ if (Array.isArray(value.wrote)) addExpectedCodeTarget(value.wrote, out)
312
+ if (Array.isArray(value.paths)) addExpectedCodeTarget(value.paths, out)
313
+ return
314
+ }
315
+ const lines = String(value).split(/\r?\n/)
316
+ for (let i = 0; i < lines.length; i++) {
317
+ const candidate = lines[i].replace(/^[-*]\s+/, '').replace(/^\x60+|\x60+$/g, '').trim()
318
+ if (candidate) out.push(candidate)
319
+ }
320
+ }
321
+
322
+ function extractExpectedCodeTargetsFromText(text) {
323
+ const out = []
324
+ const lines = String(text || '').split(/\r?\n/)
325
+ let inBlock = false
326
+ for (let i = 0; i < lines.length; i++) {
327
+ const trimmed = lines[i].trim()
328
+ if (/^EXPECTED_CODE_TARGETS:\s*$/i.test(trimmed)) { inBlock = true; continue }
329
+ if (!inBlock) continue
330
+ if (!trimmed) continue
331
+ if (/^[A-Z][A-Z0-9_ -]*:\s*$/.test(trimmed)) break
332
+ out.push(trimmed.replace(/^[-*]\s+/, '').replace(/^\x60+|\x60+$/g, '').trim())
333
+ }
334
+ return out
335
+ }
336
+
337
+ function extractExpectedCodeTargets(argTargets, codexText) {
338
+ const out = []
339
+ addExpectedCodeTarget(argTargets, out)
340
+ addExpectedCodeTarget(extractExpectedCodeTargetsFromText(codexText), out)
341
+ return filterPollableCodePaths(out)
342
+ }
343
+
344
+ function codeLandingShellQuote(value) {
345
+ return "'" + String(value).replace(/'/g, "'\"'\"'") + "'"
346
+ }
347
+
348
+ function codeLandingProbeCmd(repo, plan) {
349
+ const expectedList = plan.expectedPaths.length > 0 ? plan.expectedPaths.map(codeLandingShellQuote).join(' ') : "''"
350
+ 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'
351
+ }
352
+
235
353
  // A Bash one-liner that waits for a Codex OUT-OF-BAND artifact write to LAND: polls up to ~40s until
236
354
  // the file exists, is non-empty, AND its size is stable across two reads (write finished). Assumes a
237
355
  // fresh feature slug (no stale same-path artifact) — true for a normal /feature-adr run.
@@ -245,7 +363,7 @@ function landedProbeCmd(f) {
245
363
  // codex-rescue returns finalMessage text, not StructuredOutput; success is proven by the file landing —
246
364
  // (b) add a FOREGROUND hint so the codex runtime blocks until the write completes, (c) poll the
247
365
  // artifact, and (d) fall back to a Claude agent if it never lands (never blocks the pipeline).
248
- async function designStage(promptText, opts, artifactPath) {
366
+ async function designStage(promptText, opts, artifactPath, baseLabel) {
249
367
  if (!needsLandedBarrier(opts)) return await agent(promptText, opts)
250
368
  const codexOpts = {}
251
369
  for (const k in opts) if (k !== 'schema') codexOpts[k] = opts[k]
@@ -253,7 +371,8 @@ async function designStage(promptText, opts, artifactPath) {
253
371
  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' })
254
372
  if (res && probe && /landed=/.test(String(probe))) return { wrote: [artifactPath], summary: String(res).slice(0, 300) }
255
373
  log('design artifact did not land on codex (' + artifactPath + ') — falling back to Claude')
256
- return await agent(promptText, mergeOpts({ label: (opts.label || 'design') + ':claude-fb', phase: 'Design', schema: ARTIFACT }, {}))
374
+ const fallbackOpts = {}
375
+ return await agent(promptText, mergeOpts({ label: stageLabel((baseLabel || 'design') + ':claude-fb', fallbackOpts), phase: 'Design', schema: ARTIFACT }, fallbackOpts))
257
376
  }
258
377
 
259
378
  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' } } }
@@ -266,7 +385,8 @@ const ADR_FITNESS_CHECKLIST = 'ADR fitness checklist for Step 8: read every ' +
266
385
  phase('Router')
267
386
  await usageProbe('Router')
268
387
  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; if an ADR is explicitly forced, use Nygard as the lightweight fallback); M=4-10 (0,1,3,3.5,5,6,7,8; Nygard/ITD-light ADR); L=11-30 (all+9; MADR+Confirmation ADRs); XL=30+ (full+9; MADR+Confirmation ADRs). ADR template-weight rule: S/M -> Nygard/ITD-light; L/XL -> MADR + NHS Wales Confirmation, while every generated ADR still carries the invariant core. Return {tier, activeSteps, rationale} with the recalled patterns folded into rationale.'
269
- const routerOpts = mergeOpts({ label: 'router+recall', phase: 'Router', schema: ROUTER, effort: 'low' }, resolveStageModel('router'))
388
+ const routerModel = resolveStageModel('router')
389
+ const routerOpts = mergeOpts({ label: stageLabel('router+recall', routerModel), phase: 'Router', schema: ROUTER, effort: 'low' }, routerModel)
270
390
  modelsUsed.router = modelLabel(routerOpts)
271
391
  const router = await agent(routerPrompt, routerOpts)
272
392
  let tier = A.tier || (router ? router.tier : 'M')
@@ -287,22 +407,26 @@ const designThunks = []
287
407
  const reqExtra = isLplus ? ' Also write ' + FDIR + '/02_research.md (codebase patterns + external analogues; read the repo for the closest existing implementation to mirror).' : ''
288
408
  // Resolve per-stage model opts up-front (parser-safe: no inline resolveStageModel inside the thunk arrays).
289
409
  // research folds into requirements, ddd folds into architecture (single shared call) — recorded for reporting.
290
- const reqOpts = mergeOpts({ label: 'requirements', phase: 'Design', schema: ARTIFACT }, resolveStageModel('requirements'))
291
- const adrOpts = mergeOpts({ label: 'adr', phase: 'Design', schema: ARTIFACT }, resolveStageModel('adr'))
292
- const qcsdOpts = mergeOpts({ label: 'qcsd', phase: 'Design', schema: ARTIFACT }, resolveStageModel('ideation'))
293
- const archOpts = mergeOpts({ label: 'architecture', phase: 'Design', schema: ARTIFACT }, resolveStageModel('architecture'))
410
+ const reqModel = resolveStageModel('requirements')
411
+ const adrModel = resolveStageModel('adr')
412
+ const qcsdModel = resolveStageModel('ideation')
413
+ const archModel = resolveStageModel('architecture')
414
+ const reqOpts = mergeOpts({ label: stageLabel('requirements', reqModel), phase: 'Design', schema: ARTIFACT }, reqModel)
415
+ const adrOpts = mergeOpts({ label: stageLabel('adr', adrModel), phase: 'Design', schema: ARTIFACT }, adrModel)
416
+ const qcsdOpts = mergeOpts({ label: stageLabel('qcsd', qcsdModel), phase: 'Design', schema: ARTIFACT }, qcsdModel)
417
+ const archOpts = mergeOpts({ label: stageLabel('architecture', archModel), phase: 'Design', schema: ARTIFACT }, archModel)
294
418
  modelsUsed.requirements = modelLabel(reqOpts)
295
419
  modelsUsed.research = modelLabel(reqOpts)
296
420
  modelsUsed.adr = modelLabel(adrOpts)
297
421
  modelsUsed.ideation = modelLabel(qcsdOpts)
298
422
  modelsUsed.architecture = modelLabel(archOpts)
299
423
  modelsUsed.ddd = modelLabel(archOpts)
300
- designThunks.push(() => designStage('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, FDIR + '/01_requirements.md'))
424
+ designThunks.push(() => designStage('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, FDIR + '/01_requirements.md', 'requirements'))
301
425
  if (isMplus) {
302
- designThunks.push(() => designStage('Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. ' + ADR_TEMPLATE_GUIDE + ' Write ' + FDIR + '/03_adr/001-' + SLUG + '.md as a MADR-structured ADR that PASSES the Step-8 ADR fitness checklist (do NOT emit the legacy shape). Emit ALL of these sections, in order: a decision-shaped # Title (present-tense imperative verb — the auto-filename tracks the feature slug, so the IMPERATIVE signal lives in the title); ## Status (proposed/accepted/rejected/deprecated/superseded + a reversibility/revisit clause); ## Context (neutral, problem-first, BEFORE the Decision); ## Decision Drivers (ranked/weighted D1, D2, …); ## Considered Options (frame the CHOSEN approach as one option ALONGSIDE the rejected ones, each with symmetric Pros:/Cons:); ## Decision (concrete/testable — exact names, versions, paths, commands); ## Rationale (map each point to a driver Dn + why the losers lost); ## Consequences (Positive + Negative/Accepted Downsides + Follow-up ADRs + After-action Review with owner + date); a REQUIRED ## Confirmation stanza with Method:, Monitoring:, Success metric:, Owner:, Load-bearing property:, and Required automated check: `<test file>` NAMING the load-bearing property that MUST have a Step-8 test (the recurring lesson: the key safety property is often the untested one); and a ## Links traceability block (requirements, driving use case, related ADRs). Add a one-line provenance note (model-generated, edited for clarity) and, for a long ADR, a top-of-file table of contents. Do NOT use an "Alternatives considered" or "Testability/shift-left" heading in place of Considered Options / Confirmation. When creating ADDITIONAL ADRs, name them 03_adr/NNN-{decision-slug}.md with a lowercase-kebab, present-tense imperative, dateless, ticketless slug. Return wrote[] + summary.', adrOpts, FDIR + '/03_adr/001-' + SLUG + '.md'))
303
- designThunks.push(() => designStage('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, FDIR + '/03.5_ideation_report.md'))
426
+ designThunks.push(() => designStage('Step 3 (ADR + shift-left testability) of /feature-adr for "' + DESC + '" (' + SLUG + '). READ the actual code (' + CODE_HINT + ') to ground it. ' + ADR_TEMPLATE_GUIDE + ' Write ' + FDIR + '/03_adr/001-' + SLUG + '.md as a MADR-structured ADR that PASSES the Step-8 ADR fitness checklist (do NOT emit the legacy shape). Emit ALL of these sections, in order: a decision-shaped # Title (present-tense imperative verb — the auto-filename tracks the feature slug, so the IMPERATIVE signal lives in the title); ## Status (proposed/accepted/rejected/deprecated/superseded + a reversibility/revisit clause); ## Context (neutral, problem-first, BEFORE the Decision); ## Decision Drivers (ranked/weighted D1, D2, …); ## Considered Options (frame the CHOSEN approach as one option ALONGSIDE the rejected ones, each with symmetric Pros:/Cons:); ## Decision (concrete/testable — exact names, versions, paths, commands); ## Rationale (map each point to a driver Dn + why the losers lost); ## Consequences (Positive + Negative/Accepted Downsides + Follow-up ADRs + After-action Review with owner + date); a REQUIRED ## Confirmation stanza with Method:, Monitoring:, Success metric:, Owner:, Load-bearing property:, and Required automated check: `<test file>` NAMING the load-bearing property that MUST have a Step-8 test (the recurring lesson: the key safety property is often the untested one); and a ## Links traceability block (requirements, driving use case, related ADRs). Add a one-line provenance note (model-generated, edited for clarity) and, for a long ADR, a top-of-file table of contents. Do NOT use an "Alternatives considered" or "Testability/shift-left" heading in place of Considered Options / Confirmation. When creating ADDITIONAL ADRs, name them 03_adr/NNN-{decision-slug}.md with a lowercase-kebab, present-tense imperative, dateless, ticketless slug. Return wrote[] + summary.', adrOpts, FDIR + '/03_adr/001-' + SLUG + '.md', 'adr'))
427
+ designThunks.push(() => designStage('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, FDIR + '/03.5_ideation_report.md', 'qcsd'))
304
428
  const archExtra = isLplus ? ' Also ' + FDIR + '/04_domain_model.md (DDD).' : ''
305
- designThunks.push(() => designStage((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, FDIR + '/05_architecture.md'))
429
+ designThunks.push(() => designStage((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, FDIR + '/05_architecture.md', 'architecture'))
306
430
  }
307
431
  const design = await parallel(designThunks)
308
432
 
@@ -319,8 +443,9 @@ const planModel = resolveStageModel('plan')
319
443
  const planIsCodex = (planModel.agentType === 'codex:codex-rescue') || (MODELS.plan === undefined && PLANNER === 'codex')
320
444
  let plan = null
321
445
  if (planIsCodex) {
322
- modelsUsed.plan = (planModel.agentType === 'codex:codex-rescue') ? modelLabel(planModel) : ('codex:' + CODEX_MODEL + ':high')
323
- const codexPlanOpts = mergeOpts({ label: 'plan:codex', phase: 'Plan', agentType: 'codex:codex-rescue' }, planModel.agentType ? planModel : {})
446
+ const planCodexLabelOpts = (planModel.agentType === 'codex:codex-rescue') ? planModel : specToOpts('codex:' + CODEX_MODEL + ':high')
447
+ modelsUsed.plan = modelLabel(planCodexLabelOpts)
448
+ const codexPlanOpts = mergeOpts({ label: stageLabel('plan:codex', planCodexLabelOpts), phase: 'Plan', agentType: 'codex:codex-rescue' }, planModel.agentType ? planModel : {})
324
449
  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)
325
450
  // Codex-landed barrier for the plan artifact: a stub return is NOT proof the file was written
326
451
  // (codex writes out-of-band). Require the artifact to LAND; otherwise fall through to the Claude planner.
@@ -334,7 +459,8 @@ if (planIsCodex) {
334
459
  }
335
460
  if (plan === null && planIsCodex) reactiveBelt('Plan')
336
461
  if (plan === null) {
337
- const claudePlanOpts = mergeOpts({ label: 'plan', phase: 'Plan', schema: ARTIFACT }, planIsCodex ? {} : planModel)
462
+ const claudePlanModel = planIsCodex ? {} : planModel
463
+ const claudePlanOpts = mergeOpts({ label: stageLabel(planIsCodex ? 'plan:claude-fb' : 'plan', claudePlanModel), phase: 'Plan', schema: ARTIFACT }, claudePlanModel)
338
464
  modelsUsed.plan = planIsCodex ? 'claude-fallback' : modelLabel(claudePlanOpts)
339
465
  const claudePlan = await agent(planPrompt, claudePlanOpts)
340
466
  plan = claudePlan ? { wrote: claudePlan.wrote, summary: claudePlan.summary, planner: planIsCodex ? 'claude-fallback' : 'claude' } : null
@@ -363,9 +489,11 @@ const codePrompt = 'Step 7 (Code) of /feature-adr for "' + DESC + '" (' + SLUG +
363
489
  // under the BC omit-path it is {} (byte-identical).
364
490
  const codeModel = resolveStageModel('code')
365
491
  const codeIsCodexFirst = (MODELS.code !== undefined) ? (codeModel.agentType === 'codex:codex-rescue') : (CODER === 'codex')
366
- const codeClaudeOpts = mergeOpts({ label: 'code', phase: 'Code', schema: ARTIFACT, effort: 'high' }, codeIsCodexFirst ? {} : (codeModel.agentType ? {} : codeModel))
492
+ const codeClaudeModel = codeIsCodexFirst ? {} : (codeModel.agentType ? {} : codeModel)
493
+ const codeClaudeOpts = mergeOpts({ label: stageLabel('code', codeClaudeModel), phase: 'Code', schema: ARTIFACT, effort: 'high' }, codeClaudeModel)
367
494
  let code = null
368
495
  let coderUsed = 'claude'
496
+ let codexCodeText = ''
369
497
  if (!codeIsCodexFirst) {
370
498
  code = await agent(codePrompt, codeClaudeOpts)
371
499
  if (code) { coderUsed = 'claude'; modelsUsed.code = modelLabel(codeClaudeOpts) }
@@ -373,21 +501,24 @@ if (!codeIsCodexFirst) {
373
501
  if (code === null && !codeIsCodexFirst) reactiveBelt('Code')
374
502
  if (code === null && (codeIsCodexFirst || CODER === 'codex-fallback')) {
375
503
  if (CODER === 'codex-fallback' && !codeIsCodexFirst) log('Code: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
376
- const codeCodexOpts = mergeOpts({ label: 'code:codex', phase: 'Code', agentType: 'codex:codex-rescue' }, codeModel.agentType ? codeModel : {})
377
- const codexCode = await agent(codePrompt + CODEX_HINT, codeCodexOpts)
378
- 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) }
504
+ const codeCodexLabelOpts = codeModel.agentType ? codeModel : specToOpts('codex:' + CODEX_MODEL + ':high')
505
+ const codeCodexOpts = mergeOpts({ label: stageLabel('code:codex', codeCodexLabelOpts), phase: 'Code', agentType: 'codex:codex-rescue' }, codeModel.agentType ? codeModel : {})
506
+ 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'
507
+ const codexCode = await agent(codePrompt + CODEX_HINT + codexExpectedTargetsHint, codeCodexOpts)
508
+ 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) }
379
509
  }
380
510
 
381
511
  // Step 7.5: Codex-landed barrier. Codex applies edits OUT-OF-BAND via its own runtime; without this,
382
- // Step-8 QE reads the tree before the async write flushes and false-grades "Step 7 never ran" (grade D
383
- // on real, landed code observed on the goap-ed25519 crypto fix). Poll git status (excluding pipeline
384
- // artifacts) up to ~30s until real code changes appear, and hand the confirmed file list to QE so it
385
- // reviews the ACTUAL landed changes. Claude-coded runs are synchronous → this barrier is skipped.
512
+ // Step-8 QE can read the tree before the async write flushes and false-grade "Step 7 never ran" on real
513
+ // completed code. Poll a bounded 120s backing-off window, preferring declared expected code targets when
514
+ // known. Claude-coded runs are synchronous this barrier is skipped with zero target parsing/probe work.
386
515
  let landedNote = ''
387
- if (coderUsed === 'codex' || coderUsed === 'codex-fallback') {
388
- 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'
389
- 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' })
390
- 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)
516
+ if (needsCodeLandedBarrier(coderUsed)) {
517
+ const expectedCodeTargets = extractExpectedCodeTargets(A.expectedCodeTargets, codexCodeText)
518
+ const barrierPlan = codeLandedBarrierPlan(coderUsed, expectedCodeTargets)
519
+ const barrierCmd = codeLandingProbeCmd(REPO, barrierPlan)
520
+ 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' })
521
+ 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)
391
522
  }
392
523
 
393
524
  // Step 8: QE (brutal-honesty, agentic-qe) + MANDATORY teach
@@ -404,7 +535,8 @@ const qeModel = resolveStageModel('qe')
404
535
  // the legacy qeReviewer='codex' knob used to re-route QE back to codex even when the CODER was codex.
405
536
  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)')
406
537
  const qeIsCodex = qeShouldUseCodex()
407
- const qeClaudeOpts = mergeOpts({ label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, qeIsCodex ? {} : qeModel)
538
+ const qeClaudeModel = qeIsCodex ? {} : qeModel
539
+ const qeClaudeOpts = mergeOpts({ label: stageLabel('qe:brutal', qeClaudeModel), phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, qeClaudeModel)
408
540
  let qe = null
409
541
  let qeReviewerUsed = 'claude'
410
542
  if (!qeIsCodex) {
@@ -414,14 +546,16 @@ if (!qeIsCodex) {
414
546
  if (qe === null && !qeIsCodex) reactiveBelt('QE')
415
547
  if (qe === null && (qeIsCodex || QE_REVIEWER === 'codex-fallback')) {
416
548
  if (QE_REVIEWER === 'codex-fallback' && !qeIsCodex) log('QE: Claude unavailable (limit?) — falling back to Codex ' + CODEX_MODEL)
417
- const qeCodexOpts = mergeOpts({ label: 'qe:codex', phase: 'QE', agentType: 'codex:codex-rescue' }, qeModel.agentType ? qeModel : {})
549
+ const qeCodexLabelOpts = qeModel.agentType ? qeModel : specToOpts('codex:' + CODEX_MODEL + ':high')
550
+ const qeCodexOpts = mergeOpts({ label: stageLabel('qe:codex', qeCodexLabelOpts), phase: 'QE', agentType: 'codex:codex-rescue' }, qeModel.agentType ? qeModel : {})
418
551
  const codexQe = await agent(qePrompt + CODEX_HINT, qeCodexOpts)
419
- 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) }
552
+ 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) }
420
553
  }
421
554
  // Belt: if a codex-first QE returned null (codex unavailable), fall back to a Claude reviewer — never block.
422
555
  if (qe === null && qeIsCodex) {
423
556
  log('QE: Codex unavailable — falling back to a Claude reviewer (cross-model belt)')
424
- const qeBeltOpts = mergeOpts({ label: 'qe:brutal', phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, routingRequested ? { model: 'opus' } : {})
557
+ const qeBeltModel = routingRequested ? { model: 'opus' } : {}
558
+ const qeBeltOpts = mergeOpts({ label: stageLabel('qe:brutal:claude-fb', qeBeltModel), phase: 'QE', agentType: 'qe-code-reviewer', schema: QE }, qeBeltModel)
425
559
  qe = await agent(qePrompt, qeBeltOpts)
426
560
  if (qe) { qeReviewerUsed = 'claude'; modelsUsed.qe = modelLabel(qeBeltOpts) }
427
561
  }
@@ -433,8 +567,8 @@ if (isLplus) {
433
567
  await usageProbe('FleetQE')
434
568
  const fleetModel = resolveStageModel('fleet')
435
569
  modelsUsed.fleet = modelLabel(mergeOpts({}, fleetModel))
436
- const fleetTraceOpts = mergeOpts({ label: 'fleet:trace', phase: 'FleetQE', agentType: 'qe-requirements-validator' }, fleetModel)
437
- const fleetCovOpts = mergeOpts({ label: 'fleet:cov', phase: 'FleetQE', agentType: 'qe-coverage-specialist' }, fleetModel)
570
+ const fleetTraceOpts = mergeOpts({ label: stageLabel('fleet:trace', fleetModel), phase: 'FleetQE', agentType: 'qe-requirements-validator' }, fleetModel)
571
+ const fleetCovOpts = mergeOpts({ label: stageLabel('fleet:cov', fleetModel), phase: 'FleetQE', agentType: 'qe-coverage-specialist' }, fleetModel)
438
572
  const fleetThunks = [
439
573
  () => 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),
440
574
  () => 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),