@dzhechkov/harness-core 0.8.5 → 0.8.10
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/.dz-manifest.json +286 -82
- package/README.md +54 -2
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +11 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +38 -3
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +58 -23
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +194 -69
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +34 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +113 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +18 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/loop-blobs.generated.js +8 -8
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan.d.ts +13 -1
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +15 -1
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +51 -6
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-trace.d.ts +20 -1
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +83 -1
- package/dist/loop-trace.js.map +1 -1
- package/dist/model-recommender.d.ts +8 -0
- package/dist/model-recommender.d.ts.map +1 -1
- package/dist/model-recommender.js +31 -4
- package/dist/model-recommender.js.map +1 -1
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +178 -2
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +18 -12
- package/dist/workflow-run.js.map +1 -1
- package/package.json +19 -15
- package/sbom.json +591 -81
- package/src/amendment-trace.ts +12 -1
- package/src/compounding.ts +300 -1
- package/src/eta.ts +590 -0
- package/src/feature-adr-checkpoints.ts +39 -3
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +224 -65
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +148 -4
- package/src/index.ts +65 -3
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/loop-blobs.generated.ts +8 -8
- package/src/loop-plan.ts +36 -3
- package/src/loop-render.ts +50 -6
- package/src/loop-trace.ts +91 -2
- package/src/model-recommender.ts +35 -4
- package/src/operations.ts +199 -2
- package/src/patterns.ts +252 -43
- package/src/setup.ts +20 -17
- package/src/target-integrations.ts +225 -0
- package/src/vector-tier.ts +44 -14
- package/src/workflow-run.ts +18 -11
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Pure decisions for the Step-7.5 Codex companion liveness barrier. */
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CODE_LANDING_CEILING_MS = 7_200_000;
|
|
4
|
+
export const CODE_LANDING_CEILING_ENV = 'DZ_FEATURE_ADR_CODE_LANDING_CEILING_MS';
|
|
5
|
+
|
|
6
|
+
export type CodeLandingLivenessVerdict =
|
|
7
|
+
| 'coder-running'
|
|
8
|
+
| 'landed'
|
|
9
|
+
| 'genuinely-not-landed'
|
|
10
|
+
| 'exited-without-edits'
|
|
11
|
+
| 'dead-worker'
|
|
12
|
+
| 'inconclusive';
|
|
13
|
+
|
|
14
|
+
export type CodeLandingLivenessReason =
|
|
15
|
+
| 'recorded-pid-alive'
|
|
16
|
+
| 'recorded-pid-absent'
|
|
17
|
+
| 'terminal-companion'
|
|
18
|
+
| 'ceiling-exceeded'
|
|
19
|
+
| 'companion-probe-error'
|
|
20
|
+
| 'unparseable-companion-status'
|
|
21
|
+
| 'recorded-pid-unavailable'
|
|
22
|
+
| 'git-evidence-unavailable'
|
|
23
|
+
| 'reported-zero-touched-files';
|
|
24
|
+
|
|
25
|
+
export interface CodeLandingLivenessInput {
|
|
26
|
+
readonly companionStatus: unknown;
|
|
27
|
+
readonly recordedPidAlive: boolean | null;
|
|
28
|
+
readonly targetsChanged: boolean | null;
|
|
29
|
+
readonly elapsedMs: number;
|
|
30
|
+
readonly ceilingMs: number;
|
|
31
|
+
/**
|
|
32
|
+
* How many files the companion job itself REPORTED writing (`result.touchedFiles.length`), or
|
|
33
|
+
* null when the record could not be read. Zero from a cleanly-terminated job is a different
|
|
34
|
+
* event from "the window expired": the coder finished and wrote nothing, which is what happens
|
|
35
|
+
* when it asks a question a non-interactive dispatch cannot answer. Folding the two together
|
|
36
|
+
* hides the only cure that works — answer the gate and re-dispatch.
|
|
37
|
+
*/
|
|
38
|
+
readonly reportedTouchedFiles?: number | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CodeLandingLivenessDecision {
|
|
42
|
+
readonly verdict: CodeLandingLivenessVerdict;
|
|
43
|
+
readonly reason: CodeLandingLivenessReason;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function decideCodeLandingLiveness(input: CodeLandingLivenessInput): CodeLandingLivenessDecision {
|
|
47
|
+
const status = typeof input.companionStatus === 'string' ? input.companionStatus.trim().toLowerCase() : ''
|
|
48
|
+
const elapsedMs = Number.isFinite(input.elapsedMs) ? Math.max(0, input.elapsedMs) : 0
|
|
49
|
+
const ceilingMs = Number.isFinite(input.ceilingMs) && input.ceilingMs > 0 ? input.ceilingMs : DEFAULT_CODE_LANDING_CEILING_MS
|
|
50
|
+
const live = status === 'running' || status === 'queued'
|
|
51
|
+
const terminal = status === 'completed' || status === 'failed' || status === 'cancelled'
|
|
52
|
+
|
|
53
|
+
if (live && input.recordedPidAlive === false) {
|
|
54
|
+
return { verdict: 'dead-worker', reason: 'recorded-pid-absent' }
|
|
55
|
+
}
|
|
56
|
+
if (live && input.recordedPidAlive === true) {
|
|
57
|
+
if (elapsedMs >= ceilingMs) return { verdict: 'inconclusive', reason: 'ceiling-exceeded' }
|
|
58
|
+
return { verdict: 'coder-running', reason: 'recorded-pid-alive' }
|
|
59
|
+
}
|
|
60
|
+
if (live) return { verdict: 'inconclusive', reason: 'recorded-pid-unavailable' }
|
|
61
|
+
if (terminal) {
|
|
62
|
+
if (input.targetsChanged === true) return { verdict: 'landed', reason: 'terminal-companion' }
|
|
63
|
+
if (input.targetsChanged === false) {
|
|
64
|
+
if (input.reportedTouchedFiles === 0) {
|
|
65
|
+
return { verdict: 'exited-without-edits', reason: 'reported-zero-touched-files' }
|
|
66
|
+
}
|
|
67
|
+
return { verdict: 'genuinely-not-landed', reason: 'terminal-companion' }
|
|
68
|
+
}
|
|
69
|
+
return { verdict: 'inconclusive', reason: 'git-evidence-unavailable' }
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
verdict: 'inconclusive',
|
|
73
|
+
reason: status === '' ? 'companion-probe-error' : 'unparseable-companion-status',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function extractCodexCompanionJobId(text: unknown): string | null {
|
|
78
|
+
const match = /\bstarted in the background as (task-[a-z0-9]+(?:-[a-z0-9]+)*)\b/i.exec(String(text ?? ''));
|
|
79
|
+
return match && match[1] ? match[1] : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CodeLandingLivenessProbe {
|
|
83
|
+
readonly companionStatus: string;
|
|
84
|
+
readonly recordedPidAlive: boolean | null;
|
|
85
|
+
readonly targetsChanged: boolean | null;
|
|
86
|
+
readonly elapsedMs: number;
|
|
87
|
+
readonly ceilingMs: number;
|
|
88
|
+
readonly startMs: number;
|
|
89
|
+
readonly reportedTouchedFiles: number | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseCodeLandingLivenessSignal(text: unknown): CodeLandingLivenessProbe | null {
|
|
93
|
+
const source = String(text ?? '');
|
|
94
|
+
// touched-files is OPTIONAL in the grammar: an older probe, or a state record we could not read,
|
|
95
|
+
// simply omits it and the field stays null — which keeps the verdict at genuinely-not-landed
|
|
96
|
+
// rather than inventing a clean exit. Absence is never evidence here.
|
|
97
|
+
const match = /^CODEX-LIVENESS-SIGNAL companion=([a-z-]+) pid-alive=(true|false|unknown) targets-changed=(true|false|unknown) elapsed-ms=(\d+) ceiling-ms=(\d+) start-ms=(\d+)(?: touched-files=(\d+|unknown))?[ \t]*$/m.exec(source);
|
|
98
|
+
if (!match) return null;
|
|
99
|
+
const asTriState = (value: string): boolean | null => value === 'true' ? true : value === 'false' ? false : null;
|
|
100
|
+
return {
|
|
101
|
+
companionStatus: String(match[1]),
|
|
102
|
+
recordedPidAlive: asTriState(String(match[2])),
|
|
103
|
+
targetsChanged: asTriState(String(match[3])),
|
|
104
|
+
elapsedMs: Number(match[4]),
|
|
105
|
+
ceilingMs: Number(match[5]),
|
|
106
|
+
startMs: Number(match[6]),
|
|
107
|
+
reportedTouchedFiles: match[7] === undefined || match[7] === 'unknown' ? null : Number(match[7]),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -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 `
|
|
18
|
-
*
|
|
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
|
|
78
|
-
* `args.usageReasoning` — stage →
|
|
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
|
-
*
|
|
113
|
-
*
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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: '
|
|
128
|
-
};
|
|
131
|
+
fleet: 'medium',
|
|
132
|
+
} };
|
|
129
133
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
197
|
-
|
|
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
|
-
|
|
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'
|
|
284
|
-
return
|
|
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 → `'
|
|
291
|
-
* - coder is Claude →
|
|
292
|
-
*
|
|
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
|
|
295
|
-
if (
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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]) ||
|
|
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
|
-
|
|
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,15 +548,11 @@ 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
|
-
// Widened 2026-08-28 (MEASURED
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
// the underlying conflation of TIMEOUT with CONFIRMED-ABSENCE (filed as a backlog item — the barrier
|
|
398
|
-
// still cannot see the codex-companion job's own liveness) — it only narrows how often the false
|
|
399
|
-
// negative fires for a realistic M-tier build, at the cost of a longer worst-case wait when the
|
|
400
|
-
// coder truly produced nothing.
|
|
551
|
+
// Widened 2026-08-28 (MEASURED): the dz-deadwood coder ran 46m11s, and slop-lint was still running
|
|
552
|
+
// with its recorded PID alive at 16m38s when the old 120s window had already declared absence.
|
|
553
|
+
// Changed 2026-08-30 (MEASURED: six false verdicts; plus task-mtfhglwk stayed status=running for
|
|
554
|
+
// 8h51m after recorded PID 3639268 disappeared): this is now one git-evidence/backoff window inside
|
|
555
|
+
// the liveness-driven loop, not the whole barrier. The separate hard ceiling bounds a live worker.
|
|
401
556
|
export const DEFAULT_CODE_LANDING_MAX_WAIT_MS = 1_020_000;
|
|
402
557
|
|
|
403
558
|
export const DEFAULT_CODE_LANDING_BACKOFF_MS = [
|
|
@@ -445,11 +600,11 @@ export type CodeStageLandingStatus = 'landed' | 'genuinely-not-landed' | 'inconc
|
|
|
445
600
|
|
|
446
601
|
/** Bumped whenever the landing protocol's meaning changes. Recorded on every code-stage result so a
|
|
447
602
|
* pre-protocol checkpoint reads as no-checkpoint instead of resuming into new semantics. */
|
|
448
|
-
export const LANDING_PROTOCOL_VERSION =
|
|
603
|
+
export const LANDING_PROTOCOL_VERSION = 3;
|
|
449
604
|
|
|
450
605
|
/** R6: salted into the code stage's checkpoint hash PARTS (not the schema version — ADR-003 keeps
|
|
451
606
|
* `CKPT_SCHEMA_VERSION` at 'fa-ckpt-2' deliberately, so ONLY the code stage re-runs). */
|
|
452
|
-
export const LANDING_HASH_TOKEN = 'landing-
|
|
607
|
+
export const LANDING_HASH_TOKEN = 'landing-v3';
|
|
453
608
|
|
|
454
609
|
/** Why a landing verdict is INCONCLUSIVE. Typed, because "we do not know" needs to say WHICH
|
|
455
610
|
* unknown — the operator repair differs per reason and a single untyped bucket reads as noise. */
|
|
@@ -1545,6 +1700,7 @@ function codexCd(repo: string): string {
|
|
|
1545
1700
|
*/
|
|
1546
1701
|
export function codexExecCommand(input: {
|
|
1547
1702
|
readonly modelId?: string | null;
|
|
1703
|
+
readonly reasoning?: string | null;
|
|
1548
1704
|
readonly prompt?: string | null;
|
|
1549
1705
|
readonly timeoutBin?: string | null;
|
|
1550
1706
|
readonly timeoutSeconds?: number | null;
|
|
@@ -1553,12 +1709,15 @@ export function codexExecCommand(input: {
|
|
|
1553
1709
|
const o = input || {};
|
|
1554
1710
|
const modelId = String(o.modelId === undefined || o.modelId === null ? '' : o.modelId);
|
|
1555
1711
|
if (!isSafeCodexId(modelId)) return null;
|
|
1712
|
+
const reasoning = o.reasoning === undefined || o.reasoning === null || o.reasoning === '' ? 'high' : String(o.reasoning);
|
|
1713
|
+
if (!VALID_REASONING[reasoning]) return null;
|
|
1556
1714
|
const raw = Number(o.timeoutSeconds);
|
|
1557
1715
|
const seconds = raw === raw && raw !== Infinity && raw > 0 ? Math.floor(raw) : CODEX_EXEC_TIMEOUT_SECONDS;
|
|
1558
1716
|
const repo = String(o.repo === undefined || o.repo === null ? '' : o.repo);
|
|
1559
1717
|
const cd = repo === '' ? '' : ' -C ' + codexSq(repo);
|
|
1560
1718
|
return timeoutBinOrDefault(o.timeoutBin) + ' ' + seconds + ' codex exec' + cd
|
|
1561
|
-
+ ' -m ' + codexSq(modelId) + ' ' + codexSq(
|
|
1719
|
+
+ ' -m ' + codexSq(modelId) + ' -c model_reasoning_effort=' + codexSq(reasoning)
|
|
1720
|
+
+ ' ' + codexSq(String(o.prompt === undefined || o.prompt === null ? '' : o.prompt)) + ' < /dev/null';
|
|
1562
1721
|
}
|
|
1563
1722
|
|
|
1564
1723
|
export function codexReviewCommand(input: CodexReviewCommandInput): CodexReviewCommandResult {
|