faberun 0.26.2 → 0.26.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "faberun",
3
- "version": "0.26.2",
3
+ "version": "0.26.3",
4
4
  "description": "Faberun is a development orchestration system that turns intent into verified software: harness- and model-agnostic, it keeps the intent, coordinates the work, verifies the result and decides what happens next.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { assertObject, nonNegativeNumber, positiveInteger, positiveNumber, rejectUnknown, requireId, requireString, requireStringArray, requireTimestamp } from "./assert.mjs";
13
13
  import { composeAssignments } from "../engine/runtime-discovery.mjs";
14
- import { harnessCapabilities, resolvePermissionExecution, resolveVendor, validateCapabilityRequirements, writesWorkspace } from "../harnesses/index.mjs";
14
+ import { harnessCapabilities, readOnlyModeOf, resolvePermissionExecution, resolveVendor, validateCapabilityRequirements, writesWorkspace } from "../harnesses/index.mjs";
15
15
  import { canonicalProvider } from "./provider.mjs";
16
16
  import { stableJson } from "../util.mjs";
17
17
  /** @typedef {import("./index.mjs").NodeStatus} NodeStatus */
@@ -246,8 +246,9 @@ export function judgeWriteWarnings(runtimes, defaults, nodes) {
246
246
  const runtime = runtimes[/** @type {string} */ (id)];
247
247
  const execution = resolvePermissionExecution(runtime);
248
248
  if (!execution.field || runtime[execution.field] === undefined || !writesWorkspace(runtime)) return [];
249
- if (writesWorkspace({ ...runtime, [execution.field]: "read-only" })) return [];
250
- return [`judge runtime ${id} declares ${execution.field} ${execution.mode}; a judge's verdict reaches the gate without writing, so declare ${execution.field} read-only`];
249
+ const readOnlyMode = readOnlyModeOf(runtime);
250
+ if (readOnlyMode === null) return [];
251
+ return [`judge runtime ${id} declares ${execution.field} ${execution.mode}; a judge's verdict reaches the gate without writing, so declare ${execution.field} ${readOnlyMode}`];
251
252
  });
252
253
  }
253
254
  /**
@@ -52,8 +52,11 @@ export const claudeHarness = {
52
52
  signalsProcesses: true,
53
53
  },
54
54
 
55
- // Headless acceptEdits denies Bash; bypassPermissions executes commands.
56
- permissionExecution: { field: "permissionMode", executingModes: ["bypassPermissions"], defaultMode: "acceptEdits" },
55
+ // Headless acceptEdits denies Bash; bypassPermissions executes commands;
56
+ // plan neither edits nor runs anything, the mode a judge or a preflight ask
57
+ // needs (measured 2026-09-27: an Opus judge left on the acceptEdits default
58
+ // edited the review it was grading and was blocked judge_protocol).
59
+ permissionExecution: { field: "permissionMode", executingModes: ["bypassPermissions"], defaultMode: "acceptEdits", readOnlyModes: ["plan"] },
57
60
 
58
61
  /** @param {import("../index.mjs").HarnessRuntime} runtime @returns {string} */
59
62
  executable(runtime) {
@@ -157,6 +157,17 @@ export function resolvePermissionExecution(runtime) {
157
157
  return { executes: policy.executingModes.includes(mode), field: policy.field, mode, executingModes: policy.executingModes };
158
158
  }
159
159
 
160
+ /**
161
+ * The mode in which a runtime's harness cannot write its workspace, or `null`
162
+ * when the harness declares none.
163
+ *
164
+ * @param {{harness: string}} runtime
165
+ * @returns {string|null}
166
+ */
167
+ export function readOnlyModeOf(runtime) {
168
+ return getHarness(runtime.harness).permissionExecution?.readOnlyModes?.[0] ?? null;
169
+ }
170
+
160
171
  /**
161
172
  * Whether a runtime can write its workspace. False only in a mode its adapter
162
173
  * declares read-only; RM-058 measured a codex reviewer under `read-only`
@@ -236,6 +236,25 @@ function readFilesForKind(kind, inputs) {
236
236
  return [/** @type {string} */ (inputs.specPath)];
237
237
  }
238
238
 
239
+ /**
240
+ * The wall clock one planning stage gets, from the reasoning effort of the
241
+ * runtime that runs it. A stage is a single long turn over the whole plan, so
242
+ * the contract default (2400 s) fits a draft at ordinary effort but not a
243
+ * revise at high effort. Measured 2026-09-27: Opus 5.5 at `xhigh` revising
244
+ * a 20-node plan against 13 findings was still reasoning when the 2400 s wall
245
+ * clock ended the stage, and the pipeline, which cannot resume a stage, lost
246
+ * the draft and review it had already paid for.
247
+ *
248
+ * @param {Record<string, unknown>|undefined} runtime
249
+ * @returns {number}
250
+ */
251
+ export function planningStageTimeoutSec(runtime) {
252
+ const reasoning = runtime?.reasoning;
253
+ if (reasoning === "xhigh" || reasoning === "max") return 7200;
254
+ if (reasoning === "high") return 3600;
255
+ return 2400;
256
+ }
257
+
239
258
  /**
240
259
  * Build one of the planning pipeline's one-node discovery contracts. Pure:
241
260
  * no file is read or written, and no model is invoked. The returned object is
@@ -273,10 +292,13 @@ export function buildPlanningContract(kind, inputs) {
273
292
  verification: [],
274
293
  };
275
294
 
295
+ const stageRuntime = runtimeId === undefined ? undefined : /** @type {Record<string, unknown>|undefined} */ (inputs.runtimes[runtimeId]);
296
+
276
297
  return {
277
298
  schemaVersion: PROTOCOL_SCHEMA_VERSION,
278
299
  contractVersion: CONTRACT_VERSION,
279
300
  id: `${inputs.campaignId}-plan-${inputs.phase}-${kind}-${inputs.n}`,
301
+ timeoutSec: planningStageTimeoutSec(stageRuntime),
280
302
  campaignId: inputs.campaignId,
281
303
  goal: inputs.goal ?? `Plan ${kind} for phase ${inputs.phase}`,
282
304
  cwd: inputs.cwd ?? ".",