faberun 0.3.0
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/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/faberun.mjs +25 -0
- package/integrations/claude-code/statusline-bench.sh +42 -0
- package/integrations/claude-code/statusline.sh +80 -0
- package/package.json +33 -0
- package/skills/faberun/SKILL.md +24 -0
- package/skills/faberun/references/contract.md +380 -0
- package/skills/faberun/references/engineering.md +29 -0
- package/skills/faberun/references/handoffs.md +26 -0
- package/skills/faberun/references/operations.md +184 -0
- package/skills/faberun/references/rules.md +35 -0
- package/skills/faberun/references/workflow.md +23 -0
- package/skills/init-agentkit/SKILL.md +108 -0
- package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
- package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
- package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
- package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
- package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
- package/skills/init-agentkit/templates/AGENTS.md +110 -0
- package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
- package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
- package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
- package/skills/init-agentkit/templates/docs/VISION.md +33 -0
- package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
- package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
- package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
- package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
- package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
- package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
- package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
- package/src/campaign/brief.mjs +394 -0
- package/src/campaign/chain.mjs +555 -0
- package/src/campaign/handoff.mjs +516 -0
- package/src/campaign/index.mjs +300 -0
- package/src/campaign/journal.mjs +347 -0
- package/src/campaign/layout.mjs +51 -0
- package/src/campaign/metrics-evals.mjs +25 -0
- package/src/campaign/metrics.mjs +517 -0
- package/src/campaign/projection.mjs +250 -0
- package/src/campaign/record.mjs +102 -0
- package/src/campaign/unpark.mjs +56 -0
- package/src/cli/brand.mjs +205 -0
- package/src/cli/campaign.mjs +730 -0
- package/src/cli/contract.mjs +67 -0
- package/src/cli/init.mjs +170 -0
- package/src/cli/launch.mjs +239 -0
- package/src/cli/seat.mjs +139 -0
- package/src/cli/setup.mjs +294 -0
- package/src/cli/skills.mjs +105 -0
- package/src/cli/update.mjs +216 -0
- package/src/cli.mjs +525 -0
- package/src/contract/articles.mjs +12 -0
- package/src/contract/assert.mjs +162 -0
- package/src/contract/definition-of-done.mjs +97 -0
- package/src/contract/final-verification.mjs +96 -0
- package/src/contract/index.mjs +641 -0
- package/src/contract/judge-envelope.mjs +25 -0
- package/src/contract/review-modes.mjs +151 -0
- package/src/contract/runtime.mjs +204 -0
- package/src/contract/schema-version.mjs +25 -0
- package/src/contract/scope-findings.mjs +77 -0
- package/src/contract/snapshot.mjs +639 -0
- package/src/contract/task-packet.mjs +495 -0
- package/src/contract/untrusted.mjs +75 -0
- package/src/contract/verification.mjs +185 -0
- package/src/contract/worker-result.mjs +138 -0
- package/src/engine/assignment.mjs +63 -0
- package/src/engine/backoff.mjs +492 -0
- package/src/engine/bulk-read.mjs +361 -0
- package/src/engine/cancel.mjs +177 -0
- package/src/engine/detach.mjs +101 -0
- package/src/engine/dispatch.mjs +752 -0
- package/src/engine/failover.mjs +192 -0
- package/src/engine/gate.mjs +183 -0
- package/src/engine/judge-gate.mjs +517 -0
- package/src/engine/lifecycle.mjs +772 -0
- package/src/engine/live-preflight.mjs +299 -0
- package/src/engine/mutation.mjs +146 -0
- package/src/engine/notify-queue.mjs +327 -0
- package/src/engine/process-identity.mjs +72 -0
- package/src/engine/process.mjs +774 -0
- package/src/engine/prompts.mjs +289 -0
- package/src/engine/recover.mjs +300 -0
- package/src/engine/result-file.mjs +222 -0
- package/src/engine/resume.mjs +635 -0
- package/src/engine/retry.mjs +334 -0
- package/src/engine/review.mjs +228 -0
- package/src/engine/run-command.mjs +287 -0
- package/src/engine/run-identity.mjs +411 -0
- package/src/engine/runtime-discovery.mjs +235 -0
- package/src/engine/scheduler.mjs +526 -0
- package/src/engine/scope.mjs +378 -0
- package/src/engine/settle.mjs +207 -0
- package/src/engine/state.mjs +148 -0
- package/src/engine/supervise.mjs +713 -0
- package/src/engine/verify.mjs +167 -0
- package/src/harnesses/agy/index.mjs +62 -0
- package/src/harnesses/catalogue.mjs +509 -0
- package/src/harnesses/claude/index.mjs +90 -0
- package/src/harnesses/codex/index.mjs +87 -0
- package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
- package/src/harnesses/dsh/index.mjs +210 -0
- package/src/harnesses/dsh/runner.mjs +259 -0
- package/src/harnesses/exec-jsonl/index.mjs +788 -0
- package/src/harnesses/index.mjs +508 -0
- package/src/harnesses/protocol.mjs +531 -0
- package/src/harnesses/replay/bin.mjs +386 -0
- package/src/harnesses/replay/index.mjs +238 -0
- package/src/harnesses/zcode/index.mjs +276 -0
- package/src/host/config.mjs +87 -0
- package/src/host/home.mjs +149 -0
- package/src/host/package.mjs +23 -0
- package/src/host/preflight.mjs +520 -0
- package/src/host/tool-policy-decisions.mjs +341 -0
- package/src/host/tool-policy-hook.mjs +270 -0
- package/src/notify/index.mjs +359 -0
- package/src/notify/os-macos.mjs +81 -0
- package/src/repo/declared-paths.mjs +220 -0
- package/src/repo/integrate.mjs +546 -0
- package/src/repo/scope-closure.mjs +665 -0
- package/src/repo/signal-block.mjs +16 -0
- package/src/repo/signal.mjs +222 -0
- package/src/repo/source-identity.mjs +295 -0
- package/src/repo/workspace.mjs +557 -0
- package/src/repo/worktree.mjs +352 -0
- package/src/report/final.mjs +200 -0
- package/src/report/metrics-report.mjs +99 -0
- package/src/report/next.mjs +383 -0
- package/src/report/render.mjs +716 -0
- package/src/run/disk-gc.mjs +251 -0
- package/src/run/lock.mjs +329 -0
- package/src/run/node-store.mjs +62 -0
- package/src/run/operations.mjs +286 -0
- package/src/run/store.mjs +187 -0
- package/src/run/usage.mjs +337 -0
- package/src/seat/harnesses.mjs +83 -0
- package/src/seat/index.mjs +239 -0
- package/src/seat/tmux.mjs +208 -0
- package/src/util.mjs +0 -0
- package/src/web/api.mjs +371 -0
- package/src/web/boundary.mjs +88 -0
- package/src/web/index.html +299 -0
- package/src/web/server.mjs +552 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { getHarness, probeRuntime, registeredHarnesses, resolveVendor } from "./index.mjs";
|
|
3
|
+
import { DISCOVERY_RUNTIME_DEFINITIONS, composeAssignments } from "../engine/runtime-discovery.mjs";
|
|
4
|
+
import { errorMessage } from "../util.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Model catalogue report: which models each registered harness can run, the
|
|
8
|
+
* effort levels each accepts, and the worker/judge allocation the discovery
|
|
9
|
+
* law already implies.
|
|
10
|
+
*
|
|
11
|
+
* Only `agy` exposes a catalogue of its own — `agy models`, measured
|
|
12
|
+
* 2026-09-11: one `id<TAB>display name` line per model on stdout, a progress
|
|
13
|
+
* line on stderr, exit 0, no flags — so the report runs it when that binary is
|
|
14
|
+
* present and falls back to the declared entries when it is not. Every other
|
|
15
|
+
* harness's catalogue is declared below. No catalogue here is fetched from a
|
|
16
|
+
* network service of our own: `agy models` is the provider CLI's own surface,
|
|
17
|
+
* and no invocation spends tokens.
|
|
18
|
+
*
|
|
19
|
+
* Determinism: harness order is the canonical constant below, never object or
|
|
20
|
+
* Map iteration order (the registry decides membership only); declared model
|
|
21
|
+
* arrays are canonical; `agy models` output is sorted by id before rendering.
|
|
22
|
+
* No clock and no locale reaches the default report; `--probe` is the one
|
|
23
|
+
* opt-in that reads the host.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Display order of the registered harnesses: claude, codex, agy, dsh, zcode, exec-jsonl, replay. */
|
|
27
|
+
export const MODEL_HARNESS_ORDER = Object.freeze([
|
|
28
|
+
"claude",
|
|
29
|
+
"codex",
|
|
30
|
+
"agy",
|
|
31
|
+
"dsh",
|
|
32
|
+
"zcode",
|
|
33
|
+
"exec-jsonl",
|
|
34
|
+
"replay",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const CLAUDE_EFFORTS = Object.freeze(["low", "medium", "high", "max"]);
|
|
38
|
+
const CODEX_EFFORTS = Object.freeze(["low", "medium", "high", "xhigh"]);
|
|
39
|
+
const AGY_EFFORTS = Object.freeze(["low", "medium", "high"]);
|
|
40
|
+
/** agy.mjs collapses both of these onto `high` before `--effort` is built. */
|
|
41
|
+
const AGY_EFFORT_ALIASES = Object.freeze({ max: "high", xhigh: "high" });
|
|
42
|
+
const DSH_EFFORTS = Object.freeze(["off", "low", "high", "max"]);
|
|
43
|
+
/** @type {readonly string[]} */
|
|
44
|
+
const NO_EFFORTS = Object.freeze([]);
|
|
45
|
+
|
|
46
|
+
/** Measured: the harness accepts off/low/high/max and defaults to high; 1M tokens is its default window. */
|
|
47
|
+
const DSH_CONTEXT_WINDOW_TOKENS = 1_000_000;
|
|
48
|
+
/** The two GLM tiers Z.ai serves: the flash model's 200,000-token window and the 5.3 model's 1,048,576. */
|
|
49
|
+
const GLM_CONTEXT_WINDOW_TOKENS = 200_000;
|
|
50
|
+
const GLM_ONE_MILLION_CONTEXT_WINDOW_TOKENS = 1_048_576;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {{id: string, contextWindowTokens: number|null, efforts: readonly string[], defaultEffort: string|null, effortInModelId: string|null}} DeclaredModel
|
|
54
|
+
* @typedef {{harness: string, vendor: string|null}} ModelPath
|
|
55
|
+
* @typedef {{id: string, contextWindowTokens: number|null, efforts: string[], defaultEffort: string|null, effortInModelId: string|null, declaredBy: ModelPath[]}} ModelView
|
|
56
|
+
* @typedef {{ok: boolean, available: boolean, reason: string, version: string|null}} ProbeView
|
|
57
|
+
* @typedef {{harness: string, executable: string, vendor: string|null, vendorNote: string|null, catalogue: string, effortTransport: string|null, effortAliases: Record<string, string>, effortNotes: string[], models: ModelView[], probe?: ProbeView}} HarnessView
|
|
58
|
+
* @typedef {{id: string, harness: string, model: string, vendor: string, tier: number|string|null, costRank: number|null}} AllocationRuntime
|
|
59
|
+
* @typedef {{worker: AllocationRuntime, judge: AllocationRuntime, vendorException: string|null, reason: string}} AllocationSuggestion
|
|
60
|
+
* @typedef {{schemaVersion: number, availability: string, harnesses: HarnessView[], suggestion: AllocationSuggestion}} ModelsReport
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Repository-declared catalogues. `exec-jsonl` and `replay` stand in for an
|
|
65
|
+
* arbitrary executable and a recording, so both name no models at all: the
|
|
66
|
+
* runtime declaration supplies the model id.
|
|
67
|
+
*
|
|
68
|
+
* @type {Readonly<Record<string, readonly DeclaredModel[]>>}
|
|
69
|
+
*/
|
|
70
|
+
export const DECLARED_MODEL_CATALOGUES = Object.freeze({
|
|
71
|
+
claude: Object.freeze([
|
|
72
|
+
declaredModel("claude-sonnet-5", { efforts: CLAUDE_EFFORTS }),
|
|
73
|
+
declaredModel("claude-opus-5", { efforts: CLAUDE_EFFORTS }),
|
|
74
|
+
declaredModel("claude-sonnet-4-6", { efforts: CLAUDE_EFFORTS }),
|
|
75
|
+
]),
|
|
76
|
+
codex: Object.freeze([
|
|
77
|
+
declaredModel("gpt-5.6", { efforts: CODEX_EFFORTS }),
|
|
78
|
+
declaredModel("gpt-5.6-luna", { efforts: CODEX_EFFORTS }),
|
|
79
|
+
declaredModel("gpt-5.6-sol", { efforts: CODEX_EFFORTS }),
|
|
80
|
+
]),
|
|
81
|
+
agy: Object.freeze([
|
|
82
|
+
agyModel("gemini-3.8-flash-high"),
|
|
83
|
+
agyModel("gemini-3.8-flash-medium"),
|
|
84
|
+
agyModel("gemini-3.8-flash-low"),
|
|
85
|
+
agyModel("claude-sonnet-4-6"),
|
|
86
|
+
agyModel("claude-opus-4-6-thinking"),
|
|
87
|
+
agyModel("gpt-oss-120b-medium"),
|
|
88
|
+
]),
|
|
89
|
+
dsh: Object.freeze([
|
|
90
|
+
declaredModel("deepseek-flash", { contextWindowTokens: DSH_CONTEXT_WINDOW_TOKENS, efforts: DSH_EFFORTS, defaultEffort: "high" }),
|
|
91
|
+
declaredModel("deepseek-v4-flash", { contextWindowTokens: DSH_CONTEXT_WINDOW_TOKENS, efforts: DSH_EFFORTS, defaultEffort: "high" }),
|
|
92
|
+
declaredModel("deepseek-v4-pro", { contextWindowTokens: DSH_CONTEXT_WINDOW_TOKENS, efforts: DSH_EFFORTS, defaultEffort: "high" }),
|
|
93
|
+
declaredModel("deepseek-v4-flash-vision-exp", { contextWindowTokens: DSH_CONTEXT_WINDOW_TOKENS, efforts: DSH_EFFORTS, defaultEffort: "high" }),
|
|
94
|
+
]),
|
|
95
|
+
zcode: Object.freeze([
|
|
96
|
+
declaredModel("glm-5.3-flash", { contextWindowTokens: GLM_CONTEXT_WINDOW_TOKENS, efforts: NO_EFFORTS }),
|
|
97
|
+
// No `[1m]` row: the suffix is a Claude Code convention the ZCode CLI does
|
|
98
|
+
// not know (zcode.mjs strips it before building `ZCODE_MODEL`), so a second
|
|
99
|
+
// row would name the very same invocation. The CLI resolves the window from
|
|
100
|
+
// the provider, which projected 1M for this model.
|
|
101
|
+
declaredModel("glm-5.3", { contextWindowTokens: GLM_ONE_MILLION_CONTEXT_WINDOW_TOKENS, efforts: NO_EFFORTS }),
|
|
102
|
+
]),
|
|
103
|
+
"exec-jsonl": Object.freeze([]),
|
|
104
|
+
replay: Object.freeze([]),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** @type {Readonly<Record<string, string>>} */
|
|
108
|
+
const CATALOGUE_SOURCES = Object.freeze({
|
|
109
|
+
claude: "declared",
|
|
110
|
+
codex: "declared",
|
|
111
|
+
dsh: "declared",
|
|
112
|
+
zcode: "declared",
|
|
113
|
+
"exec-jsonl": "runtime-declared",
|
|
114
|
+
replay: "runtime-declared",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The field each adapter turns `runtime.reasoning` into.
|
|
119
|
+
*
|
|
120
|
+
* @type {Readonly<Record<string, string|null>>}
|
|
121
|
+
*/
|
|
122
|
+
const EFFORT_TRANSPORT = Object.freeze({
|
|
123
|
+
claude: "--effort",
|
|
124
|
+
codex: "config.model_reasoning_effort",
|
|
125
|
+
agy: "--effort",
|
|
126
|
+
dsh: "--reasoning (harness reasoningEffort)",
|
|
127
|
+
zcode: null,
|
|
128
|
+
"exec-jsonl": null,
|
|
129
|
+
replay: null,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Why a harness resolves no vendor: dsh, exec-jsonl, and replay declare none.
|
|
134
|
+
*
|
|
135
|
+
* @type {Readonly<Record<string, string>>}
|
|
136
|
+
*/
|
|
137
|
+
const VENDOR_NOTES = Object.freeze({
|
|
138
|
+
dsh: "unresolved: dsh declares no default vendor; the contract names it",
|
|
139
|
+
"exec-jsonl": "unresolved: exec-jsonl declares no default vendor; the contract names it",
|
|
140
|
+
replay: "unresolved: replay declares no default vendor; the contract names it",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const AGY_CATALOGUE_TIMEOUT_MS = 10_000;
|
|
144
|
+
|
|
145
|
+
const AVAILABILITY_NOTES = Object.freeze({
|
|
146
|
+
declared: "declared catalogue only; live availability is `doctor`'s report (`--probe` adds per-runtime reachability)",
|
|
147
|
+
probe: "probed per runtime (executable reachability only); `doctor` remains authoritative",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const ALLOCATION_LAW = "cheapest declared runtime by tier then costRank executes; the strongest runtime with a different resolved vendor judges";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} id
|
|
154
|
+
* @param {{contextWindowTokens?: number, efforts: readonly string[], defaultEffort?: string, effortInModelId?: string}} options
|
|
155
|
+
* @returns {DeclaredModel}
|
|
156
|
+
*/
|
|
157
|
+
function declaredModel(id, options) {
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
id,
|
|
160
|
+
contextWindowTokens: options.contextWindowTokens ?? null,
|
|
161
|
+
efforts: options.efforts,
|
|
162
|
+
defaultEffort: options.defaultEffort ?? null,
|
|
163
|
+
effortInModelId: options.effortInModelId ?? null,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* An agy model: the adapter's `--effort` vocabulary applies to every id, and
|
|
169
|
+
* an id ending in -low/-medium/-high fixes that level itself, which is the
|
|
170
|
+
* double specification the report has to show.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} id
|
|
173
|
+
* @param {number|null} [contextWindowTokens]
|
|
174
|
+
* @returns {DeclaredModel}
|
|
175
|
+
*/
|
|
176
|
+
function agyModel(id, contextWindowTokens = null) {
|
|
177
|
+
const encoded = /-(low|medium|high)$/u.exec(id)?.[1] ?? null;
|
|
178
|
+
return Object.freeze({
|
|
179
|
+
id,
|
|
180
|
+
contextWindowTokens,
|
|
181
|
+
efforts: AGY_EFFORTS,
|
|
182
|
+
defaultEffort: encoded,
|
|
183
|
+
effortInModelId: encoded,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {{probe?: boolean, cwd?: string}} [options]
|
|
189
|
+
* @returns {Promise<ModelsReport>}
|
|
190
|
+
*/
|
|
191
|
+
export async function modelsReport(options = {}) {
|
|
192
|
+
const cliCatalogue = agyCliCatalogue(options.cwd);
|
|
193
|
+
const declaredPaths = declaredPathIndex();
|
|
194
|
+
/** @type {HarnessView[]} */
|
|
195
|
+
const harnesses = [];
|
|
196
|
+
for (const harness of displayOrder()) {
|
|
197
|
+
const declared = DECLARED_MODEL_CATALOGUES[harness] ?? [];
|
|
198
|
+
const entries = harness === "agy" ? cliCatalogue ?? declared : declared;
|
|
199
|
+
const template = { harness, model: entries[0]?.id ?? "runtime-defined" };
|
|
200
|
+
/** @type {HarnessView} */
|
|
201
|
+
const view = {
|
|
202
|
+
harness,
|
|
203
|
+
executable: getHarness(harness).executable(template),
|
|
204
|
+
vendor: resolveVendor(template),
|
|
205
|
+
vendorNote: VENDOR_NOTES[harness] ?? null,
|
|
206
|
+
catalogue: harness === "agy" ? (cliCatalogue ? "agy-cli" : "declared") : CATALOGUE_SOURCES[harness] ?? "runtime-declared",
|
|
207
|
+
effortTransport: EFFORT_TRANSPORT[harness] ?? null,
|
|
208
|
+
effortAliases: harness === "agy" ? { ...AGY_EFFORT_ALIASES } : {},
|
|
209
|
+
effortNotes: harness === "agy"
|
|
210
|
+
? ["model ids ending in -low/-medium/-high fix the level themselves, and the adapter still passes --effort on top (double specification)"]
|
|
211
|
+
: [],
|
|
212
|
+
models: entries.map((entry) => modelView(harness, entry, declaredPaths)),
|
|
213
|
+
};
|
|
214
|
+
if (options.probe === true) view.probe = probeView(await probeRuntime(template, { cwd: options.cwd }));
|
|
215
|
+
harnesses.push(view);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
availability: AVAILABILITY_NOTES[options.probe === true ? "probe" : "declared"],
|
|
220
|
+
harnesses,
|
|
221
|
+
suggestion: suggestedAllocation(),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {{json?: boolean, probe?: boolean, cwd?: string}} [options]
|
|
227
|
+
* @returns {Promise<void>}
|
|
228
|
+
*/
|
|
229
|
+
export async function modelsCommand(options = {}) {
|
|
230
|
+
const report = await modelsReport(options);
|
|
231
|
+
process.stdout.write(options.json === true ? stableJsonDocument(report) : renderModelsReport(report));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {string} harness
|
|
236
|
+
* @param {DeclaredModel} entry
|
|
237
|
+
* @param {Map<string, ModelPath[]>} declaredPaths
|
|
238
|
+
* @returns {ModelView}
|
|
239
|
+
*/
|
|
240
|
+
function modelView(harness, entry, declaredPaths) {
|
|
241
|
+
return {
|
|
242
|
+
id: entry.id,
|
|
243
|
+
contextWindowTokens: entry.contextWindowTokens,
|
|
244
|
+
efforts: [...entry.efforts],
|
|
245
|
+
defaultEffort: entry.defaultEffort,
|
|
246
|
+
effortInModelId: entry.effortInModelId,
|
|
247
|
+
declaredBy: (declaredPaths.get(entry.id) ?? []).filter((path) => path.harness !== harness),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Every declared model id, with the harnesses that declare it and the vendor
|
|
253
|
+
* each of those paths resolves to. Two paths can serve one model under
|
|
254
|
+
* different resolved vendors (agy resells claude-sonnet-4-6, which the claude
|
|
255
|
+
* harness also declares), and the allocation law compares those vendors.
|
|
256
|
+
*
|
|
257
|
+
* @returns {Map<string, ModelPath[]>}
|
|
258
|
+
*/
|
|
259
|
+
function declaredPathIndex() {
|
|
260
|
+
/** @type {Map<string, ModelPath[]>} */
|
|
261
|
+
const paths = new Map();
|
|
262
|
+
for (const harness of displayOrder()) {
|
|
263
|
+
const vendor = resolveVendor({ harness });
|
|
264
|
+
for (const entry of DECLARED_MODEL_CATALOGUES[harness] ?? []) {
|
|
265
|
+
const existing = paths.get(entry.id);
|
|
266
|
+
if (existing) existing.push({ harness, vendor });
|
|
267
|
+
else paths.set(entry.id, [{ harness, vendor }]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return paths;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Registered harnesses in canonical display order. The registry supplies
|
|
275
|
+
* membership; a registered harness missing from the canonical constant is
|
|
276
|
+
* appended in codepoint order so the report still covers every adapter.
|
|
277
|
+
*
|
|
278
|
+
* @returns {string[]}
|
|
279
|
+
*/
|
|
280
|
+
function displayOrder() {
|
|
281
|
+
const registered = new Set(registeredHarnesses());
|
|
282
|
+
const extra = [...registered].filter((harness) => !MODEL_HARNESS_ORDER.includes(harness)).sort(codepointOrder);
|
|
283
|
+
return [...MODEL_HARNESS_ORDER.filter((harness) => registered.has(harness)), ...extra];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The provider CLI's own catalogue. An absent binary, a non-zero exit, or an
|
|
288
|
+
* unparsable listing falls back to the declared entries.
|
|
289
|
+
*
|
|
290
|
+
* @param {string|undefined} cwd
|
|
291
|
+
* @returns {DeclaredModel[]|null}
|
|
292
|
+
*/
|
|
293
|
+
function agyCliCatalogue(cwd) {
|
|
294
|
+
const executable = getHarness("agy").executable({ harness: "agy", model: "agy-models" });
|
|
295
|
+
const result = spawnSync(executable, ["models"], {
|
|
296
|
+
cwd,
|
|
297
|
+
encoding: "utf8",
|
|
298
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
299
|
+
timeout: AGY_CATALOGUE_TIMEOUT_MS,
|
|
300
|
+
});
|
|
301
|
+
if (result.error || result.status !== 0) return null;
|
|
302
|
+
const models = parseAgyModels(String(result.stdout ?? ""));
|
|
303
|
+
return models.length ? models : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* `agy models` prints one model per line as `id<TAB>display name`; the
|
|
308
|
+
* progress line and anything else without a plausible id is ignored, and the
|
|
309
|
+
* surviving ids are sorted so the CLI's own listing order never reaches the
|
|
310
|
+
* report.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} stdout
|
|
313
|
+
* @returns {DeclaredModel[]}
|
|
314
|
+
*/
|
|
315
|
+
export function parseAgyModels(stdout) {
|
|
316
|
+
/** @type {Set<string>} */
|
|
317
|
+
const ids = new Set();
|
|
318
|
+
for (const line of String(stdout).replace(/\[[0-9;]*m/gu, "").split(/\r?\n/u)) {
|
|
319
|
+
const candidate = line.split("\t")[0].trim();
|
|
320
|
+
if (!/^[a-z0-9][a-z0-9._[\]-]*$/iu.test(candidate)) continue;
|
|
321
|
+
ids.add(candidate);
|
|
322
|
+
}
|
|
323
|
+
return [...ids].sort(codepointOrder).map((id) => agyModel(id));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {import("./index.mjs").ProbeResult} result
|
|
328
|
+
* @returns {ProbeView}
|
|
329
|
+
*/
|
|
330
|
+
function probeView(result) {
|
|
331
|
+
return {
|
|
332
|
+
ok: result.ok,
|
|
333
|
+
available: result.availability?.available === true,
|
|
334
|
+
reason: result.availability?.reason ?? "provider_unavailable",
|
|
335
|
+
version: result.version,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The same law `validatedContract` applies when a contract omits `runtimes`:
|
|
341
|
+
* `contract.mjs` feeds `DISCOVERY_RUNTIME_DEFINITIONS` to `composeAssignments`
|
|
342
|
+
* in its declared order, so the same object and the same function are reused
|
|
343
|
+
* here with every runtime declared available. No availability probe feeds the
|
|
344
|
+
* suggestion — live reachability stays `doctor`'s and `--probe`'s business.
|
|
345
|
+
*
|
|
346
|
+
* @returns {AllocationSuggestion}
|
|
347
|
+
*/
|
|
348
|
+
function suggestedAllocation() {
|
|
349
|
+
/** @type {Record<string, {harness: string, model: string, vendor: string, tier: number|string, costRank: number}>} */
|
|
350
|
+
const runtimes = {};
|
|
351
|
+
/** @type {Record<string, {available: boolean, exhaustedUntil: string|null, reason: string}>} */
|
|
352
|
+
const availability = {};
|
|
353
|
+
for (const [id, definition] of Object.entries(DISCOVERY_RUNTIME_DEFINITIONS)) {
|
|
354
|
+
const vendor = resolveVendor(definition);
|
|
355
|
+
if (!vendor) throw new Error(`discovery runtime ${id} resolves no vendor`);
|
|
356
|
+
runtimes[id] = { ...definition, vendor };
|
|
357
|
+
availability[id] = { available: true, exhaustedUntil: null, reason: "declared" };
|
|
358
|
+
}
|
|
359
|
+
/** @param {boolean} enabled */
|
|
360
|
+
const node = (enabled) => ({ id: "models", gate: { enabled } });
|
|
361
|
+
try {
|
|
362
|
+
const assignment = composeAssignments({ runtimes, runtimeDefaults: {}, nodes: [node(true)] }, availability).models;
|
|
363
|
+
return allocation(assignment.worker, assignment.judge, null, ALLOCATION_LAW, runtimes);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
// No cross-vendor judge is admissible among the declared discovery
|
|
366
|
+
// runtimes: name the runtime the same law still picks for both roles, and
|
|
367
|
+
// say out loud that the pair is a declared same-vendor exception.
|
|
368
|
+
const assignment = composeAssignments({ runtimes, runtimeDefaults: {}, nodes: [node(false)] }, availability).models;
|
|
369
|
+
const vendor = runtimes[assignment.worker]?.vendor ?? "unresolved";
|
|
370
|
+
return allocation(
|
|
371
|
+
assignment.worker,
|
|
372
|
+
assignment.judge,
|
|
373
|
+
vendor,
|
|
374
|
+
`declared exception (${errorMessage(error)}): the worker runtime also judges`,
|
|
375
|
+
runtimes,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @param {string} worker
|
|
382
|
+
* @param {string} judge
|
|
383
|
+
* @param {string|null} vendorException
|
|
384
|
+
* @param {string} reason
|
|
385
|
+
* @param {Record<string, {harness: string, model: string, vendor: string, tier: number|string, costRank: number}>} runtimes
|
|
386
|
+
* @returns {AllocationSuggestion}
|
|
387
|
+
*/
|
|
388
|
+
function allocation(worker, judge, vendorException, reason, runtimes) {
|
|
389
|
+
return {
|
|
390
|
+
worker: allocationRuntime(worker, runtimes),
|
|
391
|
+
judge: allocationRuntime(judge, runtimes),
|
|
392
|
+
vendorException,
|
|
393
|
+
reason,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @param {string} id
|
|
399
|
+
* @param {Record<string, {harness: string, model: string, vendor: string, tier: number|string, costRank: number}>} runtimes
|
|
400
|
+
* @returns {AllocationRuntime}
|
|
401
|
+
*/
|
|
402
|
+
function allocationRuntime(id, runtimes) {
|
|
403
|
+
const runtime = runtimes[id];
|
|
404
|
+
if (!runtime) throw new Error(`allocation named an unknown runtime ${id}`);
|
|
405
|
+
return {
|
|
406
|
+
id,
|
|
407
|
+
harness: runtime.harness,
|
|
408
|
+
model: runtime.model,
|
|
409
|
+
vendor: runtime.vendor,
|
|
410
|
+
tier: runtime.tier ?? null,
|
|
411
|
+
costRank: runtime.costRank ?? null,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @param {ModelsReport} report
|
|
417
|
+
* @returns {string}
|
|
418
|
+
*/
|
|
419
|
+
export function renderModelsReport(report) {
|
|
420
|
+
const lines = [`models · availability: ${report.availability}`, ""];
|
|
421
|
+
for (const harness of report.harnesses) {
|
|
422
|
+
const vendor = harness.vendor ?? harness.vendorNote ?? "unresolved";
|
|
423
|
+
lines.push(`[${harness.harness}] executable ${harness.executable} · vendor ${vendor} · catalogue ${harness.catalogue}`);
|
|
424
|
+
lines.push(` effort: ${effortTransportLine(harness)}`);
|
|
425
|
+
for (const model of harness.models) lines.push(` ${modelLine(harness, model)}`);
|
|
426
|
+
if (!harness.models.length) lines.push(" models: none declared — the runtime declaration names the model");
|
|
427
|
+
if (harness.probe) lines.push(` probe: ${harness.probe.available ? `reachable${harness.probe.version ? ` (${harness.probe.version})` : ""}` : `unreachable (${harness.probe.reason})`}`);
|
|
428
|
+
lines.push("");
|
|
429
|
+
}
|
|
430
|
+
lines.push("suggested allocation");
|
|
431
|
+
lines.push(` worker: ${allocationLine(report.suggestion.worker)}`);
|
|
432
|
+
lines.push(` judge: ${allocationLine(report.suggestion.judge)}`);
|
|
433
|
+
lines.push(` reason: ${report.suggestion.reason}`);
|
|
434
|
+
lines.push(report.suggestion.vendorException
|
|
435
|
+
? ` cross-vendor: worker and judge resolve to ${report.suggestion.vendorException} — declared exception`
|
|
436
|
+
: ` cross-vendor: worker vendor ${report.suggestion.worker.vendor} differs from judge vendor ${report.suggestion.judge.vendor}`);
|
|
437
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {HarnessView} harness
|
|
442
|
+
* @returns {string}
|
|
443
|
+
*/
|
|
444
|
+
function effortTransportLine(harness) {
|
|
445
|
+
if (harness.effortTransport === null) return "none (this harness has no effort flag)";
|
|
446
|
+
const aliases = Object.keys(harness.effortAliases).sort(codepointOrder);
|
|
447
|
+
const aliasText = aliases.length ? ` (${aliases.map((key) => `${key}→${harness.effortAliases[key]}`).join(", ")})` : "";
|
|
448
|
+
const notes = harness.effortNotes.length ? ` · ${harness.effortNotes.join(" · ")}` : "";
|
|
449
|
+
return `${harness.effortTransport}${aliasText}${notes}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* @param {HarnessView} harness
|
|
454
|
+
* @param {ModelView} model
|
|
455
|
+
* @returns {string}
|
|
456
|
+
*/
|
|
457
|
+
function modelLine(harness, model) {
|
|
458
|
+
const parts = [model.id, `context ${model.contextWindowTokens ?? "unknown"}`, effortLine(model)];
|
|
459
|
+
for (const path of model.declaredBy) parts.push(`also declared by ${path.harness} (vendor ${path.vendor ?? "unresolved"})`);
|
|
460
|
+
if (model.effortInModelId && harness.effortTransport !== null) {
|
|
461
|
+
parts.push(`${harness.effortTransport} is passed on top of the id-encoded level`);
|
|
462
|
+
}
|
|
463
|
+
return parts.join(" · ");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* @param {ModelView} model
|
|
468
|
+
* @returns {string}
|
|
469
|
+
*/
|
|
470
|
+
function effortLine(model) {
|
|
471
|
+
if (!model.efforts.length) return "effort none";
|
|
472
|
+
const levels = model.efforts.map((effort) => (effort === model.defaultEffort ? `${effort} (default)` : effort)).join(", ");
|
|
473
|
+
if (model.defaultEffort) return `effort ${levels}${model.effortInModelId ? ", fixed by the model id" : ""}`;
|
|
474
|
+
return `effort ${levels} · default: the harness decides`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @param {AllocationRuntime} runtime
|
|
479
|
+
* @returns {string}
|
|
480
|
+
*/
|
|
481
|
+
function allocationLine(runtime) {
|
|
482
|
+
const tier = runtime.tier === null ? "" : ` · tier ${runtime.tier}`;
|
|
483
|
+
const costRank = runtime.costRank === null ? "" : ` · costRank ${runtime.costRank}`;
|
|
484
|
+
return `${runtime.id} · harness ${runtime.harness} · model ${runtime.model} · vendor ${runtime.vendor}${tier}${costRank}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** @param {string} left @param {string} right @returns {number} */
|
|
488
|
+
function codepointOrder(left, right) {
|
|
489
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* JSON with every object's keys in codepoint order, so the same report always
|
|
494
|
+
* serializes to the same bytes.
|
|
495
|
+
*
|
|
496
|
+
* @param {unknown} value
|
|
497
|
+
* @returns {string}
|
|
498
|
+
*/
|
|
499
|
+
export function stableJsonDocument(value) {
|
|
500
|
+
return `${JSON.stringify(sortedKeys(value), null, 2)}\n`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** @param {unknown} value @returns {unknown} */
|
|
504
|
+
function sortedKeys(value) {
|
|
505
|
+
if (Array.isArray(value)) return value.map(sortedKeys);
|
|
506
|
+
if (!value || typeof value !== "object") return value;
|
|
507
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
508
|
+
return Object.fromEntries(Object.keys(record).sort(codepointOrder).map((key) => [key, sortedKeys(record[key])]));
|
|
509
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { normalizeClaudeResult, parseVersion } from "../protocol.mjs";
|
|
2
|
+
import { hookSettings } from "../../host/tool-policy-hook.mjs";
|
|
3
|
+
|
|
4
|
+
/** Built-in tools a closed-packet worker needs; every other tool is preamble. */
|
|
5
|
+
export const DEFAULT_CLAUDE_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Bound the harness preamble of a Claude-compatible CLI: no skills, no MCP
|
|
9
|
+
* servers, no settings files (an explicit `--settings` still applies, so hook
|
|
10
|
+
* enforcement survives) and only the declared built-in tools. Measured on
|
|
11
|
+
* 2026-09-01 against this CLI: 65,170 uncached input tokens per trivial call
|
|
12
|
+
* with the ambient configuration, about 4,300 per turn with these flags.
|
|
13
|
+
* `--bare` would cut further but disables hooks, so it is never used.
|
|
14
|
+
*
|
|
15
|
+
* @param {import("../index.mjs").HarnessRuntime} runtime
|
|
16
|
+
* @returns {string[]}
|
|
17
|
+
*/
|
|
18
|
+
function claudePreambleArgs(runtime) {
|
|
19
|
+
return [
|
|
20
|
+
"--disable-slash-commands",
|
|
21
|
+
"--strict-mcp-config",
|
|
22
|
+
"--setting-sources",
|
|
23
|
+
"",
|
|
24
|
+
"--tools",
|
|
25
|
+
(runtime.tools ?? DEFAULT_CLAUDE_TOOLS).join(","),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @type {import("../index.mjs").HarnessAdapter}
|
|
31
|
+
*/
|
|
32
|
+
export const claudeHarness = {
|
|
33
|
+
capabilities: {
|
|
34
|
+
structuredOutput: true,
|
|
35
|
+
promptTransport: "stdin",
|
|
36
|
+
sandbox: false,
|
|
37
|
+
permissions: true,
|
|
38
|
+
continuation: true,
|
|
39
|
+
tokenBudget: false,
|
|
40
|
+
costBudget: true,
|
|
41
|
+
usage: true,
|
|
42
|
+
cost: true,
|
|
43
|
+
// The Claude-compatible hook surface enforces the tool policy mechanically.
|
|
44
|
+
toolPolicy: true,
|
|
45
|
+
// `--output-format stream-json --verbose` writes one JSON line per event
|
|
46
|
+
// as the turn runs, not one dump at exit.
|
|
47
|
+
streamsOutput: true,
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Headless acceptEdits denies Bash; bypassPermissions executes commands.
|
|
51
|
+
permissionExecution: { field: "permissionMode", executingModes: ["bypassPermissions"], defaultMode: "acceptEdits" },
|
|
52
|
+
|
|
53
|
+
/** @param {import("../index.mjs").HarnessRuntime} runtime @returns {string} */
|
|
54
|
+
executable(runtime) {
|
|
55
|
+
return process.env.FABERUN_CLAUDE_BIN ?? runtime.executable ?? "claude";
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/** @param {import("../index.mjs").HarnessRuntime} runtime @returns {string[]} */
|
|
59
|
+
versionArgs(runtime) {
|
|
60
|
+
return runtime.versionArgs ?? ["--version"];
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
parseVersion,
|
|
64
|
+
|
|
65
|
+
/** @param {import("../index.mjs").HarnessRuntime} runtime @param {string} prompt @param {import("../index.mjs").CommandOptions} options @returns {import("../index.mjs").HarnessCommand} */
|
|
66
|
+
command(runtime, prompt, options) {
|
|
67
|
+
const continuationId = options.continuationId ?? null;
|
|
68
|
+
const args = [
|
|
69
|
+
"-p",
|
|
70
|
+
...(continuationId ? ["--resume", continuationId] : []),
|
|
71
|
+
"--model",
|
|
72
|
+
runtime.model,
|
|
73
|
+
"--output-format",
|
|
74
|
+
"stream-json",
|
|
75
|
+
"--verbose",
|
|
76
|
+
"--permission-mode",
|
|
77
|
+
runtime.permissionMode ?? "acceptEdits",
|
|
78
|
+
...claudePreambleArgs(runtime),
|
|
79
|
+
];
|
|
80
|
+
if (options.toolPolicy) args.push("--settings", JSON.stringify(hookSettings(options.toolPolicy)));
|
|
81
|
+
if (runtime.reasoning) args.push("--effort", runtime.reasoning);
|
|
82
|
+
if (options.schema) args.push("--json-schema", JSON.stringify(options.schema));
|
|
83
|
+
return { executable: this.executable(runtime), args, promptTransport: "stdin", input: prompt };
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
normalize: normalizeClaudeResult,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const harness = claudeHarness;
|
|
90
|
+
export default claudeHarness;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { normalizeCodexResult, parseVersion, toml } from "../protocol.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bound the Codex harness preamble: a closed-packet worker or a read-only judge
|
|
5
|
+
* needs the shell and patch tools, not browser, computer-use, app or sub-agent
|
|
6
|
+
* tooling, MCP servers, or plugins. `features.code_mode_host` stays enabled:
|
|
7
|
+
* codex-cli 0.152.1 only surfaces commands to OpenAI models through the
|
|
8
|
+
* code-mode host, and without it they make zero tool calls and fabricate
|
|
9
|
+
* answers. Measured on the Sol gate: gpt-5.6-sol with the host disabled used
|
|
10
|
+
* 35,130 input tokens and fabricated its verdict, versus 35,199 with the host
|
|
11
|
+
* enabled and a correct tool-backed verdict; deepseek-v4-flash used 25,795
|
|
12
|
+
* with the host disabled and 25,783 with the host enabled, both correct.
|
|
13
|
+
* These overrides are emitted before the runtime's own `config` entries, so a
|
|
14
|
+
* contract can re-enable any of them.
|
|
15
|
+
*/
|
|
16
|
+
export const CODEX_PREAMBLE_OVERRIDES = Object.freeze([
|
|
17
|
+
"features.browser_use=false",
|
|
18
|
+
"features.browser_use_external=false",
|
|
19
|
+
"features.computer_use=false",
|
|
20
|
+
"features.apps=false",
|
|
21
|
+
"features.multi_agent=false",
|
|
22
|
+
"mcp_servers={}",
|
|
23
|
+
"plugins={}",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @type {import("../index.mjs").HarnessAdapter}
|
|
28
|
+
*/
|
|
29
|
+
export const codexHarness = {
|
|
30
|
+
capabilities: {
|
|
31
|
+
structuredOutput: true,
|
|
32
|
+
promptTransport: "stdin",
|
|
33
|
+
sandbox: true,
|
|
34
|
+
permissions: false,
|
|
35
|
+
continuation: true,
|
|
36
|
+
tokenBudget: true,
|
|
37
|
+
costBudget: false,
|
|
38
|
+
usage: true,
|
|
39
|
+
cost: false,
|
|
40
|
+
toolPolicy: false,
|
|
41
|
+
// `--json` writes one JSONL event per item/turn as it happens, not one
|
|
42
|
+
// dump at exit.
|
|
43
|
+
streamsOutput: true,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Every sandbox mode executes commands; sandbox only bounds their effects.
|
|
47
|
+
permissionExecution: {
|
|
48
|
+
field: "sandbox",
|
|
49
|
+
executingModes: ["read-only", "workspace-write", "danger-full-access"],
|
|
50
|
+
defaultMode: "workspace-write",
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/** @param {import("../index.mjs").HarnessRuntime} runtime @returns {string} */
|
|
54
|
+
executable(runtime) {
|
|
55
|
+
return process.env.FABERUN_CODEX_BIN ?? runtime.executable ?? "codex";
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/** @param {import("../index.mjs").HarnessRuntime} runtime @returns {string[]} */
|
|
59
|
+
versionArgs(runtime) {
|
|
60
|
+
return runtime.versionArgs ?? ["--version"];
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
parseVersion,
|
|
64
|
+
|
|
65
|
+
/** @param {import("../index.mjs").HarnessRuntime} runtime @param {string} prompt @param {import("../index.mjs").CommandOptions} options @returns {import("../index.mjs").HarnessCommand} */
|
|
66
|
+
command(runtime, prompt, options) {
|
|
67
|
+
const continuationId = options.continuationId ?? null;
|
|
68
|
+
const args = continuationId
|
|
69
|
+
? ["exec", "resume", "--json"]
|
|
70
|
+
: ["exec"];
|
|
71
|
+
if (!continuationId) args.push("--json", "--sandbox", runtime.sandbox ?? "workspace-write");
|
|
72
|
+
for (const override of CODEX_PREAMBLE_OVERRIDES) args.push("-c", override);
|
|
73
|
+
for (const [key, value] of Object.entries(runtime.config ?? {})) {
|
|
74
|
+
args.push("-c", `${key}=${toml(value)}`);
|
|
75
|
+
}
|
|
76
|
+
args.push("-c", `model=${toml(runtime.model)}`);
|
|
77
|
+
if (runtime.reasoning) args.push("-c", `model_reasoning_effort=${toml(runtime.reasoning)}`);
|
|
78
|
+
if (options.schemaPath) args.push("--output-schema", options.schemaPath);
|
|
79
|
+
if (continuationId) return { executable: this.executable(runtime), args: [...args, continuationId, prompt], promptTransport: "argv", input: null };
|
|
80
|
+
return { executable: this.executable(runtime), args, promptTransport: "stdin", input: prompt };
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
normalize: normalizeCodexResult,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const harness = codexHarness;
|
|
87
|
+
export default codexHarness;
|