faberun 0.27.0 → 0.29.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/README.md +9 -3
- package/package.json +1 -1
- package/skills/faberun/SKILL.md +3 -3
- package/skills/faberun/references/contract.md +9 -9
- package/skills/faberun/references/local-env.md +11 -0
- package/skills/faberun/references/operations.md +26 -33
- package/src/campaign/brief.mjs +41 -9
- package/src/campaign/campaign-brief.mjs +18 -3
- package/src/campaign/index.mjs +22 -9
- package/src/campaign/ledger.mjs +72 -4
- package/src/campaign/metrics.mjs +29 -2
- package/src/campaign/record.mjs +34 -0
- package/src/cli/campaign.mjs +35 -12
- package/src/cli/contract.mjs +11 -1
- package/src/cli/launch.mjs +31 -4
- package/src/cli/manual.mjs +24 -8
- package/src/cli/plan.mjs +56 -5
- package/src/cli/setup.mjs +5 -0
- package/src/cli/skills.mjs +18 -7
- package/src/cli/uninstall.mjs +542 -0
- package/src/cli/walkthrough.mjs +462 -0
- package/src/cli.mjs +72 -8
- package/src/contract/articles.mjs +30 -0
- package/src/contract/definition-of-done.mjs +164 -0
- package/src/contract/index.mjs +8 -5
- package/src/contract/judge-independence.mjs +18 -0
- package/src/contract/runtime.mjs +20 -6
- package/src/contract/scope-findings.mjs +120 -0
- package/src/contract/snapshot.mjs +19 -5
- package/src/contract/task-packet.mjs +49 -1
- package/src/contract/worker-result.mjs +37 -11
- package/src/engine/assignment.mjs +45 -0
- package/src/engine/detach.mjs +18 -0
- package/src/engine/gate.mjs +10 -2
- package/src/engine/judge-gate.mjs +24 -9
- package/src/engine/live-preflight.mjs +117 -18
- package/src/engine/prompts.mjs +58 -4
- package/src/engine/resume.mjs +119 -12
- package/src/engine/retry.mjs +11 -11
- package/src/engine/run-command.mjs +80 -4
- package/src/engine/run-identity.mjs +27 -14
- package/src/engine/runtime-discovery.mjs +15 -9
- package/src/engine/scheduler.mjs +8 -1
- package/src/engine/worker-env.mjs +179 -0
- package/src/harnesses/agy/index.mjs +11 -0
- package/src/harnesses/catalogue.mjs +29 -0
- package/src/harnesses/claude/index.mjs +98 -1
- package/src/harnesses/codex/index.mjs +15 -0
- package/src/harnesses/dsh/index.mjs +14 -0
- package/src/harnesses/exec-jsonl/index.mjs +12 -0
- package/src/harnesses/fx/index.mjs +13 -0
- package/src/harnesses/index.mjs +21 -10
- package/src/harnesses/replay/index.mjs +12 -0
- package/src/harnesses/zcode/index.mjs +19 -0
- package/src/host/config.mjs +8 -2
- package/src/host/environment-report.mjs +85 -0
- package/src/host/home.mjs +27 -0
- package/src/host/install-registry.mjs +511 -0
- package/src/host/preflight.mjs +32 -9
- package/src/host/uninstall-roots.mjs +196 -0
- package/src/plan/freeze.mjs +2 -0
- package/src/plan/pipeline.mjs +56 -4
- package/src/plan/progress.mjs +101 -0
- package/src/plan/proof-check.mjs +27 -0
- package/src/plan/proof-scope.mjs +1 -1
- package/src/plan/repo-facts.mjs +129 -14
- package/src/plan/rounds.mjs +27 -0
- package/src/plan/spec.mjs +82 -6
- package/src/plan/template.mjs +9 -0
- package/src/repo/declared-paths.mjs +74 -6
- package/src/repo/integrate.mjs +1 -1
- package/src/repo/scope-closure.mjs +59 -0
- package/src/repo/signal-block.mjs +26 -0
- package/src/repo/signal.mjs +9 -24
- package/src/repo/workspace.mjs +189 -14
- package/src/repo/worktree-prune.mjs +13 -5
- package/src/repo/worktree.mjs +54 -0
- package/src/report/campaign-brief.mjs +4 -0
- package/src/report/message.mjs +37 -6
- package/src/report/progress.mjs +13 -2
- package/src/run/paths.mjs +14 -1
- package/src/run/usage.mjs +66 -1
- package/src/util.mjs +18 -2
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment a worker, a judge, or an availability probe may receive.
|
|
3
|
+
*
|
|
4
|
+
* A controller variable reaches a child only when something named it. The
|
|
5
|
+
* names a child may see are the union of:
|
|
6
|
+
*
|
|
7
|
+
* - the base operating-system names below,
|
|
8
|
+
* - the names the selected adapter declares in its `declaredEnvironment`,
|
|
9
|
+
* - the runtime's `*.env_key` names, and
|
|
10
|
+
* - the explicit `envPassthrough` names on the runtime or the contract.
|
|
11
|
+
*
|
|
12
|
+
* Everything else the controller holds -- an `AWS_*`, a `GITHUB_TOKEN`, a
|
|
13
|
+
* `DATABASE_URL` exported in a shell profile -- is dropped. Verification
|
|
14
|
+
* commands are not workers and keep the full controller environment
|
|
15
|
+
* (`judge-gate.mjs`); only worker, judge, and availability-probe spawns route
|
|
16
|
+
* through here.
|
|
17
|
+
*
|
|
18
|
+
* The adapter lists are imported from each adapter directly instead of through
|
|
19
|
+
* `harnesses/catalogue.mjs`: the registry imports this module for its probe, so
|
|
20
|
+
* reaching the catalogue from here would close a runtime import cycle the repo
|
|
21
|
+
* forbids. The catalogue keeps its own copy of the same names for R2, and both
|
|
22
|
+
* read the same adapter exports.
|
|
23
|
+
*/
|
|
24
|
+
import { declaredEnvironment as claudeEnvironment } from "../harnesses/claude/index.mjs";
|
|
25
|
+
import { declaredEnvironment as codexEnvironment } from "../harnesses/codex/index.mjs";
|
|
26
|
+
import { declaredEnvironment as agyEnvironment } from "../harnesses/agy/index.mjs";
|
|
27
|
+
import { declaredEnvironment as dshEnvironment } from "../harnesses/dsh/index.mjs";
|
|
28
|
+
import { declaredEnvironment as fxEnvironment } from "../harnesses/fx/index.mjs";
|
|
29
|
+
import { declaredEnvironment as zcodeEnvironment } from "../harnesses/zcode/index.mjs";
|
|
30
|
+
import { declaredEnvironment as execJsonlEnvironment } from "../harnesses/exec-jsonl/index.mjs";
|
|
31
|
+
import { declaredEnvironment as replayEnvironment } from "../harnesses/replay/index.mjs";
|
|
32
|
+
import { NOTIFY_ENV_NAMES } from "../notify/index.mjs";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The base operating-system names every child needs to resolve a binary, find
|
|
36
|
+
* its home, and speak the locale. `LC_*` is a family, matched by prefix; the
|
|
37
|
+
* Windows entries are listed on every platform because the machine that reads
|
|
38
|
+
* them is the machine that has them.
|
|
39
|
+
*
|
|
40
|
+
* @type {readonly string[]}
|
|
41
|
+
*/
|
|
42
|
+
export const BASE_ENV_NAMES = Object.freeze([
|
|
43
|
+
"PATH",
|
|
44
|
+
"HOME",
|
|
45
|
+
"USER",
|
|
46
|
+
"LOGNAME",
|
|
47
|
+
"SHELL",
|
|
48
|
+
"LANG",
|
|
49
|
+
"TERM",
|
|
50
|
+
"TMPDIR",
|
|
51
|
+
"TZ",
|
|
52
|
+
"SYSTEMROOT",
|
|
53
|
+
"USERPROFILE",
|
|
54
|
+
"APPDATA",
|
|
55
|
+
"LOCALAPPDATA",
|
|
56
|
+
"COMSPEC",
|
|
57
|
+
"PATHEXT",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/** @type {ReadonlySet<string>} */
|
|
61
|
+
const BASE_NAME_SET = new Set(BASE_ENV_NAMES);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The adapter-declared names, keyed by harness. Values never travel here.
|
|
65
|
+
*
|
|
66
|
+
* A `Map` rather than the catalogue's frozen object on purpose: the repo
|
|
67
|
+
* forbids the same top-level body defined twice, and the registry owns the
|
|
68
|
+
* catalogue copy.
|
|
69
|
+
*
|
|
70
|
+
* @type {Map<string, readonly string[]>}
|
|
71
|
+
*/
|
|
72
|
+
const ADAPTER_ENVIRONMENTS = new Map([
|
|
73
|
+
["claude", claudeEnvironment],
|
|
74
|
+
["codex", codexEnvironment],
|
|
75
|
+
["agy", agyEnvironment],
|
|
76
|
+
["dsh", dshEnvironment],
|
|
77
|
+
["fx", fxEnvironment],
|
|
78
|
+
["zcode", zcodeEnvironment],
|
|
79
|
+
["exec-jsonl", execJsonlEnvironment],
|
|
80
|
+
["replay", replayEnvironment],
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} name
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function isBaseName(name) {
|
|
88
|
+
return BASE_NAME_SET.has(name) || name.startsWith("LC_");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The `*.env_key` names a runtime's config declares. A config value there is
|
|
93
|
+
* the environment-variable name, never the value, so this reads names alone.
|
|
94
|
+
*
|
|
95
|
+
* @param {{config?: Record<string, unknown>}|null|undefined} runtime
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
function envKeyNames(runtime) {
|
|
99
|
+
/** @type {string[]} */
|
|
100
|
+
const names = [];
|
|
101
|
+
for (const [key, value] of Object.entries(runtime?.config ?? {})) {
|
|
102
|
+
if (key.endsWith(".env_key") && typeof value === "string" && value.length > 0) names.push(value);
|
|
103
|
+
}
|
|
104
|
+
return names;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The non-base names one runtime may carry: what its adapter declares, what
|
|
109
|
+
* its `*.env_key` config entries name, and what the runtime or the contract
|
|
110
|
+
* explicitly passes through. The notification transport is never one of them,
|
|
111
|
+
* whatever an author writes.
|
|
112
|
+
*
|
|
113
|
+
* @param {{harness?: string, config?: Record<string, unknown>, envPassthrough?: string[]}|null|undefined} runtime
|
|
114
|
+
* @param {{envPassthrough?: string[]}|null|undefined} [contract]
|
|
115
|
+
* @returns {string[]}
|
|
116
|
+
*/
|
|
117
|
+
export function declaredEnvironmentNames(runtime, contract) {
|
|
118
|
+
const names = new Set(ADAPTER_ENVIRONMENTS.get(runtime?.harness ?? "") ?? []);
|
|
119
|
+
for (const name of envKeyNames(runtime)) names.add(name);
|
|
120
|
+
for (const name of Array.isArray(runtime?.envPassthrough) ? runtime.envPassthrough : []) names.add(name);
|
|
121
|
+
for (const name of Array.isArray(contract?.envPassthrough) ? contract.envPassthrough : []) names.add(name);
|
|
122
|
+
for (const name of NOTIFY_ENV_NAMES) names.delete(name);
|
|
123
|
+
return [...names];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The base names present in `source`, including every `LC_*` member.
|
|
128
|
+
*
|
|
129
|
+
* @param {Record<string, string|undefined>} [source]
|
|
130
|
+
* @returns {Record<string, string>}
|
|
131
|
+
*/
|
|
132
|
+
export function baseEnvironment(source = process.env) {
|
|
133
|
+
/** @type {Record<string, string>} */
|
|
134
|
+
const env = {};
|
|
135
|
+
// Look the base names up rather than filtering source keys: Windows exposes
|
|
136
|
+
// `Path`, not `PATH`, and `source[name]` is the case-insensitive read there.
|
|
137
|
+
for (const name of BASE_ENV_NAMES) {
|
|
138
|
+
const value = source[name];
|
|
139
|
+
if (value !== undefined) env[name] = value;
|
|
140
|
+
}
|
|
141
|
+
for (const [name, value] of Object.entries(source)) {
|
|
142
|
+
if (value !== undefined && name.startsWith("LC_")) env[name] = value;
|
|
143
|
+
}
|
|
144
|
+
return env;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The non-base allowlisted names present in `source`, with their controller
|
|
149
|
+
* values. This is the overlay a caller hands the gate: the gate already holds
|
|
150
|
+
* the base set in its own environment, so it only needs what the runtime adds
|
|
151
|
+
* on top.
|
|
152
|
+
*
|
|
153
|
+
* @param {{harness?: string, config?: Record<string, unknown>, envPassthrough?: string[]}|null|undefined} runtime
|
|
154
|
+
* @param {{envPassthrough?: string[]}|null|undefined} [contract]
|
|
155
|
+
* @param {Record<string, string|undefined>} [source]
|
|
156
|
+
* @returns {Record<string, string>}
|
|
157
|
+
*/
|
|
158
|
+
export function passthroughEnvironment(runtime, contract, source = process.env) {
|
|
159
|
+
/** @type {Record<string, string>} */
|
|
160
|
+
const env = {};
|
|
161
|
+
for (const name of declaredEnvironmentNames(runtime, contract)) {
|
|
162
|
+
if (isBaseName(name)) continue;
|
|
163
|
+
const value = source[name];
|
|
164
|
+
if (value !== undefined) env[name] = value;
|
|
165
|
+
}
|
|
166
|
+
return env;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The complete environment a worker, judge, or availability probe receives.
|
|
171
|
+
*
|
|
172
|
+
* @param {{harness?: string, config?: Record<string, unknown>, envPassthrough?: string[]}|null|undefined} runtime
|
|
173
|
+
* @param {{envPassthrough?: string[]}|null|undefined} [contract]
|
|
174
|
+
* @param {Record<string, string|undefined>} [source]
|
|
175
|
+
* @returns {Record<string, string>}
|
|
176
|
+
*/
|
|
177
|
+
export function workerEnvironment(runtime, contract, source = process.env) {
|
|
178
|
+
return { ...baseEnvironment(source), ...passthroughEnvironment(runtime, contract, source) };
|
|
179
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { normalizeAgyResult, parseVersion } from "../protocol.mjs";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The environment names the agy adapter reads to authenticate and configure the
|
|
5
|
+
* CLI. agy signs in through its own CLI configuration, so the only name read
|
|
6
|
+
* here is the binary override `executable()` honours. Values never travel here.
|
|
7
|
+
*
|
|
8
|
+
* @type {readonly string[]}
|
|
9
|
+
*/
|
|
10
|
+
export const declaredEnvironment = Object.freeze([
|
|
11
|
+
"FABERUN_AGY_BIN",
|
|
12
|
+
]);
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* @type {import("../index.mjs").HarnessAdapter}
|
|
5
16
|
*/
|
|
@@ -5,6 +5,14 @@ import { canonicalProvider } from "../contract/provider.mjs";
|
|
|
5
5
|
import { errorMessage } from "../util.mjs";
|
|
6
6
|
import { ANTHROPIC_MODEL_TIERS } from "./model-tiers.mjs";
|
|
7
7
|
import { spawnInvocation } from "../host/platform.mjs";
|
|
8
|
+
import { declaredEnvironment as claudeEnvironment } from "./claude/index.mjs";
|
|
9
|
+
import { declaredEnvironment as codexEnvironment } from "./codex/index.mjs";
|
|
10
|
+
import { declaredEnvironment as agyEnvironment } from "./agy/index.mjs";
|
|
11
|
+
import { declaredEnvironment as dshEnvironment } from "./dsh/index.mjs";
|
|
12
|
+
import { declaredEnvironment as fxEnvironment } from "./fx/index.mjs";
|
|
13
|
+
import { declaredEnvironment as zcodeEnvironment } from "./zcode/index.mjs";
|
|
14
|
+
import { declaredEnvironment as execJsonlEnvironment } from "./exec-jsonl/index.mjs";
|
|
15
|
+
import { declaredEnvironment as replayEnvironment } from "./replay/index.mjs";
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* Model catalogue report: which models each registered harness can run, the
|
|
@@ -38,6 +46,27 @@ export const MODEL_HARNESS_ORDER = Object.freeze([
|
|
|
38
46
|
"replay",
|
|
39
47
|
]);
|
|
40
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The environment names each registered adapter reads to authenticate and
|
|
51
|
+
* configure itself, keyed by harness in `MODEL_HARNESS_ORDER`. Each list is the
|
|
52
|
+
* adapter's own frozen `declaredEnvironment` export, and only bare names ever
|
|
53
|
+
* travel here, never values. The worker/judge environment builder unions these
|
|
54
|
+
* names with the base operating-system set and the runtime's env keys, so a
|
|
55
|
+
* name missing from this catalogue is a name the child process never receives.
|
|
56
|
+
*
|
|
57
|
+
* @type {Readonly<Record<string, readonly string[]>>}
|
|
58
|
+
*/
|
|
59
|
+
export const DECLARED_HARNESS_ENVIRONMENTS = Object.freeze({
|
|
60
|
+
claude: claudeEnvironment,
|
|
61
|
+
codex: codexEnvironment,
|
|
62
|
+
agy: agyEnvironment,
|
|
63
|
+
dsh: dshEnvironment,
|
|
64
|
+
fx: fxEnvironment,
|
|
65
|
+
zcode: zcodeEnvironment,
|
|
66
|
+
"exec-jsonl": execJsonlEnvironment,
|
|
67
|
+
replay: replayEnvironment,
|
|
68
|
+
});
|
|
69
|
+
|
|
41
70
|
const CLAUDE_EFFORTS = Object.freeze(["low", "medium", "high", "max"]);
|
|
42
71
|
const CODEX_EFFORTS = Object.freeze(["low", "medium", "high", "xhigh"]);
|
|
43
72
|
const AGY_EFFORTS = Object.freeze(["low", "medium", "high"]);
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { normalizeClaudeResult, parseVersion } from "../protocol.mjs";
|
|
2
2
|
import { hookSettings } from "../../host/tool-policy-hook.mjs";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The environment names the Claude adapter reads to authenticate and configure
|
|
6
|
+
* the CLI: the provider credential the CLI resolves for API-key sign-in and the
|
|
7
|
+
* binary override `executable()` honours. Values never travel here.
|
|
8
|
+
*
|
|
9
|
+
* @type {readonly string[]}
|
|
10
|
+
*/
|
|
11
|
+
export const declaredEnvironment = Object.freeze([
|
|
12
|
+
"ANTHROPIC_API_KEY",
|
|
13
|
+
"FABERUN_CLAUDE_BIN",
|
|
14
|
+
]);
|
|
15
|
+
|
|
4
16
|
/** Built-in tools a closed-packet worker needs; every other tool is preamble. */
|
|
5
17
|
export const DEFAULT_CLAUDE_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
6
18
|
|
|
@@ -26,6 +38,88 @@ function claudePreambleArgs(runtime) {
|
|
|
26
38
|
];
|
|
27
39
|
}
|
|
28
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The envelope this adapter returns: the shared provider envelope plus the
|
|
43
|
+
* top-level reset instant a session limit names. The field has to live here
|
|
44
|
+
* rather than in `error` so that `exhaustedUntilOf` keeps parsing the sentence
|
|
45
|
+
* against the caller's clock.
|
|
46
|
+
*
|
|
47
|
+
* @typedef {import("../index.mjs").ProviderEnvelope & {resetAt?: string}} ClaudeEnvelope
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The reset sentence a Claude quota stop writes into its error message, e.g.
|
|
52
|
+
* "You've hit your session limit · resets 6:40pm (America/Sao_Paulo)". The
|
|
53
|
+
* wall-clock form names a time and a zone but no date; an absolute form names
|
|
54
|
+
* the instant directly ("Your limit will reset at 2026-09-08 20:30:00").
|
|
55
|
+
*/
|
|
56
|
+
const SESSION_RESET_SENTENCE = /resets?(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)+|UTC|GMT)\)/iu;
|
|
57
|
+
const ABSOLUTE_RESET_SENTENCE = /reset(?:s| at| on)?\s+(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:?\d{2})?)/iu;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The instant a Claude quota stop names, or null when its message names none.
|
|
61
|
+
*
|
|
62
|
+
* The generic availability classifier (`harnesses/index.mjs`'s
|
|
63
|
+
* `exhaustedUntilOf`) parses the same sentence for the durable refusal store,
|
|
64
|
+
* but the node's own wait decision (`engine/backoff.mjs`'s
|
|
65
|
+
* `quotaResetSchedule`) reads only a structured `resetAt`. This adapter is the
|
|
66
|
+
* protocol boundary and cannot import `harnesses/index.mjs` -- that module
|
|
67
|
+
* imports this one at load -- so the sentence is parsed here. A zone the
|
|
68
|
+
* runtime does not know names no instant, and the failover edge is taken as
|
|
69
|
+
* before.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} message
|
|
72
|
+
* @param {number} [now] the instant a wall-clock sentence is read against
|
|
73
|
+
* @returns {string|null}
|
|
74
|
+
*/
|
|
75
|
+
function sessionResetAt(message, now = Date.now()) {
|
|
76
|
+
const absolute = ABSOLUTE_RESET_SENTENCE.exec(message);
|
|
77
|
+
if (absolute) {
|
|
78
|
+
const value = absolute[1];
|
|
79
|
+
const zoned = value.includes("T") || /(?:Z|[+-]\d{2}:?\d{2})$/u.test(value) ? value : `${value.replace(" ", "T")}Z`;
|
|
80
|
+
const parsed = Date.parse(zoned);
|
|
81
|
+
if (Number.isFinite(parsed)) return new Date(parsed).toISOString();
|
|
82
|
+
}
|
|
83
|
+
const match = SESSION_RESET_SENTENCE.exec(message);
|
|
84
|
+
if (!match) return null;
|
|
85
|
+
let hour = Number(match[1]);
|
|
86
|
+
const minute = Number(match[2] ?? "0");
|
|
87
|
+
const meridiem = match[3]?.toLowerCase();
|
|
88
|
+
if (meridiem === "pm" && hour < 12) hour += 12;
|
|
89
|
+
if (meridiem === "am" && hour === 12) hour = 0;
|
|
90
|
+
if (hour > 23 || minute > 59) return null;
|
|
91
|
+
/** @type {Intl.DateTimeFormatPart[]} */
|
|
92
|
+
let parts;
|
|
93
|
+
try {
|
|
94
|
+
parts = new Intl.DateTimeFormat("en-US", { timeZone: match[4], hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }).formatToParts(new Date(now));
|
|
95
|
+
} catch {
|
|
96
|
+
// An unknown zone name is not a reset time; the caller falls back to failover.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const at = (/** @type {Intl.DateTimeFormatPartTypes} */ type) => Number(parts.find((part) => part.type === type)?.value);
|
|
100
|
+
const offset = Date.UTC(at("year"), at("month") - 1, at("day"), at("hour") % 24, at("minute"), at("second")) - Math.floor(now / 1000) * 1000;
|
|
101
|
+
let target = Date.UTC(at("year"), at("month") - 1, at("day"), hour, minute) - offset;
|
|
102
|
+
if (target <= now) target += 86_400_000;
|
|
103
|
+
return new Date(target).toISOString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Thread a session-limit reset into the envelope so the node can wait for it.
|
|
108
|
+
* The instant rides the envelope's top-level `resetAt`, the field the node's
|
|
109
|
+
* wait decision (`engine/backoff.mjs`'s `quotaResetSchedule`) reads. It is not
|
|
110
|
+
* written into `error.resetAt`/`exhaustedUntil` on purpose: `exhaustedUntilOf`
|
|
111
|
+
* must keep parsing the sentence against the clock its caller passes, so a test
|
|
112
|
+
* or a replayed envelope can re-read the same message at a chosen instant.
|
|
113
|
+
*
|
|
114
|
+
* @param {ClaudeEnvelope} envelope
|
|
115
|
+
* @returns {ClaudeEnvelope}
|
|
116
|
+
*/
|
|
117
|
+
function withSessionReset(envelope) {
|
|
118
|
+
if (envelope.status !== "exhausted" || envelope.resetAt) return envelope;
|
|
119
|
+
const resetAt = sessionResetAt(String(envelope.error?.message ?? ""));
|
|
120
|
+
return resetAt ? { ...envelope, resetAt } : envelope;
|
|
121
|
+
}
|
|
122
|
+
|
|
29
123
|
/**
|
|
30
124
|
* The environment one runtime's CLI runs under when its contract points it at
|
|
31
125
|
* an Anthropic-compatible endpoint other than Anthropic's own, e.g. GLM on
|
|
@@ -174,7 +268,10 @@ export const claudeHarness = {
|
|
|
174
268
|
return declaredBaseUrl(runtime) === null;
|
|
175
269
|
},
|
|
176
270
|
|
|
177
|
-
|
|
271
|
+
/** @param {string} stdout @param {number|null} exitCode @param {string|null} signal @param {import("../index.mjs").NormalizeOptions} [options] @returns {import("../index.mjs").ProviderEnvelope} */
|
|
272
|
+
normalize(stdout, exitCode, signal, options = {}) {
|
|
273
|
+
return withSessionReset(normalizeClaudeResult(stdout, exitCode, signal, options));
|
|
274
|
+
},
|
|
178
275
|
};
|
|
179
276
|
|
|
180
277
|
export const harness = claudeHarness;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { normalizeCodexResult, parseVersion, toml } from "../protocol.mjs";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The environment names the Codex adapter reads to authenticate and configure
|
|
5
|
+
* the CLI: the provider credential for API-key sign-in, the config home
|
|
6
|
+
* `codexHome()` reads when it locates the session file its usage windows come
|
|
7
|
+
* from, and the binary override `executable()` honours. Values never travel
|
|
8
|
+
* here.
|
|
9
|
+
*
|
|
10
|
+
* @type {readonly string[]}
|
|
11
|
+
*/
|
|
12
|
+
export const declaredEnvironment = Object.freeze([
|
|
13
|
+
"CODEX_HOME",
|
|
14
|
+
"FABERUN_CODEX_BIN",
|
|
15
|
+
"OPENAI_API_KEY",
|
|
16
|
+
]);
|
|
17
|
+
|
|
3
18
|
/**
|
|
4
19
|
* Bound the Codex harness preamble: a closed-packet worker or a read-only judge
|
|
5
20
|
* needs the shell and patch tools, not browser, computer-use, app or sub-agent
|
|
@@ -3,6 +3,20 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { parseVersion } from "../protocol.mjs";
|
|
4
4
|
import { normalizeRunnerTranscript, runnerEnvironmentOverlay, withSchema } from "../runner-transcript.mjs";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The environment names the dsh adapter reads to authenticate and configure the
|
|
8
|
+
* harness: the provider credential the dsh CLI resolves for the declared
|
|
9
|
+
* provider and the binary override `executable()` honours. The environment
|
|
10
|
+
* overlay's `GIT_CONFIG_*` scrub is a generic pass over ambient keys, not a
|
|
11
|
+
* declared dependency. Values never travel here.
|
|
12
|
+
*
|
|
13
|
+
* @type {readonly string[]}
|
|
14
|
+
*/
|
|
15
|
+
export const declaredEnvironment = Object.freeze([
|
|
16
|
+
"DEEPSEEK_API_KEY",
|
|
17
|
+
"FABERUN_DSH_BIN",
|
|
18
|
+
]);
|
|
19
|
+
|
|
6
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
7
21
|
|
|
8
22
|
/**
|
|
@@ -31,6 +31,18 @@ import { finite } from "../../util.mjs";
|
|
|
31
31
|
|
|
32
32
|
export { HARNESS_OUTPUT_LIMIT_BYTES } from "../protocol.mjs";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The environment names the exec-jsonl adapter reads to authenticate and
|
|
36
|
+
* configure the wrapper: only the binary override `executable()` honours.
|
|
37
|
+
* Credentials belong to the wrapper the contract names, so the adapter reads
|
|
38
|
+
* none of them. Values never travel here.
|
|
39
|
+
*
|
|
40
|
+
* @type {readonly string[]}
|
|
41
|
+
*/
|
|
42
|
+
export const declaredEnvironment = Object.freeze([
|
|
43
|
+
"FABERUN_EXEC_JSONL_BIN",
|
|
44
|
+
]);
|
|
45
|
+
|
|
34
46
|
export const EXEC_JSONL_PROTOCOL = Object.freeze({
|
|
35
47
|
schemaVersion: 1,
|
|
36
48
|
requestType: "run.request",
|
|
@@ -10,6 +10,19 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { parseVersion } from "../protocol.mjs";
|
|
11
11
|
import { normalizeRunnerTranscript, runnerEnvironmentOverlay, withSchema } from "../runner-transcript.mjs";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The environment names this adapter reads outside the base set and the
|
|
15
|
+
* runtime's own `*.env_key` names: the provider key the client falls back to
|
|
16
|
+
* when a runtime names none, and the binary override `executable()` honours.
|
|
17
|
+
* Values never travel here.
|
|
18
|
+
*
|
|
19
|
+
* @type {readonly string[]}
|
|
20
|
+
*/
|
|
21
|
+
export const declaredEnvironment = Object.freeze([
|
|
22
|
+
"DEEPSEEK_API_KEY",
|
|
23
|
+
"FABERUN_FX_BIN",
|
|
24
|
+
]);
|
|
25
|
+
|
|
13
26
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
27
|
|
|
15
28
|
/**
|
package/src/harnesses/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fxHarness } from "./fx/index.mjs";
|
|
|
7
7
|
import { zcodeHarness } from "./zcode/index.mjs";
|
|
8
8
|
import { execJsonlHarness } from "./exec-jsonl/index.mjs";
|
|
9
9
|
import { replayHarness } from "./replay/index.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { passthroughEnvironment, workerEnvironment } from "../engine/worker-env.mjs";
|
|
11
11
|
import { spawnInvocation } from "../host/platform.mjs";
|
|
12
12
|
|
|
13
13
|
/** Current wire-contract version for runner protocol artifacts. */
|
|
@@ -72,7 +72,7 @@ const CAPABILITY_NAMES = new Set([
|
|
|
72
72
|
* `config["replay.recording"]` for commands, and dsh requires
|
|
73
73
|
* `config.provider` for the provider route every attempt runs on.
|
|
74
74
|
*
|
|
75
|
-
* @typedef {{id?: string, harness: string, model: string, reasoning?: string, sandbox?: string, permissionMode?: string, config?: Record<string, unknown>, printTimeout?: string, tools?: string[], executable?: string, args?: string[], versionArgs?: string[], maxArgvPromptBytes?: number, requiredCapabilities?: CapabilityRequirements, tier?: number|string, vendor?: string}} HarnessRuntime
|
|
75
|
+
* @typedef {{id?: string, harness: string, model: string, reasoning?: string, sandbox?: string, permissionMode?: string, config?: Record<string, unknown>, printTimeout?: string, tools?: string[], executable?: string, args?: string[], versionArgs?: string[], maxArgvPromptBytes?: number, requiredCapabilities?: CapabilityRequirements, envPassthrough?: string[], tier?: number|string, vendor?: string}} HarnessRuntime
|
|
76
76
|
*/
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -216,10 +216,14 @@ export function resolveVendor(runtime) {
|
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
218
|
* Build one provider invocation. Prompt transport is explicit in the result:
|
|
219
|
-
* stdin adapters return `input`, while argv adapters append the prompt.
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
219
|
+
* stdin adapters return `input`, while argv adapters append the prompt.
|
|
220
|
+
*
|
|
221
|
+
* The returned `env` is the runtime-specific allowlist overlay (adapter
|
|
222
|
+
* declarations, `*.env_key` names, and the runtime's `envPassthrough`) merged
|
|
223
|
+
* over the adapter's own overlay; a null value removes the ambient variable.
|
|
224
|
+
* The spawn gate unions it with the base operating-system names, so no other
|
|
225
|
+
* controller variable reaches the child. A caller-supplied `options.env`
|
|
226
|
+
* merges over everything here, once, for every harness.
|
|
223
227
|
*
|
|
224
228
|
* @param {HarnessRuntime} runtime
|
|
225
229
|
* @param {string} prompt
|
|
@@ -229,6 +233,12 @@ export function resolveVendor(runtime) {
|
|
|
229
233
|
export function providerCommand(runtime, prompt, options = {}) {
|
|
230
234
|
const harness = getHarness(runtime.harness);
|
|
231
235
|
const command = harness.command(runtime, prompt, options);
|
|
236
|
+
// The runtime-specific allowlist travels to the gate as an overlay. The gate
|
|
237
|
+
// unions it with the base operating-system names and nothing else, so a
|
|
238
|
+
// controller variable no adapter, `*.env_key`, or envPassthrough named never
|
|
239
|
+
// reaches the child. It is deliberately not the whole environment: the
|
|
240
|
+
// caller's overlay stays an overlay, and the base set stays the gate's job.
|
|
241
|
+
command.env = { ...passthroughEnvironment(runtime, undefined, process.env), ...command.env };
|
|
232
242
|
if (options.env) command.env = { ...command.env, ...options.env };
|
|
233
243
|
if (command.promptTransport === "argv") {
|
|
234
244
|
const limit = runtime.maxArgvPromptBytes ?? harness.capabilities.maxArgvPromptBytes;
|
|
@@ -507,10 +517,11 @@ export function probeRuntime(runtime, options = {}) {
|
|
|
507
517
|
const invocation = spawnInvocation(executable, args, { cwd: options.cwd });
|
|
508
518
|
child = spawn(invocation.command, invocation.args, {
|
|
509
519
|
cwd: options.cwd,
|
|
510
|
-
// A
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
520
|
+
// A probe is a worker-class spawn: it receives only the base names,
|
|
521
|
+
// the adapter's declared names, the runtime's `*.env_key` names, and
|
|
522
|
+
// any envPassthrough the runtime carries. It never delivers a
|
|
523
|
+
// notification; the controller does.
|
|
524
|
+
env: workerEnvironment(runtime),
|
|
514
525
|
stdio: ["ignore", "pipe", "pipe"],
|
|
515
526
|
...invocation.options,
|
|
516
527
|
});
|
|
@@ -2,6 +2,18 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { parseVersion } from "../protocol.mjs";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The environment names the replay adapter reads to authenticate and configure
|
|
7
|
+
* the recording: only the binary override `executable()` honours. A recording
|
|
8
|
+
* carries no credential of its own, and `replay/bin.mjs` reads no environment
|
|
9
|
+
* name at all. Values never travel here.
|
|
10
|
+
*
|
|
11
|
+
* @type {readonly string[]}
|
|
12
|
+
*/
|
|
13
|
+
export const declaredEnvironment = Object.freeze([
|
|
14
|
+
"FABERUN_REPLAY_BIN",
|
|
15
|
+
]);
|
|
16
|
+
|
|
5
17
|
/** Envelope statuses a recording may carry. */
|
|
6
18
|
const REPLAY_STATUSES = Object.freeze(new Set([
|
|
7
19
|
"done",
|
|
@@ -3,6 +3,25 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { delimiter, join } from "node:path";
|
|
4
4
|
import { normalizeZcodeResult, parseVersion } from "../protocol.mjs";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The environment names the zcode adapter reads to authenticate and configure
|
|
8
|
+
* the CLI: the credential the CLI resolves (`auth_token.env_key`, default
|
|
9
|
+
* `ZAI_API_KEY`, with `ANTHROPIC_AUTH_TOKEN` as its fallback), the app install
|
|
10
|
+
* directory `zcodeBundle()` reads, and the binary override `executable()`
|
|
11
|
+
* honours. The `ZCODE_MODEL`, `ZCODE_BASE_URL`, `ANTHROPIC_API_KEY` and
|
|
12
|
+
* `ZCODE_LOG_*` names this adapter writes into the child environment are
|
|
13
|
+
* configuration it supplies, not dependencies it reads. Values never travel
|
|
14
|
+
* here.
|
|
15
|
+
*
|
|
16
|
+
* @type {readonly string[]}
|
|
17
|
+
*/
|
|
18
|
+
export const declaredEnvironment = Object.freeze([
|
|
19
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
20
|
+
"FABERUN_ZCODE_APP_DIR",
|
|
21
|
+
"FABERUN_ZCODE_BIN",
|
|
22
|
+
"ZAI_API_KEY",
|
|
23
|
+
]);
|
|
24
|
+
|
|
6
25
|
/** Default Z.ai Anthropic-compatible endpoint serving GLM models. */
|
|
7
26
|
const ZCODE_DEFAULT_BASE_URL = "https://api.z.ai/api/anthropic";
|
|
8
27
|
|
package/src/host/config.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* enabled, which runtime is the default worker and judge, the machine's own
|
|
4
4
|
* ordered `judges` list (R18) -- read only when a contract declares none --
|
|
5
5
|
* and its ordered `reviewers` list (R19), `faberun plan`'s own machine
|
|
6
|
-
* default, read only when `--reviewers` names none.
|
|
6
|
+
* default, read only when `--reviewers` names none. Its `judgeIndependence`
|
|
7
|
+
* (R36) is the machine-wide same-vendor opt-in, read only when a contract
|
|
8
|
+
* declares none.
|
|
7
9
|
*
|
|
8
10
|
* It is separate from `host/home.mjs`, which owns the path, because reading a
|
|
9
11
|
* config is a validation problem and writing it is an atomic-write problem;
|
|
@@ -13,8 +15,9 @@
|
|
|
13
15
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
16
|
import { dirname } from "node:path";
|
|
15
17
|
import { configPath, faberunHome } from "./home.mjs";
|
|
18
|
+
import { SAME_VENDOR_REVIEW_MODE } from "../contract/judge-independence.mjs";
|
|
16
19
|
|
|
17
|
-
/** @typedef {{schemaVersion: 1, harnesses: string[], worker?: string, judge?: string, judges?: string[], reviewers?: string[], updatedAt: string}} UserConfig */
|
|
20
|
+
/** @typedef {{schemaVersion: 1, harnesses: string[], worker?: string, judge?: string, judges?: string[], reviewers?: string[], judgeIndependence?: "same-vendor", updatedAt: string}} UserConfig */
|
|
18
21
|
|
|
19
22
|
/** Paths already reported malformed, so a process that reads twice warns once. */
|
|
20
23
|
const warned = new Set();
|
|
@@ -88,5 +91,8 @@ function isUserConfig(value) {
|
|
|
88
91
|
if (record.judge !== undefined && typeof record.judge !== "string") return false;
|
|
89
92
|
if (record.judges !== undefined && (!Array.isArray(record.judges) || !record.judges.every((id) => typeof id === "string"))) return false;
|
|
90
93
|
if (record.reviewers !== undefined && (!Array.isArray(record.reviewers) || !record.reviewers.every((id) => typeof id === "string"))) return false;
|
|
94
|
+
// The machine's same-vendor opt-in shares the contract field's single
|
|
95
|
+
// accepted value; anything else is malformed, not silently ignored.
|
|
96
|
+
if (record.judgeIndependence !== undefined && record.judgeIndependence !== SAME_VENDOR_REVIEW_MODE) return false;
|
|
91
97
|
return typeof record.updatedAt === "string";
|
|
92
98
|
}
|