@dzhechkov/skills-feature-adr 1.3.51 → 1.3.53

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
@@ -410,6 +410,25 @@ Fixed roles map to fixed stages (`product-vision`→design+QE, `critic`→QE, `b
410
410
  open `extra` list adds guidance to any stage. `dz project-skills` prints the who-injected report. Absent ⇒ a
411
411
  normal generic run. (See `dz mr-rakes --gen-critic` to auto-generate the `critic` skill from your recurring rakes.)
412
412
 
413
+ ### Adversarial plan-gate at the L/XL checkpoint (needs `dz` ≥ 0.3.117)
414
+
415
+ Cross-model QE (Step 8) catches problems *after* the code is written. At the **checkpoint-after-plan** the
416
+ pipeline now also runs an adversarial **challenge panel** on the plan itself — a FRESH reviewer that did **not**
417
+ write the plan tries to BREAK it across a fixed C1-C8 owner-question set (arch-anti-cement, prod-ready, test
418
+ sufficiency + honesty, overengineering, silent decisions, runtime consistency, scope, executability):
419
+
420
+ - **Panel ≠ plan author** (hard invariant): if Claude wrote the plan the adversary is Codex (honest `codex exec`,
421
+ never a fire-and-forget stub); if Codex wrote it the adversary is a fresh Claude. An unknown family never
422
+ silently claims cross-family.
423
+ - **Every P0/P1 is cross-validated** by a second independent agent, matched by index (never by title), and the
424
+ non-validated ones are dropped — theory never reaches you. If the validator can't cover them, they are
425
+ surfaced **UNVALIDATED**, never silently dropped.
426
+ - **Advisory** — the verdict returns as `challengeVerdict` alongside the ADR + plan; it **never auto-blocks**.
427
+ - Calibrated by `architecture/vision.md` + `testing.md` + `map.json` + `architecture/degradations.md` (a
428
+ deviation from a pattern registered in the degradations file is **not** flagged). Run it ad-hoc with
429
+ `dz challenge --plan <plan.md>` or the `challenge-panel` skill; scaffold the degradations registry via
430
+ `dz feature-adr-setup --from-spec <spec with {"degradations":true}> --apply`.
431
+
413
432
  ### ADR quality gate (Step 3 generates → Step 8 enforces)
414
433
 
415
434
  Step 3 and Step 8 share an ADR best-practices contract distilled from the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dzhechkov/skills-feature-adr",
3
- "version": "1.3.51",
3
+ "version": "1.3.53",
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"
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: challenge-panel
3
+ description: >-
4
+ Adversarial plan-gate. BEFORE code, a FRESH reviewer (never the plan author) tries to BREAK an
5
+ implementation plan across a fixed C1-C8 owner-question set, every serious finding is cross-validated,
6
+ and the verdict is surfaced as ADVICE — never an auto-block. Trigger on "прогони challenge", "челлендж
7
+ плана", "challenge the plan", "adversarial review of this plan", or from the feature-adr plan checkpoint.
8
+ ---
9
+
10
+ # Challenge Panel — adversarial plan-gate
11
+
12
+ feature-adr's cross-model QE (Step 8) catches problems AFTER the code is written. The most expensive
13
+ mistakes — overengineering, silent decisions, cemented degradations, test-theater, unrealistic scope —
14
+ cement at the PLAN stage, and the plan's author structurally cannot find their own gaps (author bias).
15
+ This skill runs a "break the plan, don't confirm it" panel at the plan stage, by a FRESH agent that did
16
+ NOT write the plan, with cross-validated findings. **Advise, not block.**
17
+
18
+ ## Hard invariant (never relax)
19
+
20
+ **The panel is NEVER the plan's own author.** Dispatch the adversary on a DIFFERENT model family than the
21
+ one that wrote the plan:
22
+ - plan written by Claude → adversary = Codex (cross-family). Run it through the honest synchronous path
23
+ (`codex exec` / `safeCodexAgent`), never a fire-and-forget wrapper — a stub reads exactly like a clean
24
+ review. If Codex is unavailable, fall back LOUDLY to a fresh Claude agent and say it was NOT cross-family.
25
+ - plan written by Codex → adversary = a fresh Claude agent (already cross-family).
26
+ - ad-hoc / unknown author → a fresh agent that did not participate in writing the plan.
27
+
28
+ `dz challenge --author <model>` prints which cross-family adversary to dispatch.
29
+
30
+ ## Protocol
31
+
32
+ 1. **Assemble the WIDE context (deterministic).** Run:
33
+ ```bash
34
+ dz challenge --plan <path/to/06_implementation_plan.md>
35
+ ```
36
+ This prints the challenge brief: the plan + `architecture/vision.md` + `architecture/testing.md` +
37
+ `architecture/map.json` + `architecture/degradations.md` inlined (missing docs degrade gracefully) +
38
+ the fixed C1-C8 questions + the verdict JSON schema. A WIDE context is load-bearing — hand a reviewer a
39
+ narrow slice and you get findings about the slice.
40
+ - `--context-only` shows just what the panel will read (and the chosen adversary).
41
+ - `--json` emits the context + brief + adversary as JSON for a subagent.
42
+
43
+ 2. **Fire the panel (fresh adversary ≠ author).** Dispatch a subagent on the cross-family model with the
44
+ brief. It answers C1-C8 in "break it" mode and returns the verdict JSON:
45
+ | # | Question |
46
+ |---|----------|
47
+ | C1 | Architecture anti-cement — cements a NEW bad pattern/boundary? (deviating from a REGISTERED accepted degradation is NOT a finding) |
48
+ | C2 | Production-ready — where does it fall over? name the concrete input/condition |
49
+ | C3 | Test sufficiency + honesty BOTH ways — an ADR-named "never X" with no falsifying test? tests that are theater? |
50
+ | C4 | Overengineering sweep — built for a requirement nobody stated? the simpler thing? |
51
+ | C5 | Silent decisions — a policy made without surfacing it as a decision the owner could refuse |
52
+ | C6 | Runtime consistency — contradicts an existing convention (error shape, config source, ESM, naming)? |
53
+ | C7 | Scope — > ~1.5× what the request needs? the concrete cut list |
54
+ | C8 | Executability — could a non-author complete every step without coming back to ask? |
55
+
56
+ Each finding: `c` (C-number), `severity` (P0/P1/P2), `title`, `why` (a concrete failing input/condition —
57
+ never a general worry), optional `where`.
58
+
59
+ 3. **Cross-validate every P0/P1 (mandatory anti-noise).** Dispatch a SECOND, independent agent: for each
60
+ P0/P1, "real and reachable, or FP/theory?" — default to false when uncertain. **Drop** every P0/P1 that
61
+ is not confirmed. Theory never reaches the owner (a false gate kills trust). P2 pass through.
62
+
63
+ 4. **Surface the verdict — ADVISE, never block.** Present the cross-validated P0/P1 prominently + P2 as
64
+ notes. Do NOT auto-abort. The owner decides whether to revise the plan, accept a finding into
65
+ `architecture/degradations.md`, or proceed.
66
+
67
+ ## Calibration docs (optional, improve precision)
68
+
69
+ - `architecture/vision.md` — boundaries + principles (what the product deliberately does NOT do).
70
+ - `architecture/testing.md` — what "done" and an honest test mean here.
71
+ - `architecture/degradations.md` — the accepted-degradations registry: patterns you KNOW are imperfect but
72
+ keep on purpose. **C1 does not flag a deviation from anything registered here.** Scaffold a starter with
73
+ `dz feature-adr-setup --from-spec <spec with {"degradations":true}> --apply`.
74
+
75
+ Absent docs never error — the panel simply runs with less calibration.
76
+
77
+ ## In the feature-adr pipeline
78
+
79
+ At the L/XL **checkpoint-after-plan**, feature-adr runs this panel automatically (a fresh adversary ≠ the
80
+ Step-6 planner), cross-validates, and returns `challengeVerdict` alongside the ADR + plan — advisory. In
81
+ **plain** `/feature-adr`, offer the panel at the planning checkpoint before coding.
@@ -169,6 +169,21 @@ Estimated {K} files to create/modify
169
169
  ═══════════════════════════════════════════════════════
170
170
  ```
171
171
 
172
+ ## Challenge panel (adversarial plan-gate — before coding)
173
+
174
+ Before leaving the plan checkpoint for code (especially L/XL), offer the **`challenge-panel`** skill — a
175
+ FRESH adversary (never the plan author) tries to BREAK the plan across the fixed C1-C8 owner questions,
176
+ every P0/P1 is cross-validated, and the verdict is ADVISORY (never an auto-block):
177
+
178
+ ```bash
179
+ dz challenge --plan features/<slug>/06_implementation_plan.md --author <planner-model>
180
+ ```
181
+
182
+ Dispatch the panel on the CROSS-FAMILY model (`--author` prints which): plan by Claude → Codex adversary
183
+ (honest `codex exec`, never a stub); plan by Codex → fresh Claude. Drop any P0/P1 the cross-validator does
184
+ not confirm. A finding the owner accepts can be recorded in `architecture/degradations.md` so C1 stops
185
+ re-flagging it. In the ultracode workflow this врезка runs automatically and returns `challengeVerdict`.
186
+
172
187
  ## Quality Gates
173
188
 
174
189
  - [ ] Every task has clear done criteria
@@ -570,6 +570,36 @@ const isMplus = tier === 'M' || tier === 'L' || tier === 'XL'
570
570
  const isLplus = tier === 'L' || tier === 'XL'
571
571
  log('Router: tier ' + tier)
572
572
 
573
+ // ── AUTO-COST pre-resolution (feature learned-cost-routing) ──
574
+ // A stage whose spec is 'auto-cost' is resolved HERE (tier is now known) to a concrete model via
575
+ // `dz routing --select` — the workflow is sandboxed (no fs), so selection I/O shells out to dz. Byte-identical
576
+ // no-op (ZERO agent calls) when no stage is 'auto-cost'. Order matters: resolve `code` FIRST so `qe` can be
577
+ // forced to the CROSS-family of the coder (the named cross-model-QE guard). Escalate-on-fail across runs is
578
+ // automatic: a gate-FAIL recorded below down-ranks the model so the NEXT run's select picks the next rung.
579
+ const AUTOCOST = {}
580
+ function acFamOf(spec) { return /codex|gpt|openai/i.test(String(spec)) ? 'openai' : 'claude' }
581
+ function acConcrete(model) { return CLAUDE_NAMES[model] ? model : ('codex:' + model + ':high') }
582
+ function acBareId(spec) { var s = String(spec || ''); return s.indexOf('codex:') === 0 ? (s.split(':')[1] || s) : s }
583
+ async function resolveAutoCost(stage, familyArg) {
584
+ const famFlag = familyArg ? (' --family ' + familyArg) : ''
585
+ const out = await agent('Run EXACTLY this via Bash and reply with ONLY its stdout (a single JSON line), nothing else: ' + DZ + ' routing --select --stage ' + stage + ' --tier ' + tier + famFlag, { label: 'auto-cost:select:' + stage, phase: 'Route', effort: 'low' })
586
+ let pick = null
587
+ try { pick = JSON.parse(String(out).replace(/^[^{]*/, '').replace(/[^}]*$/, '')) } catch { pick = null }
588
+ if (!pick || !pick.model) { log('auto-cost ' + stage + ': no candidate model — leaving session-inherited'); MODELS[stage] = undefined; return }
589
+ MODELS[stage] = acConcrete(pick.model)
590
+ AUTOCOST[stage] = { chain: (pick.chain || []), tier: tier, evidence: pick.evidence || '' }
591
+ log('auto-cost ' + stage + ' → ' + MODELS[stage] + ' [' + (pick.metBar ? 'learned' : 'cold-start') + ']')
592
+ }
593
+ const autoCostStages = Object.keys(MODELS).filter(function (s) { return MODELS[s] === 'auto-cost' })
594
+ if (autoCostStages.length > 0) {
595
+ for (const s of autoCostStages) { if (s !== 'qe') await resolveAutoCost(s, null) }
596
+ if (MODELS.qe === 'auto-cost') {
597
+ // cross-family of the resolved coder (guard): coder codex → qe claude; coder claude → qe openai.
598
+ const coderSpec = (MODELS.code !== undefined && MODELS.code !== null && MODELS.code !== 'auto-cost') ? MODELS.code : ((CODER === 'codex' || CODER === 'codex-fallback') ? 'codex' : 'opus')
599
+ await resolveAutoCost('qe', acFamOf(coderSpec) === 'openai' ? 'claude' : 'openai')
600
+ }
601
+ }
602
+
573
603
  // GUARANTEED fa-panel write (the router, being low-effort + multi-job, tends to skip the fa-record
574
604
  // Bash call). A dedicated single-command agent reliably lights up the live /feature-adr panel at the
575
605
  // most visible moment. Uses the workspace bin (PATH-independent). Best-effort — never blocks.
@@ -663,6 +693,73 @@ if (plan === null) {
663
693
  plan = claudePlan ? { wrote: claudePlan.wrote, summary: claudePlan.summary, planner: planIsCodex ? 'claude-fallback' : 'claude' } : null
664
694
  }
665
695
 
696
+ // ── R6 challenge panel: adversarial plan-gate at the checkpoint (ADVISE, never block) ──
697
+ // The panel is NEVER the plan's own author (ADR §1 hard invariant): author=Claude → a cross-family Codex
698
+ // adversary via the HONEST synchronous codex exec path (safeCodexAgent — a stub would read as a clean
699
+ // review, so it is never used for this data-returning verdict); author=Codex → a FRESH Claude adversary.
700
+ // The adversary loads the WIDE context via `dz challenge` (plan + vision + testing + map + degradations),
701
+ // answers C1-C8 in break-it mode, then every P0/P1 is cross-validated by an independent agent and the
702
+ // non-validated ones are dropped (anti-noise). Failure anywhere degrades LOUDLY to a fresh Claude panel.
703
+ const CHALLENGE_SCHEMA = { type: 'object', additionalProperties: false, required: ['findings', 'summary'], properties: { findings: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['c', 'severity', 'title', 'why'], properties: { c: { type: 'string' }, severity: { type: 'string', enum: ['P0', 'P1', 'P2'] }, title: { type: 'string' }, why: { type: 'string' }, where: { type: 'string' } } } }, summary: { type: 'string' } } }
704
+ const CHALLENGE_CIDS = new Set(['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8'])
705
+ // sh-quote for safe interpolation into a Bash string (QE #13 injection via REPO path / dzBin).
706
+ function shq(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'" }
707
+ // Validate a raw adversary payload → {findings,summary} or null (QE #7: {} / {findings:[]} / [null] are not
708
+ // a fake-clean pass). A value without a findings ARRAY is null → loud fallback; junk findings are dropped.
709
+ function sanitizeChallengeVerdict(raw) {
710
+ if (!raw || typeof raw !== 'object' || !Array.isArray(raw.findings)) return null
711
+ const findings = raw.findings.filter((f) => f && typeof f === 'object' && CHALLENGE_CIDS.has(String(f.c)) && (f.severity === 'P0' || f.severity === 'P1' || f.severity === 'P2') && typeof f.title === 'string' && f.title !== '' && typeof f.why === 'string' && f.why !== '')
712
+ return { findings: findings, summary: typeof raw.summary === 'string' ? raw.summary : '' }
713
+ }
714
+ async function runChallengePanel(planRel, plannerName) {
715
+ const authorIsCodex = /codex|gpt|openai|\bo[1-9]\b/i.test(String(plannerName || ''))
716
+ const dzChallenge = 'cd ' + shq(REPO) + ' && ' + shq(DZ) + ' challenge --plan ' + shq(planRel) + ' --author ' + shq(plannerName || 'claude')
717
+ // Preflight (QE #11): the plan artifact must EXIST and be non-empty, else an adversary hallucinates a
718
+ // verdict on a missing file. Surface a loud status instead of a fake review.
719
+ const pre = await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + 'cd ' + shq(REPO) + ' && (test -s ' + shq(planRel) + ' && echo PLAN_OK || echo PLAN_MISSING)', { label: 'challenge:preflight', phase: 'Plan', effort: 'low' })
720
+ if (!/PLAN_OK/.test(String(pre || ''))) { log('Challenge panel: plan artifact missing/empty (' + planRel + ') — panel skipped'); return { status: 'no-plan', adversary: null, findings: [], summary: '', note: 'Challenge panel skipped — the implementation plan artifact was missing or empty.' } }
721
+ let verdict = null
722
+ let adversary = authorIsCodex ? 'claude' : 'codex'
723
+ if (!authorIsCodex) {
724
+ // author=Claude → Codex adversary (cross-family). Compact prompt: Codex reads the files itself (no 24k
725
+ // brief inlined). safeCodexAgent is the honest exec path; null/invalid ⇒ loud Claude fallback below.
726
+ const cx = await safeCodexAgent('You are a FRESH adversarial reviewer of an implementation plan you did NOT write. Read these files: ' + planRel + ' , architecture/vision.md , architecture/degradations.md (relative to repo ' + REPO + '). BREAK the plan, do not confirm it. Answer C1 arch-anti-cement (deviating from a pattern in the degradations registry is NOT a finding), C2 prod-ready, C3 test sufficiency+honesty both ways, C4 overengineering, C5 silent decisions, C6 runtime consistency, C7 scope>1.5x, C8 executability. Output ONLY minified JSON {"findings":[{"c","severity":"P0|P1|P2","title","why","where"}],"summary"}.', { label: 'challenge:codex-adversary', phase: 'Plan' })
727
+ if (cx) { try { verdict = sanitizeChallengeVerdict(JSON.parse(String(cx).replace(/^[^{]*/, '').replace(/[^}]*$/, ''))) } catch { verdict = null } }
728
+ if (!verdict) { log('Challenge panel: Codex adversary unavailable/unparseable/invalid — falling back to a FRESH Claude panel (NOT cross-family; run `dz challenge` + codex manually for a cross-family pass)'); adversary = 'claude-fallback' }
729
+ }
730
+ if (!verdict) {
731
+ // Claude adversary (fresh instance ≠ the author): loads the WIDE brief via dz, then answers the schema.
732
+ const raw = await agent('You are a FRESH adversarial reviewer. You did NOT write this plan. First run EXACTLY this via Bash to load the wide challenge brief (plan + vision + testing + map + degradations + the C1-C8 questions): ' + dzChallenge + '\nThen BREAK the plan per C1-C8 (do NOT confirm it): a finding is a concrete failing input/condition, never a general worry; deviating from a pattern in the degradations registry is NOT a finding. Return the verdict.', { label: 'challenge:claude-adversary', phase: 'Plan', schema: CHALLENGE_SCHEMA })
733
+ verdict = sanitizeChallengeVerdict(raw)
734
+ }
735
+ if (!verdict) { log('Challenge panel: no usable verdict from any adversary — surfacing unavailable status (advisory)'); return { status: 'adversary-unavailable', adversary: adversary, findings: [], summary: '', note: 'Challenge panel could not produce a verdict — run `dz challenge` + the panel manually.' } }
736
+ // Cross-validate P0/P1 by INDEX (QE #5: never by title — duplicate titles cross-contaminate). Deterministic
737
+ // sorted order shared with the validator; results[i] aligns to pp[i].
738
+ const rank = { P0: 3, P1: 2, P2: 1 }
739
+ const pp = verdict.findings.filter((f) => f.severity === 'P0' || f.severity === 'P1').sort((a, b) => (rank[b.severity] - rank[a.severity]) || (a.c < b.c ? -1 : a.c > b.c ? 1 : 0))
740
+ let confirmed = verdict.findings.filter((f) => f.severity === 'P2')
741
+ let status = 'ok'
742
+ if (pp.length > 0) {
743
+ const numbered = pp.map((f, i) => ({ i: i, c: f.c, severity: f.severity, title: f.title, why: f.why }))
744
+ const cv = await agent('Independently CROSS-VALIDATE these adversarial plan findings against the plan at ' + planRel + ' (repo ' + REPO + '). For EACH by its "i" index, decide if it is REAL and reachable, or FP/theory. Default to real=false when uncertain. Findings JSON (with stable index i): ' + JSON.stringify(numbered) + '\nReturn {"results":[{"i":<the index>,"real":true|false}...]} covering EVERY index exactly once.', { label: 'challenge:cross-validate', phase: 'Plan', schema: { type: 'object', additionalProperties: false, required: ['results'], properties: { results: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['i', 'real'], properties: { i: { type: 'number' }, real: { type: 'boolean' } } } } } } })
745
+ const realByIndex = new Map((cv && Array.isArray(cv.results) ? cv.results : []).map((r) => [Number(r.i), r.real === true]))
746
+ // QE #8: a validator OUTAGE (missing indices) must NOT read as "clean" — do not silently drop the P0/P1.
747
+ const covered = pp.every((_, i) => realByIndex.has(i))
748
+ if (!covered) {
749
+ status = 'cross-validation-incomplete'
750
+ log('Challenge panel: cross-validator did not cover every P0/P1 — surfacing them UNVALIDATED (not dropped, not confirmed)')
751
+ for (const f of pp) confirmed.push(Object.assign({}, f, { crossValidated: false, unvalidated: true }))
752
+ } else {
753
+ pp.forEach((f, i) => { if (realByIndex.get(i) === true) confirmed.push(Object.assign({}, f, { crossValidated: true })) })
754
+ }
755
+ }
756
+ confirmed.sort((a, b) => (rank[b.severity] - rank[a.severity]) || (a.c < b.c ? -1 : a.c > b.c ? 1 : 0))
757
+ const note = status === 'cross-validation-incomplete'
758
+ ? 'ADVISORY — cross-validation was INCOMPLETE; P0/P1 shown are UNVALIDATED (verify manually). Nothing blocks.'
759
+ : 'ADVISORY — the owner decides; nothing blocks. Cross-validated P0/P1 + all P2 shown.'
760
+ return { status: status, adversary: adversary, findings: confirmed, summary: verdict.summary, note: note }
761
+ }
762
+
666
763
  // Hybrid checkpoint for L/XL
667
764
  const stopHere = STOP_AFTER === 'plan' || (isLplus && STOP_AFTER !== 'none')
668
765
  if (stopHere) {
@@ -673,7 +770,11 @@ if (stopHere) {
673
770
  const qePlanned = qeShouldUseCodex() ? modelLabel(resolveStageModel('qe')) : modelLabel(mergeOpts({ agentType: 'qe-code-reviewer' }, resolveStageModel('qe')))
674
771
  const plannedModels = mergeOpts(modelsUsed, { code: codePlanned + ' (planned)', qe: qePlanned + ' (planned)' })
675
772
  if (isLplus) plannedModels.fleet = modelLabel(resolveStageModel('fleet')) + ' (planned)'
676
- return { tier: tier, phase: 'checkpoint-after-plan', artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: plannedModels, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: 'L/XL checkpoint - review the ADR + plan (+ the planned code/qe/fleet models), then re-invoke with args.stopAfter="none" to implement + QE.' }
773
+ // R6 врезка: adversarial plan-gate (advise). Panel plan author; wrapped so a panel failure never blocks the checkpoint.
774
+ let challengeVerdict = null
775
+ try { challengeVerdict = plan ? await runChallengePanel('features/' + SLUG + '/06_implementation_plan.md', plan.planner) : null }
776
+ catch (e) { log('Challenge panel errored (advisory, ignored): ' + (e && e.message ? e.message : String(e))) }
777
+ return { tier: tier, phase: 'checkpoint-after-plan', artifactsDir: FDIR, planner: (plan ? plan.planner : null), plan: (plan ? plan.summary : null), modelsUsed: plannedModels, challengeVerdict: challengeVerdict, usageEvents: usageEvents, usageThreshold: USAGE_THRESHOLD, polymorphism: POLY.hasManifest ? POLY.report : null, note: 'L/XL checkpoint - review the ADR + plan (+ the planned code/qe/fleet models) + the challenge panel verdict (advisory), then re-invoke with args.stopAfter="none" to implement + QE.' }
677
778
  }
678
779
 
679
780
  // Step 7: Code (optional Codex fallback on Claude-limit exhaustion)
@@ -774,6 +875,55 @@ if (qe === null && qeIsCodex) {
774
875
  const claimGate = step8ClaimGate(qe && qe.claimCheck ? qe.claimCheck : null)
775
876
  log(claimGate.note)
776
877
 
878
+ // ── AUTO-COST outcome recording (feature learned-cost-routing) ──
879
+ // The two-phase label lands here: every auto-cost stage that produced an artifact records a PROVISIONAL (i);
880
+ // the CODE stage's authoritative gate (ii) is the Step-8 QE grade (A/B = pass) — a produced-but-gate-FAILED
881
+ // run is recorded as a FAILURE, down-ranking that model for the next run (ADR §2). Byte-identical no-op when
882
+ // nothing was auto-cost. Gate-less stages get weak provisional credit; richer per-stage gates are Phase-2.
883
+ if (Object.keys(AUTOCOST).length > 0) {
884
+ const recPhase = isLplus ? 'FleetQE' : 'QE'
885
+ // Record ONLY stages that ACTUALLY RAN and produced an artifact (QE finding: a blanket loop over configured
886
+ // auto-cost stages credited skipped/failed/fallback stages). `code`/`plan` are guarded by their result var.
887
+ // ATTRIBUTION guard (QE #2): the auto-cost pick's outcome must be attributed to the PICK's model — but a
888
+ // codex-FALLBACK produces the code on a DIFFERENT model when the picked model returned null. So:
889
+ // • the picked model ran & produced → provisional + finalize by the QE gate.
890
+ // • a fallback fired (pick returned null) → the PICK FAILED to deliver → finalize(pick, false), NOT the
891
+ // fallback model (which was not auto-cost-selected). This down-ranks the pick honestly.
892
+ const codeMid = acBareId(MODELS.code)
893
+ if (AUTOCOST.code && codeMid) {
894
+ const pickIsCodex = /codex|gpt/i.test(String(MODELS.code))
895
+ const pickRan = pickIsCodex ? (coderUsed === 'codex') : (coderUsed === 'claude')
896
+ if (pickRan && code) {
897
+ const codePassed = !!(qe && /^[AB]$/i.test(String(qe.grade || '').trim()))
898
+ await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + DZ + ' routing --record-provisional --stage code --tier ' + AUTOCOST.code.tier + ' --model ' + codeMid, { label: 'auto-cost:record:code', phase: recPhase, effort: 'low' })
899
+ await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + DZ + ' routing --finalize --stage code --tier ' + AUTOCOST.code.tier + ' --model ' + codeMid + ' --success ' + (codePassed ? 'true' : 'false'), { label: 'auto-cost:finalize:code', phase: recPhase, effort: 'low' })
900
+ } else {
901
+ // the picked model did not deliver (fallback fired or produced nothing) → record it as a failure.
902
+ log('auto-cost code: picked model ' + codeMid + ' did not deliver (coderUsed=' + coderUsed + ') — recording a failure')
903
+ await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + DZ + ' routing --finalize --stage code --tier ' + AUTOCOST.code.tier + ' --model ' + codeMid + ' --success false', { label: 'auto-cost:finalize:code-fail', phase: recPhase, effort: 'low' })
904
+ }
905
+ }
906
+ // PLAN: provisional only (its gate is landing, already enforced upstream); guarded by the plan result var.
907
+ const planMid = acBareId(MODELS.plan)
908
+ if (AUTOCOST.plan && plan && planMid) {
909
+ await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + DZ + ' routing --record-provisional --stage plan --tier ' + AUTOCOST.plan.tier + ' --model ' + planMid, { label: 'auto-cost:record:plan', phase: recPhase, effort: 'low' })
910
+ }
911
+ // QE: gate (ii) = produced a PARSEABLE verdict with a grade (qe non-null). A qe that named no grade fell back
912
+ // and is not the auto-cost pick, so record only when the picked reviewer actually delivered a verdict. Its
913
+ // model ran iff qeReviewerUsed matches the pick's family (else a belt/fallback reviewer ran).
914
+ const qeMid = acBareId(MODELS.qe)
915
+ if (AUTOCOST.qe && qeMid) {
916
+ const qePickIsCodex = /codex|gpt/i.test(String(MODELS.qe))
917
+ const qePickRan = qePickIsCodex ? (qeReviewerUsed === 'codex') : (qeReviewerUsed === 'claude')
918
+ if (qePickRan && qe && String(qe.grade || '').trim() !== '') {
919
+ await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + DZ + ' routing --record-provisional --stage qe --tier ' + AUTOCOST.qe.tier + ' --model ' + qeMid, { label: 'auto-cost:record:qe', phase: recPhase, effort: 'low' })
920
+ await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + DZ + ' routing --finalize --stage qe --tier ' + AUTOCOST.qe.tier + ' --model ' + qeMid + ' --success true', { label: 'auto-cost:finalize:qe', phase: recPhase, effort: 'low' })
921
+ } else {
922
+ await agent('Run EXACTLY this via Bash and reply with ONLY its stdout: ' + DZ + ' routing --finalize --stage qe --tier ' + AUTOCOST.qe.tier + ' --model ' + qeMid + ' --success false', { label: 'auto-cost:finalize:qe-fail', phase: recPhase, effort: 'low' })
923
+ }
924
+ }
925
+ }
926
+
777
927
  // Step 9: Fleet QE (L/XL)
778
928
  let fleet = 'skipped (S/M)'
779
929
  if (isLplus) {
@@ -818,5 +968,6 @@ return {
818
968
  brain: BRAIN,
819
969
  polymorphism: POLY.hasManifest ? POLY.report : null,
820
970
  claimGate: claimGate,
971
+ autoCost: Object.keys(AUTOCOST).length ? AUTOCOST : null,
821
972
  promiseTags: tags,
822
973
  }