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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { rejectUnknown, requireId, requireString } from "./assert.mjs";
|
|
2
|
+
import { errorMessage, shellWords } from "../util.mjs";
|
|
3
|
+
import { Script } from "node:vm";
|
|
2
4
|
/**
|
|
3
5
|
* Schema 2 Definition of Done items. Each item is an object that names an
|
|
4
6
|
* observable outcome and declares how it is proven: mechanically through a
|
|
@@ -210,5 +212,167 @@ export function unquotedFilterValueWarnings(items, index) {
|
|
|
210
212
|
return warnings;
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
/**
|
|
216
|
+
* The body of an inline `node -e`/`node --eval` script named by a command
|
|
217
|
+
* proof's shell command, or null when the command carries none. The proof runs
|
|
218
|
+
* through a shell, so the body is the next shell word with its quotes removed:
|
|
219
|
+
* `shellWords` expands and escapes nothing, which is also how the shell hands
|
|
220
|
+
* the body to node. `-e` belongs to other commands too (`grep -e` the common
|
|
221
|
+
* one), so a scan only starts at a token that names the node binary.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} commandText
|
|
224
|
+
* @returns {string|null}
|
|
225
|
+
*/
|
|
226
|
+
export function inlineScriptBody(commandText) {
|
|
227
|
+
const tokens = shellWords(commandText);
|
|
228
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
229
|
+
if (!isNodeExecutable(tokens[index])) continue;
|
|
230
|
+
for (let position = index + 1; position < tokens.length; position += 1) {
|
|
231
|
+
const token = tokens[position];
|
|
232
|
+
if (SHELL_SEPARATORS.has(token)) break;
|
|
233
|
+
if (token === "-e" || token === "--eval") {
|
|
234
|
+
const body = tokens[position + 1];
|
|
235
|
+
return body === undefined || SHELL_SEPARATORS.has(body) ? null : body;
|
|
236
|
+
}
|
|
237
|
+
if (token.startsWith("--eval=")) return token.slice("--eval=".length);
|
|
238
|
+
if (token.startsWith("-e=")) return token.slice("-e=".length);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The syntax error a `node -e`/`node --eval` body names, or null when the
|
|
246
|
+
* command is not an inline script or its body parses. `vm.Script` is node's
|
|
247
|
+
* own grammar for `-e`; the two shapes node's module wrapper accepts but a
|
|
248
|
+
* classic script does not -- top-level `await` and static ESM syntax -- are
|
|
249
|
+
* re-tried or allowed rather than refused, so a valid proof is never refused
|
|
250
|
+
* for a body this check cannot parse. A genuine error in the tail of an
|
|
251
|
+
* awaited body still surfaces through the async re-try.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} commandText
|
|
254
|
+
* @returns {string|null}
|
|
255
|
+
*/
|
|
256
|
+
export function inlineScriptParseError(commandText) {
|
|
257
|
+
const body = inlineScriptBody(commandText);
|
|
258
|
+
return body === null ? null : scriptSyntaxError(body);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Refuse a contract whose command proof is an inline script that does not
|
|
263
|
+
* parse, naming the node and the Definition of Done item. The launch calls
|
|
264
|
+
* this before the first dispatch: a body the parser rejects can never exit 0,
|
|
265
|
+
* so every attempt would be spent on work no delivery could satisfy.
|
|
266
|
+
*
|
|
267
|
+
* @param {{id?: unknown, definitionOfDone?: {id?: unknown, proof?: {kind?: unknown, ref?: unknown}}[]}[]} nodes
|
|
268
|
+
* @returns {void}
|
|
269
|
+
*/
|
|
270
|
+
export function assertInlineScriptProofsParse(nodes) {
|
|
271
|
+
for (const node of nodes) {
|
|
272
|
+
for (const item of node.definitionOfDone ?? []) {
|
|
273
|
+
if (item.proof?.kind !== "command" || typeof item.proof.ref !== "string") continue;
|
|
274
|
+
const parseError = inlineScriptParseError(item.proof.ref);
|
|
275
|
+
if (parseError === null) continue;
|
|
276
|
+
const nodeName = node.id === undefined ? "a node" : `node ${node.id}`;
|
|
277
|
+
throw new TypeError(`${nodeName}, Definition of Done item "${item.id}": the proof command's inline script does not parse: ${parseError}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The quote a command proof's text leaves open, or null when every quote it
|
|
284
|
+
* opens is closed. `kind: "command"` goes through `/bin/sh -c`, so a text with
|
|
285
|
+
* an odd number of `'` or `"` is refused by the shell before the command runs,
|
|
286
|
+
* with `unexpected EOF while looking for matching`. Measured 2026-09-27 (AP13
|
|
287
|
+
* of safe-to-hand-to-friend): the freeze accepted
|
|
288
|
+
* `node --test --test-name-pattern=a stranger's first campaign completes
|
|
289
|
+
* offline test/…` — an apostrophe inside an unquoted test title — the node did
|
|
290
|
+
* the work and passed its own test in 15 s, and both attempts were spent on a
|
|
291
|
+
* command `/bin/sh` never ran.
|
|
292
|
+
*
|
|
293
|
+
* This is a quote scanner, not a shell parser: it reports the two failures a
|
|
294
|
+
* proof can carry without expanding anything (an open quote, or nothing at
|
|
295
|
+
* all), and leaves every other shell construct to the shell. A `$'…'` string
|
|
296
|
+
* and a quote inside a here-document are the constructs it does not model.
|
|
297
|
+
*
|
|
298
|
+
* @param {string} commandText
|
|
299
|
+
* @returns {string|null}
|
|
300
|
+
*/
|
|
301
|
+
export function unclosedQuote(commandText) {
|
|
302
|
+
/** @type {string|null} */
|
|
303
|
+
let quote = null;
|
|
304
|
+
let escaped = false;
|
|
305
|
+
for (const character of commandText) {
|
|
306
|
+
if (escaped) { escaped = false; continue; }
|
|
307
|
+
// Outside double quotes a backslash escapes the next character; a
|
|
308
|
+
// backslash inside single quotes is literal, which POSIX also gives it.
|
|
309
|
+
if (quote !== "'" && character === "\\") { escaped = true; continue; }
|
|
310
|
+
if (quote === null && (character === "'" || character === '"')) { quote = character; continue; }
|
|
311
|
+
if (quote === character) quote = null;
|
|
312
|
+
}
|
|
313
|
+
return quote;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Refuse a contract whose command proof the shell cannot parse, naming the
|
|
318
|
+
* node, the item and the open quote. The freeze and the launch both call it:
|
|
319
|
+
* a command no attempt can run is the same waste as an inline script that
|
|
320
|
+
* cannot parse, and cheaper to refuse than to discover.
|
|
321
|
+
*
|
|
322
|
+
* @param {{id?: unknown, definitionOfDone?: {id?: unknown, proof?: {kind?: unknown, ref?: unknown}}[]}[]} nodes
|
|
323
|
+
* @returns {void}
|
|
324
|
+
*/
|
|
325
|
+
export function assertProofCommandsParse(nodes) {
|
|
326
|
+
for (const node of nodes) {
|
|
327
|
+
for (const item of node.definitionOfDone ?? []) {
|
|
328
|
+
if (item.proof?.kind !== "command" || typeof item.proof.ref !== "string") continue;
|
|
329
|
+
const quote = unclosedQuote(item.proof.ref);
|
|
330
|
+
if (quote === null) continue;
|
|
331
|
+
const nodeName = node.id === undefined ? "a node" : `node ${node.id}`;
|
|
332
|
+
throw new TypeError(`${nodeName}, Definition of Done item "${item.id}": the proof command leaves a ${quote} open, so /bin/sh refuses it before it runs ("unexpected EOF while looking for matching \`${quote}'"). Quote the value, as in --test-name-pattern="<title>".`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** @param {string} token @returns {boolean} */
|
|
338
|
+
function isNodeExecutable(token) {
|
|
339
|
+
const base = (token.split(/[\\/]/u).pop() ?? token).toLowerCase();
|
|
340
|
+
return base === "node" || base === "nodejs" || base === "node.exe";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} body
|
|
345
|
+
* @returns {string|null}
|
|
346
|
+
*/
|
|
347
|
+
function scriptSyntaxError(body) {
|
|
348
|
+
try {
|
|
349
|
+
void new Script(body);
|
|
350
|
+
return null;
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const message = errorMessage(error);
|
|
353
|
+
if (AWAIT_OUTSIDE_ASYNC.test(message)) {
|
|
354
|
+
try {
|
|
355
|
+
// The rest of the body still has to parse, so an unparsable tail is
|
|
356
|
+
// refused even when the head needed the async wrapper to parse at all.
|
|
357
|
+
void new Script(`(async () => {\n${body}\n})`);
|
|
358
|
+
return null;
|
|
359
|
+
} catch (wrapped) {
|
|
360
|
+
const wrappedMessage = errorMessage(wrapped);
|
|
361
|
+
return MODULE_SYNTAX.test(wrappedMessage) ? null : wrappedMessage;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// node's `-e` accepts static imports, exports and `import.meta`; no stable
|
|
365
|
+
// API parses a module here, so a module-only diagnostic is not a syntax
|
|
366
|
+
// error this check can prove and the body is allowed.
|
|
367
|
+
return MODULE_SYNTAX.test(message) ? null : message;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** vm.Script's diagnostics for syntax node's `-e` module wrapper accepts. */
|
|
372
|
+
const AWAIT_OUTSIDE_ASYNC = /await is only valid in async functions|^Unexpected reserved word$/u;
|
|
373
|
+
const MODULE_SYNTAX = /Cannot use import statement outside a module|Cannot use 'import\.meta' outside a module|Unexpected token 'export'/u;
|
|
374
|
+
/** Shell tokens that end one command, so a later `-e` is not this node's. */
|
|
375
|
+
const SHELL_SEPARATORS = new Set(["&&", "||", "|", ";", "&", ">", ">>", "<"]);
|
|
376
|
+
|
|
213
377
|
/** The node:test filter flags a `kind: "command"` proof's shell can split on an unquoted value. */
|
|
214
378
|
const TEST_FILTER_FLAGS = ["--test-name-pattern", "--test-skip-pattern"];
|
package/src/contract/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
|
-
import { loadTaskPacket, renderWorkerPrompt } from "./task-packet.mjs";
|
|
4
|
+
import { loadTaskPacket, renderWorkerPrompt, validateEnvPassthrough } from "./task-packet.mjs";
|
|
5
5
|
import { RESERVED_ARTICLES } from "./articles.mjs";
|
|
6
6
|
import { judgmentReasonWarnings, unquotedFilterValueWarnings, validateDefinitionOfDone } from "./definition-of-done.mjs";
|
|
7
7
|
import { validateFinalVerification, validateSharedVerification } from "./final-verification.mjs";
|
|
@@ -37,7 +37,7 @@ const CONTRACT_FIELDS = new Set([
|
|
|
37
37
|
"schemaVersion", "contractVersion", "id", "campaignId", "goal", "cwd", "sourceIdentity",
|
|
38
38
|
"maxParallel", "pollIntervalMs", "stallTimeoutSec", "timeoutSec", "maxTurns", "phaseSessionReuse",
|
|
39
39
|
"runtimeDefaults", "runtimes", "nodes", "warnings", "finalVerification", "sharedVerification", "nodeAdvisory", "judges",
|
|
40
|
-
"judgeIndependence",
|
|
40
|
+
"judgeIndependence", "envPassthrough",
|
|
41
41
|
]);
|
|
42
42
|
const DEFAULTS_FIELDS = new Set(["worker", "judge"]);
|
|
43
43
|
const NODE_FIELDS = new Set([
|
|
@@ -68,13 +68,13 @@ const GATE_REVIEWS = new Set(["none", "advisory", "blocking"]);
|
|
|
68
68
|
|
|
69
69
|
/** @typedef {{mode: "execution"|"discovery"|"autonomous", objective: string, instructions: string[], readFiles: string[], writeFiles?: string[], writeRoots?: string[], symbols: string[], scopeAcknowledged?: string[], decisions: string[], nonGoals: string[], verification: VerificationCommand[]}} TaskPacket */
|
|
70
70
|
|
|
71
|
-
/** @typedef {{harness: "claude"|"codex"|"agy"|"dsh"|"fx"|"zcode"|"exec-jsonl"|"replay", model: string, reasoning?: string, sandbox?: "read-only"|"workspace-write"|"danger-full-access", permissionMode?: string, config?: Record<string, unknown>, printTimeout?: string, tools?: string[], executable?: string, args?: string[], versionArgs?: string[], maxArgvPromptBytes?: number, requiredCapabilities?: CapabilityRequirements, costRank?: number, fallback?: string, vendor: string, tier?: number|string, stallTimeoutSec?: number, maxConcurrent?: number}} ValidatedRuntime */
|
|
71
|
+
/** @typedef {{harness: "claude"|"codex"|"agy"|"dsh"|"fx"|"zcode"|"exec-jsonl"|"replay", model: string, reasoning?: string, sandbox?: "read-only"|"workspace-write"|"danger-full-access", permissionMode?: string, config?: Record<string, unknown>, printTimeout?: string, tools?: string[], executable?: string, args?: string[], versionArgs?: string[], maxArgvPromptBytes?: number, requiredCapabilities?: CapabilityRequirements, costRank?: number, fallback?: string, vendor: string, tier?: number|string, stallTimeoutSec?: number, maxConcurrent?: number, envPassthrough?: string[]}} ValidatedRuntime */
|
|
72
72
|
|
|
73
73
|
/** @typedef {{enabled: boolean, review?: ("none"|"advisory"|"blocking"), runtime?: string, failOn?: ("minor"|"major"|"critical")[], maxRevisions?: number, requiredCapabilities?: CapabilityRequirements, skipWhen?: {verificationGreen: true, maxChangedPaths: number}}} ValidatedGate */
|
|
74
74
|
|
|
75
75
|
/** @typedef {{id: string, type: string, phase: string, requirementIds?: string[], runtime?: string, dependsOn: string[], taskPacket: TaskPacket, taskPacketFile?: string, prompt: string, definitionOfDone: import("./definition-of-done.mjs").DefinitionOfDoneItem[], gate: ValidatedGate, timeoutSec?: number, maxTurns?: number, requiredCapabilities: CapabilityRequirements, packetHash: string, sourceIdentity: SourceIdentity, replayPolicy: "safe"|"reconcile"|"never", sameProviderReview: boolean, humanStep?: {step: string, command: string}}} ValidatedNode */
|
|
76
76
|
|
|
77
|
-
/** @typedef {{schemaVersion: number, contractVersion: string, id: string, campaignId: string, goal: string, cwd: string, sourceIdentity: SourceIdentity, runtimes: Record<string, ValidatedRuntime>, runtimeDefaults: {worker?: string, judge?: string}, judges?: string[], judgeIndependence?: "same-vendor", nodes: ValidatedNode[], maxParallel: number, pollIntervalMs: number, stallTimeoutSec: number, timeoutSec: number, maxTurns: number, phaseSessionReuse: boolean, finalVerification?: VerificationCommand[], sharedVerification?: VerificationCommand[], nodeAdvisory?: NodeAdvisoryPolicy, warnings: string[]}} ValidatedContract */
|
|
77
|
+
/** @typedef {{schemaVersion: number, contractVersion: string, id: string, campaignId: string, goal: string, cwd: string, sourceIdentity: SourceIdentity, runtimes: Record<string, ValidatedRuntime>, runtimeDefaults: {worker?: string, judge?: string}, judges?: string[], judgeIndependence?: "same-vendor", nodes: ValidatedNode[], maxParallel: number, pollIntervalMs: number, stallTimeoutSec: number, timeoutSec: number, maxTurns: number, phaseSessionReuse: boolean, finalVerification?: VerificationCommand[], sharedVerification?: VerificationCommand[], nodeAdvisory?: NodeAdvisoryPolicy, envPassthrough?: string[], warnings: string[]}} ValidatedContract */
|
|
78
78
|
/** @typedef {{costUsd?: number, durationSec?: number}} NodeAdvisoryPolicy */
|
|
79
79
|
|
|
80
80
|
/** @typedef {"pending"|"running"|"done"|"no-op"|"blocked"|"failed"|"exhausted"|"stalled"|"canceled"} NodeStatus */
|
|
@@ -134,6 +134,9 @@ export function validateContract(raw, contractPath, options = {}) {
|
|
|
134
134
|
requireId(raw.id, "contract.id");
|
|
135
135
|
requireId(raw.campaignId, "contract.campaignId");
|
|
136
136
|
requireString(raw.goal, "contract.goal");
|
|
137
|
+
// The contract-wide passthrough, unioned with each runtime's own at spawn
|
|
138
|
+
// time. Absent keeps every contract authored before the field valid.
|
|
139
|
+
if (raw.envPassthrough !== undefined) validateEnvPassthrough(raw.envPassthrough, "contract.envPassthrough");
|
|
137
140
|
|
|
138
141
|
const contractDir = dirname(resolve(contractPath));
|
|
139
142
|
const cwd = resolve(contractDir, typeof raw.cwd === "string" ? raw.cwd : ".");
|
|
@@ -150,7 +153,7 @@ export function validateContract(raw, contractPath, options = {}) {
|
|
|
150
153
|
throw new TypeError("contract.runtimes must be an object");
|
|
151
154
|
}
|
|
152
155
|
const runtimes = /** @type {Record<string, ValidatedRuntime>} */ ({});
|
|
153
|
-
for (const [id, runtime] of Object.entries(rawRuntimes)) runtimes[id] = validateRuntime(id, runtime);
|
|
156
|
+
for (const [id, runtime] of Object.entries(rawRuntimes)) runtimes[id] = validateRuntime(id, runtime, { persisted });
|
|
154
157
|
// A runtime's fallback is validated against sibling runtimes once every
|
|
155
158
|
// runtime is known, so declaration order never matters.
|
|
156
159
|
for (const [id, runtime] of Object.entries(runtimes)) {
|
|
@@ -25,6 +25,24 @@ export function validateJudgeIndependence(value, label) {
|
|
|
25
25
|
return value;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The `judgeIndependence` in force for a launch (R36): the contract's own
|
|
30
|
+
* declaration wins, and a contract that declares none inherits the machine
|
|
31
|
+
* config's opt-in. Both sources are already validated to the single accepted
|
|
32
|
+
* value before they reach here -- the contract by `validateJudgeIndependence`,
|
|
33
|
+
* the config by `isUserConfig` -- so this is a plain precedence read, not a
|
|
34
|
+
* second validation. A value neither source names (or a malformed raw config a
|
|
35
|
+
* test hands in) reads as no opt-in, exactly as an absent field would.
|
|
36
|
+
*
|
|
37
|
+
* @param {{judgeIndependence?: string}|null|undefined} contract
|
|
38
|
+
* @param {{judgeIndependence?: string}|null|undefined} config
|
|
39
|
+
* @returns {"same-vendor"|undefined}
|
|
40
|
+
*/
|
|
41
|
+
export function effectiveJudgeIndependence(contract, config) {
|
|
42
|
+
const mode = contract?.judgeIndependence ?? config?.judgeIndependence;
|
|
43
|
+
return mode === SAME_VENDOR_REVIEW_MODE ? SAME_VENDOR_REVIEW_MODE : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
/**
|
|
29
47
|
* Same-vendor mode's own admissibility rule for a worker/judge pair that
|
|
30
48
|
* share a vendor: a judge model absent from the declared tier table can never
|
package/src/contract/runtime.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* validator to reach it.
|
|
11
11
|
*/
|
|
12
12
|
import { assertObject, nonNegativeNumber, positiveInteger, positiveNumber, rejectUnknown, requireId, requireString, requireStringArray, requireTimestamp } from "./assert.mjs";
|
|
13
|
+
import { validateEnvPassthrough } from "./task-packet.mjs";
|
|
13
14
|
import { composeAssignments } from "../engine/runtime-discovery.mjs";
|
|
14
15
|
import { harnessCapabilities, readOnlyModeOf, resolvePermissionExecution, resolveVendor, validateCapabilityRequirements, writesWorkspace } from "../harnesses/index.mjs";
|
|
15
16
|
import { canonicalProvider } from "./provider.mjs";
|
|
@@ -25,7 +26,7 @@ import { stableJson } from "../util.mjs";
|
|
|
25
26
|
const RUNTIME_FIELDS = new Set([
|
|
26
27
|
"harness", "model", "reasoning", "sandbox", "permissionMode", "config", "printTimeout", "tools",
|
|
27
28
|
"executable", "args", "versionArgs", "maxArgvPromptBytes", "requiredCapabilities", "costRank",
|
|
28
|
-
"fallback", "vendor", "tier", "pricing", "stallTimeoutSec", "maxConcurrent",
|
|
29
|
+
"fallback", "vendor", "tier", "pricing", "stallTimeoutSec", "maxConcurrent", "envPassthrough",
|
|
29
30
|
]);
|
|
30
31
|
const RUNTIME_HARNESSES = new Set(["claude", "codex", "agy", "dsh", "fx", "zcode", "exec-jsonl", "replay"]);
|
|
31
32
|
|
|
@@ -92,9 +93,11 @@ export function routeRuntime(contract, node, role = "worker", event = {}) {
|
|
|
92
93
|
/**
|
|
93
94
|
* @param {string} id
|
|
94
95
|
* @param {unknown} runtime
|
|
96
|
+
* @param {{persisted?: boolean}} [options] `persisted` is a replay of a contract
|
|
97
|
+
* already accepted at launch; see the vendor rule below.
|
|
95
98
|
* @returns {ValidatedRuntime}
|
|
96
99
|
*/
|
|
97
|
-
export function validateRuntime(id, runtime) {
|
|
100
|
+
export function validateRuntime(id, runtime, options = {}) {
|
|
98
101
|
requireId(id, `runtime ${id}`);
|
|
99
102
|
assertObject(runtime, `runtime ${id}`);
|
|
100
103
|
rejectUnknown(runtime, RUNTIME_FIELDS, `runtime ${id}`);
|
|
@@ -102,16 +105,24 @@ export function validateRuntime(id, runtime) {
|
|
|
102
105
|
const typedRuntime = /** @type {{harness: string, model: string, vendor?: string, config?: Record<string, unknown>}} */ (runtime);
|
|
103
106
|
// The canonical provider derived from the route, the model's family, or the
|
|
104
107
|
// harness default outranks a declared `vendor`: a label that contradicts it
|
|
105
|
-
// is refused (R18), and the runtime's vendor becomes the derived
|
|
106
|
-
// rather than whatever text the contract declared. `replay` and
|
|
107
|
-
// derive none — for those the declared vendor stands, as it
|
|
108
|
+
// is refused (R18) at authoring, and the runtime's vendor becomes the derived
|
|
109
|
+
// provider rather than whatever text the contract declared. `replay` and
|
|
110
|
+
// `exec-jsonl` derive none — for those the declared vendor stands, as it
|
|
111
|
+
// always has. A persisted load takes the derived provider without refusing,
|
|
112
|
+
// so a run launched before R18 stays readable and cancelable.
|
|
108
113
|
const derived = canonicalProvider(typedRuntime);
|
|
109
114
|
const declared = typeof typedRuntime.vendor === "string" && typedRuntime.vendor.length ? typedRuntime.vendor : undefined;
|
|
110
115
|
let vendor;
|
|
111
116
|
if (derived) {
|
|
112
|
-
if (declared !== undefined && declared !== derived) {
|
|
117
|
+
if (declared !== undefined && declared !== derived && options.persisted !== true) {
|
|
113
118
|
throw new TypeError(`runtime ${id} declares vendor ${declared} but ${typedRuntime.harness} ${typedRuntime.model} is provider ${derived}`);
|
|
114
119
|
}
|
|
120
|
+
// A replay canonicalizes a contradicting label instead of refusing it.
|
|
121
|
+
// Measured 2026-09-27 (AP6): three runs launched before R18 carried
|
|
122
|
+
// `vendor: anthropic-sonnet` against the model claude-sonnet-5, and every
|
|
123
|
+
// reader of their persisted contract — `cancel`, `status`, `report` — threw
|
|
124
|
+
// on a rule those contracts predate, leaving the runs unsuspended forever.
|
|
125
|
+
// The rule still refuses the label where it can be acted on: at authoring.
|
|
115
126
|
vendor = derived;
|
|
116
127
|
} else {
|
|
117
128
|
vendor = resolveVendor(typedRuntime);
|
|
@@ -150,6 +161,9 @@ function validateRuntimeValues(runtime, label, executableRequired) {
|
|
|
150
161
|
if (runtime.args !== undefined) requireStringArray(runtime.args, `${label}.args`);
|
|
151
162
|
if (runtime.versionArgs !== undefined) requireStringArray(runtime.versionArgs, `${label}.versionArgs`);
|
|
152
163
|
if (runtime.maxArgvPromptBytes !== undefined) positiveInteger(runtime.maxArgvPromptBytes, `${label}.maxArgvPromptBytes`);
|
|
164
|
+
// Names only, on the runtime descriptor directly or on the contract: either
|
|
165
|
+
// one asks the worker allowlist to carry the variable through by name.
|
|
166
|
+
if (runtime.envPassthrough !== undefined) validateEnvPassthrough(runtime.envPassthrough, `${label}.envPassthrough`);
|
|
153
167
|
if (runtime.costRank !== undefined) nonNegativeNumber(runtime.costRank, `${label}.costRank`);
|
|
154
168
|
if (runtime.stallTimeoutSec !== undefined) positiveNumber(runtime.stallTimeoutSec, `${label}.stallTimeoutSec`);
|
|
155
169
|
if (runtime.pricing !== undefined) validatePricing(runtime.pricing, `${label}.pricing`);
|
|
@@ -87,3 +87,123 @@ export function verificationFailureWithScope(verdict, scope) {
|
|
|
87
87
|
}],
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
|
|
91
|
+
/** The additions a reauthor discovery pass may propose to a refused packet. */
|
|
92
|
+
/** @typedef {{readFiles?: string[], writeFiles?: string[], scopeAcknowledged?: string[], symbols?: string[]}} ReauthorAdditions */
|
|
93
|
+
|
|
94
|
+
/** The bounded journal record a reauthor attempt leaves behind. */
|
|
95
|
+
export const MAX_REAUTHOR_PATHS = 64;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A widened packet may not take over a write another node's packet already
|
|
99
|
+
* owns. `validateContract` catches a packet that does not close its own scope
|
|
100
|
+
* and a pair whose test one node cannot touch, but two nodes declaring the
|
|
101
|
+
* same write are each individually closed, so no per-packet check sees the
|
|
102
|
+
* collision. This names the owner the discovery pass must leave alone.
|
|
103
|
+
*
|
|
104
|
+
* @param {import("../contract/index.mjs").ValidatedNode[]} nodes every node of the contract
|
|
105
|
+
* @param {string} nodeId the node being widened
|
|
106
|
+
* @param {string[]|undefined} addedWriteFiles the writes the discovery pass proposes to add
|
|
107
|
+
* @returns {{path: string, nodeId: string, reason: string}[]}
|
|
108
|
+
*/
|
|
109
|
+
export function reauthorWriteConflicts(nodes, nodeId, addedWriteFiles) {
|
|
110
|
+
const added = new Set(addedWriteFiles ?? []);
|
|
111
|
+
if (added.size === 0) return [];
|
|
112
|
+
/** @type {{path: string, nodeId: string, reason: string}[]} */
|
|
113
|
+
const conflicts = [];
|
|
114
|
+
for (const node of nodes) {
|
|
115
|
+
if (node.id === nodeId) continue;
|
|
116
|
+
for (const path of node.taskPacket.writeFiles ?? []) {
|
|
117
|
+
if (!added.has(path)) continue;
|
|
118
|
+
conflicts.push({
|
|
119
|
+
path,
|
|
120
|
+
nodeId: node.id,
|
|
121
|
+
reason: `${path} is already declared in writeFiles by node ${node.id}; a widened packet may not take over another node's write`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return conflicts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The bounded journal record a reauthor attempt leaves: the widened packet the
|
|
130
|
+
* discovery pass proposed (`reauthorProposal`) and the hard budget it spent
|
|
131
|
+
* (`reauthorRounds`). It lives in `reauthor.jsonl` beside the run's events
|
|
132
|
+
* rather than on the node snapshot, so a proposal never moves the authored
|
|
133
|
+
* packet hash or asks the snapshot schema to carry a transient shape.
|
|
134
|
+
*
|
|
135
|
+
* @param {{
|
|
136
|
+
* nodeId: string,
|
|
137
|
+
* outcome: "applied"|"approval_required"|"rounds_exhausted",
|
|
138
|
+
* riskTier: string,
|
|
139
|
+
* budget: number,
|
|
140
|
+
* roundsUsed: number,
|
|
141
|
+
* additions: ReauthorAdditions|null|undefined,
|
|
142
|
+
* packet: import("./task-packet.mjs").TaskPacket|null|undefined,
|
|
143
|
+
* findings: string[]|null|undefined,
|
|
144
|
+
* history: {round: number, accepted: boolean, additions?: ReauthorAdditions, findings?: string[]}[],
|
|
145
|
+
* }} entry
|
|
146
|
+
* @returns {Record<string, unknown>}
|
|
147
|
+
*/
|
|
148
|
+
export function reauthorProposalRecord({ nodeId, outcome, riskTier, budget, roundsUsed, additions, packet, findings, history }) {
|
|
149
|
+
/** @param {string[]|undefined} paths @returns {string[]} */
|
|
150
|
+
const bounded = (paths) => [...new Set(paths ?? [])].slice(0, MAX_REAUTHOR_PATHS);
|
|
151
|
+
return {
|
|
152
|
+
at: new Date().toISOString(),
|
|
153
|
+
node: nodeId,
|
|
154
|
+
outcome,
|
|
155
|
+
riskTier,
|
|
156
|
+
reauthorProposal: {
|
|
157
|
+
packet: packet ?? null,
|
|
158
|
+
addedReadFiles: bounded(additions?.readFiles),
|
|
159
|
+
addedWriteFiles: bounded(additions?.writeFiles),
|
|
160
|
+
addedScopeAcknowledged: bounded(additions?.scopeAcknowledged),
|
|
161
|
+
findings: (findings ?? []).slice(0, 8),
|
|
162
|
+
},
|
|
163
|
+
reauthorRounds: {
|
|
164
|
+
budget,
|
|
165
|
+
used: roundsUsed,
|
|
166
|
+
exhausted: outcome === "rounds_exhausted",
|
|
167
|
+
history: history.map((item) => ({
|
|
168
|
+
round: item.round,
|
|
169
|
+
accepted: item.accepted,
|
|
170
|
+
addedReadFiles: bounded(item.additions?.readFiles),
|
|
171
|
+
addedWriteFiles: bounded(item.additions?.writeFiles),
|
|
172
|
+
findings: (item.findings ?? []).slice(0, 8),
|
|
173
|
+
})),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The risk tier a frozen node's gate encodes: a disabled gate is low risk, a
|
|
180
|
+
* blocking review is high, an advisory review is standard. A contract does not
|
|
181
|
+
* carry the plan's own riskTier, so the gate it froze into is the durable
|
|
182
|
+
* statement of how much a widening of its packet is trusted.
|
|
183
|
+
*
|
|
184
|
+
* @param {import("../contract/index.mjs").ValidatedNode} node
|
|
185
|
+
* @returns {"low"|"standard"|"high"}
|
|
186
|
+
*/
|
|
187
|
+
export function reauthorRiskTier(node) {
|
|
188
|
+
if (!node.gate.enabled) return "low";
|
|
189
|
+
return node.gate.review === "blocking" ? "high" : "standard";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether a validated widening may be applied without an explicit operator
|
|
194
|
+
* yes. Mirrors the planning pipeline's `--approve-below`: `high` approves
|
|
195
|
+
* everything, `none` approves nothing, and the default `standard` approves
|
|
196
|
+
* everything but a high-risk node. `approve` overrides the threshold.
|
|
197
|
+
*
|
|
198
|
+
* @param {import("../contract/index.mjs").ValidatedNode} node
|
|
199
|
+
* @param {{approve?: boolean, approveBelow?: string}} [options]
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
202
|
+
export function reauthorApproved(node, options = {}) {
|
|
203
|
+
if (options.approve === true) return true;
|
|
204
|
+
const approveBelow = options.approveBelow ?? "standard";
|
|
205
|
+
if (approveBelow === "high") return true;
|
|
206
|
+
if (approveBelow === "none") return false;
|
|
207
|
+
if (approveBelow !== "standard") throw new TypeError(`approveBelow must be one of standard, high, none: ${approveBelow}`);
|
|
208
|
+
return reauthorRiskTier(node) !== "high";
|
|
209
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Buffer } from "node:buffer";
|
|
12
12
|
import { MAX_SCOPE_FINDING_PATHS } from "./scope-findings.mjs";
|
|
13
13
|
import { REVIEW_MODES } from "./review-modes.mjs";
|
|
14
|
+
import { SAME_VENDOR_REVIEW_MODE } from "./judge-independence.mjs";
|
|
14
15
|
import { assertObject, boundedString, nonNegativeInteger, nonNegativeNumber, positiveInteger, positiveNumber, rejectUnknown, requireId, requireInteger, requirePacketHash, requireString, requireTimestamp } from "./assert.mjs";
|
|
15
16
|
import { stableJson } from "../util.mjs";
|
|
16
17
|
import { isAbsolute } from "node:path";
|
|
@@ -26,11 +27,17 @@ import { validateWorkerResult } from "./worker-result.mjs";
|
|
|
26
27
|
/** @typedef {import("./index.mjs").RunMetadata} RunMetadata */
|
|
27
28
|
/** @typedef {import("./index.mjs").ValidatedNode} ValidatedNode */
|
|
28
29
|
|
|
30
|
+
/** Hard byte ceiling for an operator's answer text, enforced where it is accepted. */
|
|
31
|
+
export const OPERATOR_ANSWER_MAX_BYTES = 8 * 1024;
|
|
32
|
+
|
|
33
|
+
/** Hard ceiling for the failure evidence of a `Previous attempt` section, in bytes. */
|
|
34
|
+
export const PREVIOUS_ATTEMPT_EVIDENCE_MAX_BYTES = 8 * 1024;
|
|
35
|
+
|
|
29
36
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
37
|
+
* The whole persisted `Previous attempt` section: the bounded evidence, then
|
|
38
|
+
* the operator's answer carried whole under its heading (engine/retry.mjs).
|
|
32
39
|
*/
|
|
33
|
-
|
|
40
|
+
const PREVIOUS_ATTEMPT_MAX_BYTES = PREVIOUS_ATTEMPT_EVIDENCE_MAX_BYTES + OPERATOR_ANSWER_MAX_BYTES + 64;
|
|
34
41
|
|
|
35
42
|
const GATE_VERDICTS = new Set(["pass", "fail", "invalid_judge_output"]);
|
|
36
43
|
const NODE_STATUSES = new Set(["pending", "running", "done", "no-op", "blocked", "failed", "exhausted", "stalled", "canceled"]);
|
|
@@ -203,7 +210,7 @@ export function validateNodeSnapshot(value, expectedNode = null) {
|
|
|
203
210
|
if (typeof value.previousAttempt !== "string" || !value.previousAttempt.trim()) {
|
|
204
211
|
throw new TypeError("node snapshot.previousAttempt must be a non-empty string");
|
|
205
212
|
}
|
|
206
|
-
if (Buffer.byteLength(value.previousAttempt, "utf8") >
|
|
213
|
+
if (Buffer.byteLength(value.previousAttempt, "utf8") > PREVIOUS_ATTEMPT_MAX_BYTES) throw new TypeError(`node snapshot.previousAttempt exceeds ${PREVIOUS_ATTEMPT_MAX_BYTES} bytes`);
|
|
207
214
|
}
|
|
208
215
|
if (expectedNode) validateSnapshotBinding(value, expectedNode);
|
|
209
216
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 128 * 1024) throw new TypeError("node snapshot exceeds 131072 bytes");
|
|
@@ -508,12 +515,19 @@ function validateRoutingState(value, label) {
|
|
|
508
515
|
if (value.judgeList !== undefined) validateJudgeListState(value.judgeList, `${label}.judgeList`);
|
|
509
516
|
if (value.assignments !== undefined) {
|
|
510
517
|
assertObject(value.assignments, `${label}.assignments`);
|
|
511
|
-
rejectUnknown(value.assignments, new Set(["worker", "judge", "composedWorker", "composedJudge"]), `${label}.assignments`);
|
|
518
|
+
rejectUnknown(value.assignments, new Set(["worker", "judge", "composedWorker", "composedJudge", "judgeIndependence"]), `${label}.assignments`);
|
|
512
519
|
requireId(value.assignments.worker, `${label}.assignments.worker`);
|
|
513
520
|
requireId(value.assignments.judge, `${label}.assignments.judge`);
|
|
514
521
|
for (const key of ["composedWorker", "composedJudge"]) {
|
|
515
522
|
if (value.assignments[key] !== undefined && typeof value.assignments[key] !== "boolean") throw new TypeError(`${label}.assignments.${key} must be boolean`);
|
|
516
523
|
}
|
|
524
|
+
// R36: the effective judge-independence mode the launch composed under,
|
|
525
|
+
// recorded so a resume, brief or report reads the decision back. Only the
|
|
526
|
+
// one accepted mode may be persisted; its absence is the cross-vendor
|
|
527
|
+
// default.
|
|
528
|
+
if (value.assignments.judgeIndependence !== undefined && value.assignments.judgeIndependence !== SAME_VENDOR_REVIEW_MODE) {
|
|
529
|
+
throw new TypeError(`${label}.assignments.judgeIndependence must be "${SAME_VENDOR_REVIEW_MODE}"`);
|
|
530
|
+
}
|
|
517
531
|
}
|
|
518
532
|
if (value.availability !== undefined) {
|
|
519
533
|
assertObject(value.availability, `${label}.availability`);
|
|
@@ -2,6 +2,7 @@ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "nod
|
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { validateVerificationCommands } from "./verification.mjs";
|
|
5
|
+
import { RESERVED_OWNER_DECISIONS, uncoveredReservedOwnerDecisions } from "./articles.mjs";
|
|
5
6
|
import { errorCode } from "../util.mjs";
|
|
6
7
|
import { requireString, requireStringArray } from "./assert.mjs";
|
|
7
8
|
|
|
@@ -101,6 +102,25 @@ export function loadTaskPacket(node, contractDir, cwd, index, options = {}) {
|
|
|
101
102
|
return validateTaskPacket(packet, index, cwd, options);
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The `## Reserved owner decisions` section every worker prompt carries: the
|
|
107
|
+
* same list the judge reads, with the packet's own coverage spelled out, so an
|
|
108
|
+
* uncovered owner decision is refused with `blocked_context` instead of being
|
|
109
|
+
* chosen by a worker.
|
|
110
|
+
*
|
|
111
|
+
* @param {string[]} decisions
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
function reservedOwnerDecisionsLines(decisions) {
|
|
115
|
+
const uncovered = uncoveredReservedOwnerDecisions(decisions);
|
|
116
|
+
return [
|
|
117
|
+
"## Reserved owner decisions",
|
|
118
|
+
`These decisions belong to the repository owner, never to this node: ${RESERVED_OWNER_DECISIONS.join(", ")}.`,
|
|
119
|
+
`Reserved decisions with no coverage in "Decisions already made": ${uncovered.length ? uncovered.join(", ") : "(none)"}.`,
|
|
120
|
+
"If the task requires taking an uncovered decision, do not choose it: return the blocked_context worker-result object naming the decision.",
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
104
124
|
/**
|
|
105
125
|
* @param {TaskPacket} packet
|
|
106
126
|
* @param {string} nodeId
|
|
@@ -133,6 +153,8 @@ export function renderWorkerPrompt(packet, nodeId) {
|
|
|
133
153
|
"## Decisions already made",
|
|
134
154
|
...bulletOrNone(packet.decisions),
|
|
135
155
|
"",
|
|
156
|
+
...reservedOwnerDecisionsLines(packet.decisions),
|
|
157
|
+
"",
|
|
136
158
|
"## Non-goals",
|
|
137
159
|
...bulletOrNone(packet.nonGoals),
|
|
138
160
|
"",
|
|
@@ -266,6 +288,28 @@ export function validateTaskPacket(packet, index, cwd, options = {}) {
|
|
|
266
288
|
};
|
|
267
289
|
}
|
|
268
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The environment-variable names an operator allows through to a worker, the
|
|
293
|
+
* one validator both the runtime descriptor and the contract share. The list
|
|
294
|
+
* is names only -- never values -- so it selects from the controller's own
|
|
295
|
+
* environment without carrying a secret of its own. A name is the portable
|
|
296
|
+
* shell shape: a letter or underscore, then letters, digits, or underscores.
|
|
297
|
+
*
|
|
298
|
+
* @param {unknown} value
|
|
299
|
+
* @param {string} label
|
|
300
|
+
* @returns {string[]}
|
|
301
|
+
*/
|
|
302
|
+
export function validateEnvPassthrough(value, label) {
|
|
303
|
+
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array of environment-variable names`);
|
|
304
|
+
const names = /** @type {unknown[]} */ (value);
|
|
305
|
+
names.forEach((name, index) => {
|
|
306
|
+
if (typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) {
|
|
307
|
+
throw new TypeError(`${label}[${index}] must be an environment-variable name`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
return /** @type {string[]} */ (names);
|
|
311
|
+
}
|
|
312
|
+
|
|
269
313
|
/**
|
|
270
314
|
* @param {string} path
|
|
271
315
|
* @param {string} label
|
|
@@ -456,6 +500,8 @@ function renderDiscoveryPrompt(packet, nodeId) {
|
|
|
456
500
|
"## Decisions already made",
|
|
457
501
|
...bulletOrNone(packet.decisions),
|
|
458
502
|
"",
|
|
503
|
+
...reservedOwnerDecisionsLines(packet.decisions),
|
|
504
|
+
"",
|
|
459
505
|
"## Non-goals",
|
|
460
506
|
...bulletOrNone(packet.nonGoals),
|
|
461
507
|
"",
|
|
@@ -484,7 +530,7 @@ function renderAutonomousPrompt(packet, nodeId) {
|
|
|
484
530
|
packet.objective,
|
|
485
531
|
"",
|
|
486
532
|
"## Closed context",
|
|
487
|
-
"This autonomous context may inspect the repository read-only as needed.
|
|
533
|
+
"This autonomous context may inspect the repository read-only as needed. The write roots below are the expected boundary, not a hard wall: a write outside them is allowed when the task requires it, and the result must name the path and the reason. Return the blocked_context worker-result object below only for context that is genuinely missing.",
|
|
488
534
|
"",
|
|
489
535
|
"## Instructions",
|
|
490
536
|
...numbered(packet.instructions),
|
|
@@ -501,6 +547,8 @@ function renderAutonomousPrompt(packet, nodeId) {
|
|
|
501
547
|
"## Decisions already made",
|
|
502
548
|
...bulletOrNone(packet.decisions),
|
|
503
549
|
"",
|
|
550
|
+
...reservedOwnerDecisionsLines(packet.decisions),
|
|
551
|
+
"",
|
|
504
552
|
"## Non-goals",
|
|
505
553
|
...bulletOrNone(packet.nonGoals),
|
|
506
554
|
"",
|