@dzhechkov/harness-core 0.8.2 → 0.8.6

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.
Files changed (81) hide show
  1. package/.dz-manifest.json +146 -58
  2. package/README.md +72 -2
  3. package/dist/cmd-usage.d.ts +148 -0
  4. package/dist/cmd-usage.d.ts.map +1 -0
  5. package/dist/cmd-usage.js +548 -0
  6. package/dist/cmd-usage.js.map +1 -0
  7. package/dist/compounding.d.ts +4 -0
  8. package/dist/compounding.d.ts.map +1 -1
  9. package/dist/compounding.js +6 -0
  10. package/dist/compounding.js.map +1 -1
  11. package/dist/contract-checklist.d.ts +123 -0
  12. package/dist/contract-checklist.d.ts.map +1 -0
  13. package/dist/contract-checklist.js +700 -0
  14. package/dist/contract-checklist.js.map +1 -0
  15. package/dist/feature-adr-checkpoints.d.ts +11 -2
  16. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  17. package/dist/feature-adr-checkpoints.js +37 -2
  18. package/dist/feature-adr-checkpoints.js.map +1 -1
  19. package/dist/feature-adr-routing.d.ts +58 -23
  20. package/dist/feature-adr-routing.d.ts.map +1 -1
  21. package/dist/feature-adr-routing.js +208 -59
  22. package/dist/feature-adr-routing.js.map +1 -1
  23. package/dist/guard.d.ts +25 -0
  24. package/dist/guard.d.ts.map +1 -1
  25. package/dist/guard.js +59 -1
  26. package/dist/guard.js.map +1 -1
  27. package/dist/index.d.ts +9 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +8 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/loop-blobs.generated.js +8 -8
  32. package/dist/loop-blobs.generated.js.map +1 -1
  33. package/dist/loop-plan.d.ts +13 -1
  34. package/dist/loop-plan.d.ts.map +1 -1
  35. package/dist/loop-plan.js +15 -1
  36. package/dist/loop-plan.js.map +1 -1
  37. package/dist/loop-render.d.ts.map +1 -1
  38. package/dist/loop-render.js +51 -6
  39. package/dist/loop-render.js.map +1 -1
  40. package/dist/loop-trace.d.ts +20 -1
  41. package/dist/loop-trace.d.ts.map +1 -1
  42. package/dist/loop-trace.js +83 -1
  43. package/dist/loop-trace.js.map +1 -1
  44. package/dist/model-recommender.d.ts +8 -0
  45. package/dist/model-recommender.d.ts.map +1 -1
  46. package/dist/model-recommender.js +31 -4
  47. package/dist/model-recommender.js.map +1 -1
  48. package/dist/qe-bridge.d.ts.map +1 -1
  49. package/dist/qe-bridge.js +9 -0
  50. package/dist/qe-bridge.js.map +1 -1
  51. package/dist/restart-advisor.d.ts +103 -0
  52. package/dist/restart-advisor.d.ts.map +1 -0
  53. package/dist/restart-advisor.js +445 -0
  54. package/dist/restart-advisor.js.map +1 -0
  55. package/dist/slop-lint.d.ts +128 -0
  56. package/dist/slop-lint.d.ts.map +1 -0
  57. package/dist/slop-lint.js +607 -0
  58. package/dist/slop-lint.js.map +1 -0
  59. package/dist/workflow-run.d.ts.map +1 -1
  60. package/dist/workflow-run.js +18 -12
  61. package/dist/workflow-run.js.map +1 -1
  62. package/package.json +19 -15
  63. package/sbom.json +277 -57
  64. package/src/cmd-usage.ts +720 -0
  65. package/src/compounding.ts +13 -0
  66. package/src/contract-checklist.ts +973 -0
  67. package/src/deadwood-allowlist.json +80 -0
  68. package/src/feature-adr-checkpoints.ts +38 -2
  69. package/src/feature-adr-routing.ts +238 -55
  70. package/src/guard.ts +79 -1
  71. package/src/index.ts +81 -1
  72. package/src/loop-blobs.generated.ts +8 -8
  73. package/src/loop-plan.ts +36 -3
  74. package/src/loop-render.ts +50 -6
  75. package/src/loop-trace.ts +91 -2
  76. package/src/model-recommender.ts +35 -4
  77. package/src/qe-bridge.ts +9 -0
  78. package/src/restart-advisor.ts +579 -0
  79. package/src/slop-lint.ts +762 -0
  80. package/src/slop-markers.json +71 -0
  81. package/src/workflow-run.ts +18 -11
@@ -0,0 +1,80 @@
1
+ {
2
+ "v": 1,
3
+ "entries": [
4
+ {
5
+ "surface": "sign",
6
+ "kind": "command",
7
+ "reason": "trust-root signing and verification run only on key or release events; rarity is the design"
8
+ },
9
+ {
10
+ "surface": "delivery-check",
11
+ "kind": "command",
12
+ "reason": "release gate invoked per delivery, not as part of ordinary daily command traffic"
13
+ },
14
+ {
15
+ "surface": "deadwood",
16
+ "kind": "command",
17
+ "reason": "the measurement command deliberately does not record itself, so its zero count is structural"
18
+ },
19
+ {
20
+ "surface": "no-secrets",
21
+ "kind": "rule",
22
+ "reason": "credential safety net whose zero-firing state is the healthy outcome"
23
+ },
24
+ {
25
+ "surface": "no-workspace-star",
26
+ "kind": "rule",
27
+ "reason": "publish safety net; zero firings means no broken workspace dependency would ship"
28
+ },
29
+ {
30
+ "surface": "no-skill-drift",
31
+ "kind": "rule",
32
+ "reason": "canonical-copy safety net; zero firings means projected skills stayed in sync"
33
+ },
34
+ {
35
+ "surface": "readme-consistency",
36
+ "kind": "rule",
37
+ "reason": "documentation drift safety net; zero firings means measured counts stayed aligned"
38
+ },
39
+ {
40
+ "surface": "skills-registrable",
41
+ "kind": "rule",
42
+ "reason": "package registration safety net; zero firings means shipped skill layouts stayed loadable"
43
+ },
44
+ {
45
+ "surface": "readme-first",
46
+ "kind": "rule",
47
+ "reason": "release documentation safety net; zero firings means behavior changes carried their README update"
48
+ },
49
+ {
50
+ "surface": "agents-md-policy-sync",
51
+ "kind": "rule",
52
+ "reason": "governance projection safety net; zero firings means policy copies stayed synchronized"
53
+ },
54
+ {
55
+ "surface": "lockfile-in-sync",
56
+ "kind": "rule",
57
+ "reason": "dependency release safety net; zero firings means package manifests and the lockfile agreed"
58
+ },
59
+ {
60
+ "surface": "store-bloat-cap",
61
+ "kind": "rule",
62
+ "reason": "learning-store capacity safety net; zero firings means the configured cap was respected"
63
+ },
64
+ {
65
+ "surface": "no-stubs",
66
+ "kind": "rule",
67
+ "reason": "incomplete-source safety net; zero firings means no unfinished implementation marker reached publish"
68
+ },
69
+ {
70
+ "surface": "review-round",
71
+ "kind": "rule",
72
+ "reason": "independent-review safety net evaluated only for publishable source changes"
73
+ },
74
+ {
75
+ "surface": "licence-hold",
76
+ "kind": "rule",
77
+ "reason": "release safety net that fires only while a package has an unresolved licence grant"
78
+ }
79
+ ]
80
+ }
@@ -26,7 +26,7 @@
26
26
 
27
27
  /** Blob version stamp read by scripts/gen-loop-blobs.mjs (feature loop-designer, ADR-004) — the
28
28
  * ONLY loop-designer change to this canonical file; bump when any blob-exported semantic changes. */
29
- export const BLOB_VERSION = '1.1.0';
29
+ export const BLOB_VERSION = '1.2.0';
30
30
 
31
31
  /** Stages the workflow checkpoints, in pipeline order. Cheap side-channel agents (usage probes,
32
32
  * fa-record, auto-cost selects) are never checkpointed; the opt-in Delivery gate re-runs by design
@@ -421,7 +421,7 @@ export function captureFailureRecord(stage: unknown, mode: unknown, reason: unkn
421
421
  }
422
422
 
423
423
  /** Training-pair record format version. Bump on any field-shape change. */
424
- export const TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-2';
424
+ export const TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-3';
425
425
 
426
426
  /** Oversize guard cap over input+output combined (same posture as
427
427
  * CHECKPOINT_MAX_RESULT_CHARS, sized for full stage prompts): an over-cap pair is
@@ -460,6 +460,14 @@ export interface TrainingPairProvenance {
460
460
  minutes: number | null;
461
461
  }
462
462
 
463
+ /** Resolved routing axes captured with every pair for later Fable-vs-grade analysis. */
464
+ export interface TrainingPairBudget {
465
+ primary: 'claude' | 'codex';
466
+ claude: 'normal' | 'eco';
467
+ codex: 'normal' | 'eco';
468
+ preset: 'normal' | 'eco' | 'hybrid' | 'custom' | 'unset';
469
+ }
470
+
463
471
  export interface TrainingPairTruncation {
464
472
  /** Original (pre-truncation) char counts + full-text hashes — what was cut is NAMED. */
465
473
  inputChars: number;
@@ -479,6 +487,7 @@ export interface TrainingPair {
479
487
  output: string;
480
488
  evaluation: TrainingPairEvaluation;
481
489
  provenance: TrainingPairProvenance;
490
+ budgetMode: TrainingPairBudget | null;
482
491
  truncated: TrainingPairTruncation | null;
483
492
  captureMode: 'capture' | 'backfill';
484
493
  resumed: boolean;
@@ -615,6 +624,31 @@ function coerceText(v: unknown): string {
615
624
  }
616
625
  }
617
626
 
627
+ /** Normalize routing metadata without ever blocking capture. Undefined means the operator supplied
628
+ * neither axis; malformed/throwing inputs are explicit null, never a fabricated nearest preset.
629
+ * An explicit `preset:'unset'` preserves primary while recording that no budget arg was supplied. */
630
+ function normalizeTrainingPairBudget(raw: unknown): TrainingPairBudget | null {
631
+ try {
632
+ if (raw === undefined) return { primary: 'claude', claude: 'normal', codex: 'normal', preset: 'unset' };
633
+ if (raw === null || typeof raw !== 'object') return null;
634
+ const value = raw as Record<string, unknown>;
635
+ const primary = value.primary;
636
+ const claude = value.claude;
637
+ const codex = value.codex;
638
+ if (primary !== 'claude' && primary !== 'codex') return null;
639
+ if (claude !== 'normal' && claude !== 'eco') return null;
640
+ if (codex !== 'normal' && codex !== 'eco') return null;
641
+ if (value.preset === 'unset') return { primary, claude, codex, preset: 'unset' };
642
+ let preset: TrainingPairBudget['preset'] = 'custom';
643
+ if (claude === 'normal' && codex === 'normal') preset = 'normal';
644
+ else if (claude === 'eco' && codex === 'eco') preset = 'eco';
645
+ else if (claude === 'eco' && codex === 'normal') preset = 'hybrid';
646
+ return { primary, claude, codex, preset };
647
+ } catch {
648
+ return null;
649
+ }
650
+ }
651
+
618
652
  /** Assemble one SFT-ready training pair. Deterministic (ts passed in). Applies the oversize
619
653
  * guard: when input+output exceed TRAINPAIR_MAX_IO_CHARS combined, each over-budget side is
620
654
  * truncated with a marker naming the cut char count + the fnv1a64 of its FULL text (the
@@ -630,6 +664,7 @@ export function buildTrainingPair(opts: {
630
664
  output: unknown;
631
665
  evaluation?: Partial<TrainingPairEvaluation> | null;
632
666
  provenance?: Partial<TrainingPairProvenance> | null;
667
+ budgetMode?: unknown;
633
668
  captureMode?: unknown;
634
669
  resumed?: unknown;
635
670
  }): TrainingPair {
@@ -670,6 +705,7 @@ export function buildTrainingPair(opts: {
670
705
  tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,
671
706
  minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,
672
707
  },
708
+ budgetMode: normalizeTrainingPairBudget(opts.budgetMode),
673
709
  truncated,
674
710
  captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',
675
711
  resumed: opts.resumed === true,
@@ -14,8 +14,8 @@
14
14
  *
15
15
  * The load-bearing property: a model that WRITES code must not also SELF-QE.
16
16
  * When `args.models.qe` is unset, the QE stage is auto-routed to the OTHER
17
- * family than the resolved coder (codex-coder → Claude `opus`; Claude-coder →
18
- * `codex:<top>:high`, or `opus` if codex is unavailable — never a block).
17
+ * family than the resolved coder (codex-coder → Claude `sonnet`; Claude-coder →
18
+ * the Codex-budget cell, or `opus` if codex is unavailable — never a block).
19
19
  *
20
20
  * DESIGN CONSTRAINT — the Workflow parser is STRICTER than `node --check`
21
21
  * (no nested template literals, no inline `cond ? agent() : null` in arrays).
@@ -74,11 +74,15 @@ export interface RoutingEnv {
74
74
  */
75
75
  readonly usageOverride?: boolean;
76
76
  /**
77
- * Per-stage reasoning for the usage-override spec (merged OVER {@link OVERRIDE_REASONING}).
78
- * `args.usageReasoning` — stage → `'high'|'xhigh'|…`; a single stage may be overridden without
77
+ * Per-stage reasoning for the usage-override spec (merged OVER {@link STAGE_EFFORT}).
78
+ * `args.usageReasoning` — stage → a supported reasoning level; a single stage may be overridden without
79
79
  * touching the others.
80
80
  */
81
81
  readonly usageReasoning?: Record<string, string>;
82
+ /** Raw `args.budget`: a named preset or a per-family object. */
83
+ readonly budget?: unknown;
84
+ /** The productive family for design + code; absent defaults to Claude. */
85
+ readonly primary?: 'claude' | 'codex';
82
86
  }
83
87
 
84
88
  // ── USAGE-ADAPTIVE ROUTING (pre-emptive codex switch at >= usageThreshold) ────
@@ -109,39 +113,33 @@ export interface UsageDecision {
109
113
  }
110
114
 
111
115
  /**
112
- * Per-stage reasoning applied under the usage override (LOCKED L-4, arch §4.1 verbatim):
113
- * design/code/plan stages `xhigh`; router/qe/fleet `high`. A pure DATA table (not control
114
- * flow); user-overridable via `env.usageReasoning`. An unknown stage falls back to `'high'`.
116
+ * GPT-5.6 reasoning ladder used by the routing tables. The model default is `medium`; `xhigh`
117
+ * and `max` stay valid explicit values but are absent from shipped defaults until evals justify them.
115
118
  */
116
- export const OVERRIDE_REASONING: Record<string, 'high' | 'xhigh'> = {
117
- router: 'high',
118
- requirements: 'xhigh',
119
- research: 'xhigh',
120
- adr: 'xhigh',
121
- ideation: 'xhigh',
122
- ddd: 'xhigh',
123
- architecture: 'xhigh',
124
- plan: 'xhigh',
125
- code: 'xhigh',
119
+ export type ReasoningLevel = 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
120
+ export const STAGE_EFFORT: { override: Record<string, ReasoningLevel> } = { override: {
121
+ router: 'medium',
122
+ requirements: 'medium',
123
+ research: 'medium',
124
+ adr: 'high',
125
+ ideation: 'medium',
126
+ ddd: 'high',
127
+ architecture: 'high',
128
+ plan: 'high',
129
+ code: 'medium',
126
130
  qe: 'high',
127
- fleet: 'high',
128
- };
131
+ fleet: 'medium',
132
+ } };
129
133
 
130
- /**
131
- * The gpt-5.6-ready TOP codex-id pick SHARED by the cross-model QE default and the usage
132
- * override so adding an id to {@link KNOWN_CODEX} (e.g. `gpt-5.7`) retargets BOTH with zero
133
- * control-flow diff (AC-3). Pinned `CODEX_MODEL` when `'auto'`; else the last non-`auto` key of
134
- * `KNOWN_CODEX`.
135
- */
134
+ /** One-release compatibility alias for existing public imports. */
135
+ // Frozen (QE round-1 F10): the alias exports the SAME object as the live effort table, so a
136
+ // consumer mutating the compatibility alias would silently mutate shipped routing. Freezing keeps
137
+ // the read-compatibility promise while making any such write throw loudly in strict mode.
138
+ export const OVERRIDE_REASONING = Object.freeze(STAGE_EFFORT.override);
139
+
140
+ /** The flagship Codex id, independent of the spellability allowlist's insertion order. */
136
141
  export function topCodexId(env: RoutingEnv): string {
137
- let top = env.CODEX_MODEL;
138
- if (top === 'auto') {
139
- const ids = Object.keys(KNOWN_CODEX);
140
- for (let i = 0; i < ids.length; i++) {
141
- if (ids[i] !== 'auto') top = ids[i] || top;
142
- }
143
- }
144
- return top;
142
+ return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS.flagship;
145
143
  }
146
144
 
147
145
  /**
@@ -190,11 +188,80 @@ export function decideUsageAction(
190
188
  /** Known codex ids. Adding a new id (e.g. `'gpt-5.7'`) is a DATA-ONLY change. */
191
189
  export const KNOWN_CODEX: Record<string, number> = { auto: 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 };
192
190
 
191
+ export type CodexTier = 'flagship' | 'workhorse' | 'high-volume';
192
+
193
+ /**
194
+ * Capability tiers are routing data, separate from {@link KNOWN_CODEX}'s spellability role.
195
+ * The allowlist is not an availability check — probe every id before every run
196
+ * (`codex exec -m <id> "Reply with exactly: OK"`); ids drift in both directions on a
197
+ * timescale of days (probed 2026-08-18: 3 ids HTTP 400; probed 2026-08-29: all 3 exit 0).
198
+ */
199
+ export const CODEX_TIERS: Record<CodexTier, string> = {
200
+ flagship: 'gpt-5.6-sol',
201
+ workhorse: 'gpt-5.6-terra',
202
+ 'high-volume': 'gpt-5.6-luna',
203
+ };
204
+
205
+ export function codexIdForTier(tier: CodexTier, env: RoutingEnv): string {
206
+ return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS[tier];
207
+ }
208
+
209
+ export type BudgetLevel = 'normal' | 'eco';
210
+ export interface BudgetAxis {
211
+ readonly claude: BudgetLevel;
212
+ readonly codex: BudgetLevel;
213
+ }
214
+
215
+ export const BUDGET_PRESETS: Record<'normal' | 'eco' | 'hybrid', BudgetAxis> = {
216
+ normal: { claude: 'normal', codex: 'normal' },
217
+ eco: { claude: 'eco', codex: 'eco' },
218
+ hybrid: { claude: 'eco', codex: 'normal' },
219
+ };
220
+
221
+ export function resolveBudgetMode(raw: unknown): BudgetAxis {
222
+ if (raw === undefined) return BUDGET_PRESETS.normal;
223
+ if (typeof raw === 'string') {
224
+ const preset = BUDGET_PRESETS[raw as keyof typeof BUDGET_PRESETS];
225
+ if (!preset) throw new RangeError('budget: unknown preset "' + raw + '" — valid: normal|eco|hybrid');
226
+ return preset;
227
+ }
228
+ if (raw && typeof raw === 'object') {
229
+ const value = raw as Record<string, unknown>;
230
+ for (const key of Object.keys(value)) {
231
+ if (key !== 'claude' && key !== 'codex') {
232
+ throw new RangeError('budget: unknown family key "' + key + '" — valid: claude|codex');
233
+ }
234
+ }
235
+ for (const key of ['claude', 'codex'] as const) {
236
+ const level = value[key];
237
+ if (level !== undefined && level !== 'normal' && level !== 'eco') {
238
+ throw new RangeError('budget.' + key + ': unknown level "' + level + '" — valid: normal|eco');
239
+ }
240
+ }
241
+ return {
242
+ claude: (value.claude as BudgetLevel | undefined) || 'normal',
243
+ codex: (value.codex as BudgetLevel | undefined) || 'normal',
244
+ };
245
+ }
246
+ throw new RangeError('budget: expected a preset name or {claude,codex} object, got ' + typeof raw);
247
+ }
248
+
249
+ export function budgetPresetName(axis: BudgetAxis): 'normal' | 'eco' | 'hybrid' | null {
250
+ for (const name of ['normal', 'eco', 'hybrid'] as const) {
251
+ const preset = BUDGET_PRESETS[name];
252
+ if (preset.claude === axis.claude && preset.codex === axis.codex) return name;
253
+ }
254
+ return null;
255
+ }
256
+
193
257
  /** The Claude model names the Workflow runtime accepts as `agent()` `model`. */
194
258
  export const CLAUDE_NAMES: Record<string, number> = { fable: 1, opus: 1, sonnet: 1, haiku: 1 };
195
259
 
196
- /** The codex-companion `--effort` vocabulary; a spec's `<reasoning>` is clamped to this (unknown ⇒ 'high'). */
197
- export const VALID_REASONING: Record<string, number> = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1 };
260
+ /**
261
+ * The codex-companion `--effort` vocabulary. `minimal` remains temporarily for compatibility
262
+ * until a live companion probe establishes whether it can be retired; `max` is part of GPT-5.6's ladder.
263
+ */
264
+ export const VALID_REASONING: Record<string, number> = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1, max: 1 };
198
265
 
199
266
  /**
200
267
  * The proven DEFAULT TABLE, applied only when the user opts into routing.
@@ -215,6 +282,80 @@ export const DEFAULT_MODELS: Record<string, string | null> = {
215
282
  fleet: 'sonnet',
216
283
  };
217
284
 
285
+ type FamilyRoutingTables = Record<'claude' | 'codex', Record<BudgetLevel, Partial<Record<string, string | null>>>>;
286
+
287
+ /** Four family half-tables; presets compose the Claude and Codex halves independently. */
288
+ export const ROUTING_TABLES: Record<'claude' | 'codex', FamilyRoutingTables> = {
289
+ claude: {
290
+ claude: {
291
+ normal: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'fable', ideation: 'sonnet', ddd: 'fable', architecture: 'fable', plan: 'opus', code: 'sonnet', fleet: 'sonnet' },
292
+ eco: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: 'sonnet', fleet: 'sonnet' },
293
+ },
294
+ codex: { normal: {}, eco: {} },
295
+ },
296
+ codex: {
297
+ claude: {
298
+ normal: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },
299
+ eco: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },
300
+ },
301
+ codex: { normal: {}, eco: {} },
302
+ },
303
+ };
304
+
305
+ function codexCell(tier: CodexTier, effort: ReasoningLevel, env: RoutingEnv): string {
306
+ return 'codex:' + codexIdForTier(tier, env) + ':' + effort;
307
+ }
308
+
309
+ export function budgetTable(
310
+ primary: 'claude' | 'codex',
311
+ mode: BudgetAxis,
312
+ env: RoutingEnv,
313
+ ): Partial<Record<string, string | null>> {
314
+ const claudeHalf = ROUTING_TABLES[primary].claude[mode.claude];
315
+ let codexHalf: Partial<Record<string, string | null>>;
316
+ if (primary === 'claude') {
317
+ const qeSpec = mode.codex === 'normal'
318
+ ? codexCell('flagship', 'high', env)
319
+ : codexCell('workhorse', 'medium', env);
320
+ codexHalf = { ...ROUTING_TABLES.claude.codex[mode.codex], qe: env.codexAvailable === false ? 'opus' : qeSpec };
321
+ } else {
322
+ const normal = mode.codex === 'normal';
323
+ const design = codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'medium', env);
324
+ codexHalf = {
325
+ requirements: design,
326
+ research: design,
327
+ adr: design,
328
+ ideation: design,
329
+ ddd: design,
330
+ architecture: design,
331
+ plan: codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'low', env),
332
+ code: codexCell(normal ? 'flagship' : 'workhorse', 'medium', env),
333
+ };
334
+ }
335
+ return { ...claudeHalf, ...codexHalf };
336
+ }
337
+
338
+ /** Optional Claude precision review after A-normal's recall-oriented Sonnet QE. */
339
+ export function qePrecisionPassSpec(
340
+ primary: 'claude' | 'codex',
341
+ budget: BudgetAxis,
342
+ tier: 'S' | 'M' | 'L' | 'XL',
343
+ _env: RoutingEnv,
344
+ ): string | null {
345
+ if (primary !== 'codex') return null;
346
+ if (budget.claude !== 'normal') return null;
347
+ if (tier !== 'L' && tier !== 'XL') return null;
348
+ return 'opus';
349
+ }
350
+
351
+ /** Build/test-time guard for the matrix's load-bearing cross-family property. */
352
+ export function assertCrossFamilyQe(codeSpec: string | null, qeSpec: string | null): void {
353
+ const fam = (s: string | null) => (s && String(s).split(':')[0] === 'codex') ? 'codex' : 'claude';
354
+ if (fam(codeSpec) === fam(qeSpec)) {
355
+ throw new Error('cross-family QE violated: code=' + codeSpec + ' qe=' + qeSpec);
356
+ }
357
+ }
358
+
218
359
  // ── Pure resolvers (byte-equivalent to the workflow's inline block) ──────────
219
360
 
220
361
  /**
@@ -247,11 +388,8 @@ export function specToOpts(spec: string | null | undefined, env: RoutingEnv): St
247
388
  id = env.CODEX_MODEL;
248
389
  }
249
390
  let reasoning = parts[2] || 'high';
250
- // Clamp to the codex-companion --effort vocabulary so a bad token (e.g. `codex:gpt-5.6:banana`)
251
- // can't silently reach the runtime as `--effort banana` (QE LOW). Unknown ⇒ 'high'.
252
391
  if (!VALID_REASONING[reasoning]) {
253
- log('models: unknown reasoning ' + reasoning + ' — using high');
254
- reasoning = 'high';
392
+ throw new RangeError('models: invalid reasoning "' + reasoning + '"valid: ' + Object.keys(VALID_REASONING).join('|'));
255
393
  }
256
394
  return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning };
257
395
  }
@@ -278,33 +416,43 @@ export function resolveCoderSpec(env: RoutingEnv): string {
278
416
  * `MODELS.code='codex'` still routes QE to Claude (never codex-self-QE).
279
417
  */
280
418
  export function coderIsCodex(env: RoutingEnv): boolean {
281
- if (env.CODER === 'codex' || env.CODER === 'codex-fallback') return true;
282
419
  const codeSpec = env.MODELS.code;
283
- if (codeSpec && String(codeSpec).split(':')[0] === 'codex') return true;
284
- return false;
420
+ if (codeSpec !== undefined && codeSpec !== null) return String(codeSpec).split(':')[0] === 'codex';
421
+ if (env.CODER === 'codex' || env.CODER === 'codex-fallback') return true;
422
+ return env.primary === 'codex';
285
423
  }
286
424
 
287
425
  /**
288
426
  * The CROSS-MODEL QE default (load-bearing). Called only when `MODELS.qe` is
289
427
  * unset. Resolves to the OTHER family than the coder:
290
- * - coder is codex → `'opus'` (Claude reviews codex's code)
291
- * - coder is Claude → codex-available ? `'codex:<top>:high'` : `'opus'` (never block)
292
- * `<top>` = `CODEX_MODEL` when pinned (≠'auto'), else the top `KNOWN_CODEX` id.
428
+ * - coder is codex → `'sonnet'` (the recall-oriented Claude reviewer)
429
+ * - coder is Claude → the Codex-budget cell (`sol:high` or `terra:medium`),
430
+ * or `'opus'` when Codex is unavailable (never block)
431
+ * A pinned `CODEX_MODEL` still outranks the tier selected by the budget table.
293
432
  */
294
- export function resolveQeSpec(env: RoutingEnv): string {
295
- if (coderIsCodex(env)) return 'opus';
433
+ export function resolveQeSpecForCoder(coderCodex: boolean, env: RoutingEnv): string {
434
+ if (coderCodex) return 'sonnet';
296
435
  const CODEX_AVAILABLE = env.codexAvailable !== false;
297
436
  if (!CODEX_AVAILABLE) return 'opus';
298
- return 'codex:' + topCodexId(env) + ':high';
437
+ const budget = resolveBudgetMode(env.budget);
438
+ return budget.codex === 'eco'
439
+ ? 'codex:' + codexIdForTier('workhorse', env) + ':medium'
440
+ : 'codex:' + codexIdForTier('flagship', env) + ':high';
441
+ }
442
+
443
+ export function resolveQeSpec(env: RoutingEnv): string {
444
+ return resolveQeSpecForCoder(coderIsCodex(env), env);
299
445
  }
300
446
 
301
447
  /**
302
- * Whether the caller opted into routing at all. When FALSE (no `args.models`,
303
- * no codex knobs), every Claude stage resolves to `{}` → byte-identical to today.
448
+ * Whether the caller opted into routing at all. When FALSE (no axes, `args.models`,
449
+ * or Codex knobs), every Claude stage resolves to `{}` → byte-identical to today.
304
450
  */
305
451
  export function routingRequested(env: RoutingEnv): boolean {
306
452
  return (
307
453
  Object.keys(env.MODELS).length > 0 ||
454
+ env.primary !== undefined ||
455
+ env.budget !== undefined ||
308
456
  env.PLANNER === 'codex' ||
309
457
  env.CODER === 'codex' ||
310
458
  env.CODER === 'codex-fallback' ||
@@ -338,12 +486,12 @@ export function qeShouldUseCodex(env: RoutingEnv): boolean {
338
486
  * Resolve a stage to its `agent()` opts fragment.
339
487
  * 1. explicit `MODELS[stage]` wins
340
488
  * 2. else if the user did NOT opt into routing → `{}` (byte-identical BC path)
341
- * 3. else the DEFAULT TABLE fills the gap
489
+ * 3. else the primary × per-family-budget table fills the gap
342
490
  * 4. `code`/`qe` `null` sentinels resolve via the coder / cross-model rules
343
491
  */
344
492
  export function resolveStageModel(stage: string, env: RoutingEnv): StageOpts {
345
493
  if (env.usageOverride) {
346
- const r = (env.usageReasoning && env.usageReasoning[stage]) || OVERRIDE_REASONING[stage] || 'high';
494
+ const r = (env.usageReasoning && env.usageReasoning[stage]) || STAGE_EFFORT.override[stage] || 'medium';
347
495
  const o = specToOpts('codex:' + topCodexId(env) + ':' + r, env);
348
496
  o._usageSwitched = true;
349
497
  return o;
@@ -351,7 +499,18 @@ export function resolveStageModel(stage: string, env: RoutingEnv): StageOpts {
351
499
  let spec = env.MODELS[stage];
352
500
  if (spec === undefined) {
353
501
  if (!routingRequested(env)) return {};
354
- spec = DEFAULT_MODELS[stage];
502
+ if (stage === 'code' && (env.CODER === 'codex' || env.CODER === 'codex-fallback')) {
503
+ return specToOpts(resolveCoderSpec(env), env);
504
+ }
505
+ if (stage === 'plan' && env.PLANNER === 'codex') {
506
+ return specToOpts('codex:' + env.CODEX_MODEL + ':high', env);
507
+ }
508
+ if (stage === 'qe') {
509
+ return specToOpts(resolveQeSpec(env), env);
510
+ }
511
+ const resolvedPrimary = env.primary || 'claude';
512
+ const cell = budgetTable(resolvedPrimary, resolveBudgetMode(env.budget), env)[stage];
513
+ spec = cell !== undefined ? cell : DEFAULT_MODELS[stage];
355
514
  }
356
515
  if (stage === 'code' && (spec === null || spec === undefined)) return specToOpts(resolveCoderSpec(env), env);
357
516
  if (stage === 'qe' && (spec === null || spec === undefined)) return specToOpts(resolveQeSpec(env), env);
@@ -389,7 +548,16 @@ export function codexEffortHint(opts: StageOpts | null | undefined): string {
389
548
 
390
549
  // ── Step-7.5 CODE landed barrier (Codex out-of-band write flush) ────────────
391
550
 
392
- export const DEFAULT_CODE_LANDING_MAX_WAIT_MS = 120_000;
551
+ // Widened 2026-08-28 (MEASURED, not a guess): the dz-deadwood coder job ran 46m11s end to end, and
552
+ // the slop-lint coder was independently confirmed STILL RUNNING (codex-companion status "running",
553
+ // pid alive, mid-TDD-cycle, actively applying file changes) at 16m38s elapsed — a full 8x past the
554
+ // old 120s window. The old window declared "genuinely not landed" and fed Step-8 QE a false empty
555
+ // tree, producing a grade-D report entirely about an absence that was never real. This does not fix
556
+ // the underlying conflation of TIMEOUT with CONFIRMED-ABSENCE (filed as a backlog item — the barrier
557
+ // still cannot see the codex-companion job's own liveness) — it only narrows how often the false
558
+ // negative fires for a realistic M-tier build, at the cost of a longer worst-case wait when the
559
+ // coder truly produced nothing.
560
+ export const DEFAULT_CODE_LANDING_MAX_WAIT_MS = 1_020_000;
393
561
 
394
562
  export const DEFAULT_CODE_LANDING_BACKOFF_MS = [
395
563
  1_000,
@@ -403,6 +571,17 @@ export const DEFAULT_CODE_LANDING_BACKOFF_MS = [
403
571
  20_000,
404
572
  25_000,
405
573
  25_000,
574
+ 30_000,
575
+ 30_000,
576
+ 60_000,
577
+ 60_000,
578
+ 60_000,
579
+ 90_000,
580
+ 90_000,
581
+ 120_000,
582
+ 120_000,
583
+ 120_000,
584
+ 120_000,
406
585
  ] as const;
407
586
 
408
587
  export const CODE_LANDED_BARRIER_SLEEPS_SECONDS = DEFAULT_CODE_LANDING_BACKOFF_MS.map((ms) => ms / 1000);
@@ -1525,6 +1704,7 @@ function codexCd(repo: string): string {
1525
1704
  */
1526
1705
  export function codexExecCommand(input: {
1527
1706
  readonly modelId?: string | null;
1707
+ readonly reasoning?: string | null;
1528
1708
  readonly prompt?: string | null;
1529
1709
  readonly timeoutBin?: string | null;
1530
1710
  readonly timeoutSeconds?: number | null;
@@ -1533,12 +1713,15 @@ export function codexExecCommand(input: {
1533
1713
  const o = input || {};
1534
1714
  const modelId = String(o.modelId === undefined || o.modelId === null ? '' : o.modelId);
1535
1715
  if (!isSafeCodexId(modelId)) return null;
1716
+ const reasoning = o.reasoning === undefined || o.reasoning === null || o.reasoning === '' ? 'high' : String(o.reasoning);
1717
+ if (!VALID_REASONING[reasoning]) return null;
1536
1718
  const raw = Number(o.timeoutSeconds);
1537
1719
  const seconds = raw === raw && raw !== Infinity && raw > 0 ? Math.floor(raw) : CODEX_EXEC_TIMEOUT_SECONDS;
1538
1720
  const repo = String(o.repo === undefined || o.repo === null ? '' : o.repo);
1539
1721
  const cd = repo === '' ? '' : ' -C ' + codexSq(repo);
1540
1722
  return timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + ' codex exec' + cd
1541
- + ' -m ' + codexSq(modelId) + ' ' + codexSq(String(o.prompt === undefined || o.prompt === null ? '' : o.prompt)) + ' < /dev/null';
1723
+ + ' -m ' + codexSq(modelId) + ' -c model_reasoning_effort=' + codexSq(reasoning)
1724
+ + ' ' + codexSq(String(o.prompt === undefined || o.prompt === null ? '' : o.prompt)) + ' < /dev/null';
1542
1725
  }
1543
1726
 
1544
1727
  export function codexReviewCommand(input: CodexReviewCommandInput): CodexReviewCommandResult {