@odla-ai/harness 0.10.1 → 0.10.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.
Files changed (38) hide show
  1. package/dist/{chunk-U324RQ4N.js → chunk-CPV7ZVHH.js} +1 -1
  2. package/dist/{chunk-U324RQ4N.js.map → chunk-CPV7ZVHH.js.map} +1 -1
  3. package/dist/{chunk-OZBJNTML.js → chunk-ESLFS3FY.js} +4 -4
  4. package/dist/{chunk-5LRYJKUI.js → chunk-FUDTSAOZ.js} +3 -3
  5. package/dist/{chunk-VDY5V7ZG.js → chunk-JV45JAAW.js} +3 -3
  6. package/dist/chunk-JV45JAAW.js.map +1 -0
  7. package/dist/{chunk-FAN2R3GW.js → chunk-KXFI3WM2.js} +5 -1
  8. package/dist/chunk-KXFI3WM2.js.map +1 -0
  9. package/dist/{chunk-WM34GGTK.js → chunk-TMBYA5JX.js} +107 -13
  10. package/dist/chunk-TMBYA5JX.js.map +1 -0
  11. package/dist/cli.cjs +1 -1
  12. package/dist/cli.cjs.map +1 -1
  13. package/dist/cli.js +4 -4
  14. package/dist/code-runtime-cli.cjs +179 -79
  15. package/dist/code-runtime-cli.cjs.map +1 -1
  16. package/dist/code-runtime-cli.js +5 -5
  17. package/dist/index.cjs +5 -1
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +2 -2
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +3 -3
  22. package/dist/node.cjs +183 -83
  23. package/dist/node.cjs.map +1 -1
  24. package/dist/node.d.cts +11 -2
  25. package/dist/node.d.ts +11 -2
  26. package/dist/node.js +6 -6
  27. package/dist/testing.cjs.map +1 -1
  28. package/dist/testing.d.cts +1 -1
  29. package/dist/testing.d.ts +1 -1
  30. package/dist/testing.js +1 -1
  31. package/dist/{types-_8y8vBDI.d.ts → types-ocy70g_p.d.cts} +1 -1
  32. package/dist/{types-_8y8vBDI.d.cts → types-ocy70g_p.d.ts} +1 -1
  33. package/package.json +1 -1
  34. package/dist/chunk-FAN2R3GW.js.map +0 -1
  35. package/dist/chunk-VDY5V7ZG.js.map +0 -1
  36. package/dist/chunk-WM34GGTK.js.map +0 -1
  37. /package/dist/{chunk-OZBJNTML.js.map → chunk-ESLFS3FY.js.map} +0 -0
  38. /package/dist/{chunk-5LRYJKUI.js.map → chunk-FUDTSAOZ.js.map} +0 -0
package/dist/node.cjs CHANGED
@@ -156,7 +156,7 @@ function parseAgentOutput(line) {
156
156
  if (message2.type === "tool.request") {
157
157
  const input = record(message2.input);
158
158
  const tool = String(message2.tool);
159
- if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.run_recipe"].includes(tool)) {
159
+ if (!input || !["sandbox.read", "sandbox.apply_patch", "sandbox.edit", "sandbox.run_recipe"].includes(tool)) {
160
160
  throw new HarnessProtocolError("tool.request requires a registered tool and object input");
161
161
  }
162
162
  return {
@@ -2658,10 +2658,12 @@ Prefer these over listing the tree \u2014 a full listing of a real repository is
2658
2658
  of thousands of tokens and you will carry it for the rest of the session.
2659
2659
 
2660
2660
  Then odla_search for a literal string, odla_read for a bounded range, and
2661
- odla_apply_git_diff to change something. A patch must start with
2662
- "diff --git a/<path> b/<path>", include matching "---" and "+++" headers and
2663
- numbered "@@" hunks with at least one line of surrounding context, and must never
2664
- use "*** Begin Patch" wrappers.
2661
+ odla_edit_file to change an existing file: give it the exact current text
2662
+ (copied verbatim from odla_read) and the text that replaces it; it must match
2663
+ once. Use odla_apply_git_diff only to create or delete whole files. A patch must
2664
+ start with "diff --git a/<path> b/<path>", include matching "---" and "+++"
2665
+ headers and numbered "@@" hunks with at least one line of surrounding context,
2666
+ and must never use "*** Begin Patch" wrappers.
2665
2667
 
2666
2668
  The workspace, model, and tool effects are controlled by the host broker.
2667
2669
  Never claim a build or test passed unless odla_run_recipe returned that result.`;
@@ -2721,6 +2723,21 @@ function codeSkill(opts) {
2721
2723
  },
2722
2724
  handler: (input, ctx) => call("sandbox.apply_patch", input, ctx.signal)
2723
2725
  };
2726
+ const editFile = {
2727
+ name: "odla_edit_file",
2728
+ description: "Change an existing file by replacing one exact stretch of its current text with new text. oldText must match the file exactly once (copy it verbatim from odla_read, including indentation and blank lines; widen it if it would match more than once). Prefer this over odla_apply_git_diff for every change to an existing file; use odla_apply_git_diff only to create or delete whole files.",
2729
+ inputSchema: {
2730
+ type: "object",
2731
+ required: ["path", "oldText", "newText"],
2732
+ properties: {
2733
+ path: { type: "string", minLength: 1, maxLength: 1024 },
2734
+ oldText: { type: "string", minLength: 1, maxLength: 262144 },
2735
+ newText: { type: "string", maxLength: 262144 }
2736
+ },
2737
+ additionalProperties: false
2738
+ },
2739
+ handler: (input, ctx) => call("sandbox.edit", input, ctx.signal)
2740
+ };
2724
2741
  const runRecipe = {
2725
2742
  name: "odla_run_recipe",
2726
2743
  description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
@@ -2807,8 +2824,9 @@ function codeSkill(opts) {
2807
2824
  true
2808
2825
  )
2809
2826
  ];
2810
- const effects = opts.readOnly ? [] : [applyPatch, runRecipe];
2811
- const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2827
+ const runsRecipes = opts.recipes ?? !opts.readOnly;
2828
+ const effects = [...opts.readOnly ? [] : [applyPatch], ...runsRecipes ? [runRecipe] : []];
2829
+ const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2812
2830
  return { name: "code", tools };
2813
2831
  }
2814
2832
 
@@ -2865,6 +2883,7 @@ async function runCodeAgentAttempt(options) {
2865
2883
  model: "brokered",
2866
2884
  surface,
2867
2885
  ...options.readOnly === void 0 ? {} : { readOnly: options.readOnly },
2886
+ ...options.recipes === void 0 ? {} : { recipes: options.recipes },
2868
2887
  ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
2869
2888
  ...options.extraSkills ? { extraSkills: options.extraSkills } : {},
2870
2889
  ...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
@@ -3098,6 +3117,10 @@ function codeToolRequestPresentation(request) {
3098
3117
  if (request.tool === "sandbox.apply_patch") {
3099
3118
  return { kind: "patch", ...patchStats(input.patch) };
3100
3119
  }
3120
+ if (request.tool === "sandbox.edit") {
3121
+ const lines = (value) => typeof value === "string" ? value.slice(0, 262144).split("\n").length : 0;
3122
+ return { kind: "patch", additions: lines(input.newText), deletions: lines(input.oldText) };
3123
+ }
3101
3124
  const recipeId = text(input.recipeId, 120);
3102
3125
  return recipeId ? { kind: "recipe", recipeId } : void 0;
3103
3126
  }
@@ -3225,6 +3248,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
3225
3248
  authority: "authority",
3226
3249
  patch: "payload"
3227
3250
  });
3251
+ var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
3252
+ workspace: "destination",
3253
+ authority: "authority",
3254
+ patch: "payload"
3255
+ });
3228
3256
  var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
3229
3257
  workspace: "destination",
3230
3258
  authority: "authority",
@@ -3292,6 +3320,14 @@ function createCodePolicyGate(options) {
3292
3320
  patch: { role: "payload", value: patch2 }
3293
3321
  }, []);
3294
3322
  },
3323
+ edit: async (input) => {
3324
+ const base = await environment(input, options, "sandbox.edit");
3325
+ const patch2 = unsafe(base, input.patch, "patch");
3326
+ return authorize(input, options, base, EDIT, {
3327
+ ...base.fixedArgs,
3328
+ patch: { role: "payload", value: patch2 }
3329
+ }, []);
3330
+ },
3295
3331
  recipe: async (input) => {
3296
3332
  const base = await environment(input, options, "sandbox.run_recipe");
3297
3333
  const conversions = await conversionRegistry([
@@ -3436,13 +3472,134 @@ function response(request, ok, content, details) {
3436
3472
  return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
3437
3473
  }
3438
3474
 
3475
+ // src/code-tool-edit.ts
3476
+ var import_promises12 = require("fs/promises");
3477
+
3478
+ // src/code-tool-graph.ts
3479
+ var import_promises11 = require("fs/promises");
3480
+ var import_node_path11 = require("path");
3481
+ var import_graph = require("@odla-ai/graph");
3482
+ var import_code4 = require("@odla-ai/graph/code");
3483
+ var cache = /* @__PURE__ */ new Map();
3484
+ function workspaceGraphs(workspaceDir, paths2) {
3485
+ const existing = cache.get(workspaceDir);
3486
+ if (existing) return existing;
3487
+ const read2 = (path) => (0, import_promises11.readFile)((0, import_node_path11.join)(workspaceDir, path), "utf8");
3488
+ const built = (async () => ({
3489
+ // No knownTables: a staged workspace may not carry migrations, and a filter
3490
+ // that silently drops every table is worse than an unfiltered one. Callers
3491
+ // with ground truth should build the graph themselves.
3492
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
3493
+ }))();
3494
+ cache.set(workspaceDir, built);
3495
+ return built;
3496
+ }
3497
+ function forgetWorkspaceGraphs(workspaceDir) {
3498
+ cache.delete(workspaceDir);
3499
+ }
3500
+ var shortId = (id) => id.slice(id.indexOf(":") + 1);
3501
+ function renderOverview(graphs, prefix) {
3502
+ const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
3503
+ if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
3504
+ const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
3505
+ const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
3506
+ return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
3507
+ }
3508
+ function renderWhereIs(graphs, symbol) {
3509
+ const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
3510
+ path: shortId(id),
3511
+ pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
3512
+ dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
3513
+ })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
3514
+ if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
3515
+ return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
3516
+ }
3517
+ function renderWhoImports(graphs, path) {
3518
+ const id = (0, import_graph.nodeId)(import_code4.FILE, path);
3519
+ const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
3520
+ if (importers.length === 0) {
3521
+ return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
3522
+ }
3523
+ return importers.slice(0, 40).map(shortId).sort().join("\n");
3524
+ }
3525
+ function renderWhoTouches(graphs, query) {
3526
+ const needle = query.toLowerCase();
3527
+ const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
3528
+ if (hits.length === 0) return `No table or namespace matching "${query}".`;
3529
+ return hits.map((hit) => {
3530
+ const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
3531
+ return [
3532
+ `${hit.name} (${hit.kind})`,
3533
+ ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
3534
+ ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
3535
+ ].join("\n");
3536
+ }).join("\n\n");
3537
+ }
3538
+
3539
+ // src/code-tool-edit.ts
3540
+ var MAX_TEXT_BYTES = 256 * 1024;
3541
+ function editAsDiff(path, oldText, newText) {
3542
+ const removed = oldText.split("\n").map((line) => `-${line}`);
3543
+ const added = newText.split("\n").map((line) => `+${line}`);
3544
+ return `diff --git a/${path} b/${path}
3545
+ --- a/${path}
3546
+ +++ b/${path}
3547
+ @@ -1,${removed.length} +1,${added.length} @@
3548
+ ${[...removed, ...added].join("\n")}
3549
+ `;
3550
+ }
3551
+ function occurrences(haystack, needle) {
3552
+ let count = 0;
3553
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
3554
+ return count;
3555
+ }
3556
+ async function editCodeFile(workspaceDir, path, oldText, newText) {
3557
+ if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
3558
+ if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
3559
+ throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
3560
+ }
3561
+ if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
3562
+ const target = resolveCodePath(workspaceDir, path);
3563
+ const current = await (0, import_promises12.readFile)(target, "utf8");
3564
+ const found = occurrences(current, oldText);
3565
+ if (found === 0) {
3566
+ throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
3567
+ }
3568
+ if (found > 1) {
3569
+ throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
3570
+ }
3571
+ const at = current.indexOf(oldText);
3572
+ await (0, import_promises12.writeFile)(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
3573
+ return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
3574
+ }
3575
+ async function edit(context, request, options, policy, registry) {
3576
+ exactKeys(request.input, ["path", "oldText", "newText"]);
3577
+ const path = stringField(request.input, "path");
3578
+ const oldText = stringField(request.input, "oldText");
3579
+ const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
3580
+ const paths2 = await registry.files(context.workspaceDir);
3581
+ if (!paths2.includes(path)) {
3582
+ throw new TypeError(`no such file in the staged workspace: "${path}". Use sandbox.overview, sandbox.where_is or sandbox.search to find the correct path.`);
3583
+ }
3584
+ if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
3585
+ throw new TypeError("edit targets a read-only reference source");
3586
+ }
3587
+ const patch2 = editAsDiff(path, oldText, newText);
3588
+ const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
3589
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3590
+ const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
3591
+ registry.invalidate(context.workspaceDir);
3592
+ forgetWorkspaceGraphs(context.workspaceDir);
3593
+ return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
3594
+ }
3595
+
3439
3596
  // src/code-tool-reads.ts
3440
- var import_promises13 = require("fs/promises");
3597
+ var import_promises14 = require("fs/promises");
3441
3598
 
3442
3599
  // src/code-tool-discovery.ts
3443
3600
  var import_node_child_process6 = require("child_process");
3444
- var import_promises11 = require("fs/promises");
3445
- var import_node_path11 = require("path");
3601
+ var import_promises13 = require("fs/promises");
3602
+ var import_node_path12 = require("path");
3446
3603
  var DEFAULT_MAX_FILES = 2e4;
3447
3604
  var DEFAULT_MAX_RESULTS = 100;
3448
3605
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
@@ -3467,13 +3624,13 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
3467
3624
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
3468
3625
  const paths2 = [];
3469
3626
  const walk = async (directory) => {
3470
- for (const entry of await (0, import_promises11.readdir)(directory, { withFileTypes: true })) {
3627
+ for (const entry of await (0, import_promises13.readdir)(directory, { withFileTypes: true })) {
3471
3628
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
3472
3629
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
3473
- const target = (0, import_node_path11.resolve)(directory, entry.name);
3630
+ const target = (0, import_node_path12.resolve)(directory, entry.name);
3474
3631
  if (entry.isDirectory()) await walk(target);
3475
3632
  else if (entry.isFile()) {
3476
- const path = (0, import_node_path11.relative)(root, target).split("\\").join("/");
3633
+ const path = (0, import_node_path12.relative)(root, target).split("\\").join("/");
3477
3634
  try {
3478
3635
  validateRelativePath(path);
3479
3636
  } catch {
@@ -3484,7 +3641,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
3484
3641
  }
3485
3642
  }
3486
3643
  };
3487
- await walk((0, import_node_path11.resolve)(root));
3644
+ await walk((0, import_node_path12.resolve)(root));
3488
3645
  return paths2.sort();
3489
3646
  }
3490
3647
  function listWorkspace(paths2, options = {}) {
@@ -3598,7 +3755,7 @@ async function fallbackSearch(root, scoped, options) {
3598
3755
  if (matches.length >= options.maxResults) break;
3599
3756
  let source;
3600
3757
  try {
3601
- source = await (0, import_promises11.readFile)((0, import_node_path11.resolve)(root, path));
3758
+ source = await (0, import_promises13.readFile)((0, import_node_path12.resolve)(root, path));
3602
3759
  } catch {
3603
3760
  continue;
3604
3761
  }
@@ -3615,67 +3772,6 @@ async function fallbackSearch(root, scoped, options) {
3615
3772
  return matches;
3616
3773
  }
3617
3774
 
3618
- // src/code-tool-graph.ts
3619
- var import_promises12 = require("fs/promises");
3620
- var import_node_path12 = require("path");
3621
- var import_graph = require("@odla-ai/graph");
3622
- var import_code4 = require("@odla-ai/graph/code");
3623
- var cache = /* @__PURE__ */ new Map();
3624
- function workspaceGraphs(workspaceDir, paths2) {
3625
- const existing = cache.get(workspaceDir);
3626
- if (existing) return existing;
3627
- const read2 = (path) => (0, import_promises12.readFile)((0, import_node_path12.join)(workspaceDir, path), "utf8");
3628
- const built = (async () => ({
3629
- // No knownTables: a staged workspace may not carry migrations, and a filter
3630
- // that silently drops every table is worse than an unfiltered one. Callers
3631
- // with ground truth should build the graph themselves.
3632
- graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
3633
- }))();
3634
- cache.set(workspaceDir, built);
3635
- return built;
3636
- }
3637
- function forgetWorkspaceGraphs(workspaceDir) {
3638
- cache.delete(workspaceDir);
3639
- }
3640
- var shortId = (id) => id.slice(id.indexOf(":") + 1);
3641
- function renderOverview(graphs, prefix) {
3642
- const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
3643
- if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
3644
- const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
3645
- const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
3646
- return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
3647
- }
3648
- function renderWhereIs(graphs, symbol) {
3649
- const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
3650
- path: shortId(id),
3651
- pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
3652
- dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
3653
- })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
3654
- if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
3655
- return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
3656
- }
3657
- function renderWhoImports(graphs, path) {
3658
- const id = (0, import_graph.nodeId)(import_code4.FILE, path);
3659
- const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
3660
- if (importers.length === 0) {
3661
- return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
3662
- }
3663
- return importers.slice(0, 40).map(shortId).sort().join("\n");
3664
- }
3665
- function renderWhoTouches(graphs, query) {
3666
- const needle = query.toLowerCase();
3667
- const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
3668
- if (hits.length === 0) return `No table or namespace matching "${query}".`;
3669
- return hits.map((hit) => {
3670
- const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
3671
- return [
3672
- `${hit.name} (${hit.kind})`,
3673
- ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
3674
- ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
3675
- ].join("\n");
3676
- }).join("\n\n");
3677
- }
3678
-
3679
3775
  // src/code-tool-reads.ts
3680
3776
  var GRAPH_TOOLS = /* @__PURE__ */ new Set([
3681
3777
  "sandbox.overview",
@@ -3698,11 +3794,11 @@ async function read(context, request, options, policy, registry) {
3698
3794
  const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
3699
3795
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3700
3796
  const target = resolveCodePath(context.workspaceDir, path);
3701
- const info = await (0, import_promises13.stat)(target);
3797
+ const info = await (0, import_promises14.stat)(target);
3702
3798
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
3703
3799
  throw new TypeError("file is not a bounded regular source file");
3704
3800
  }
3705
- const source = await (0, import_promises13.readFile)(target);
3801
+ const source = await (0, import_promises14.readFile)(target);
3706
3802
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
3707
3803
  const lines = source.toString("utf8").split("\n");
3708
3804
  const content = lines.slice(startLine - 1, endLine).join("\n");
@@ -3817,6 +3913,7 @@ async function route(context, request, options, recipes, policy, registry) {
3817
3913
  if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
3818
3914
  if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
3819
3915
  if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
3916
+ if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
3820
3917
  return await recipe(context, request, options, recipes, policy);
3821
3918
  } catch (reason) {
3822
3919
  return response(request, false, toolFailureMessage(reason));
@@ -4526,6 +4623,9 @@ var TheseusRuntimeEngine = class {
4526
4623
  prompt: metadata.prompt,
4527
4624
  signal: active.abort.signal,
4528
4625
  readOnly: metadata.readOnly,
4626
+ // A review-role session is read-only and still runs the proof; a planner
4627
+ // marked read-only by its payload runs nothing.
4628
+ recipes: metadata.role === "review" || !metadata.readOnly,
4529
4629
  recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
4530
4630
  ...extraSkills.length ? { extraSkills } : {}
4531
4631
  });
@@ -4857,7 +4957,7 @@ function chooseStrategy(signals = {}) {
4857
4957
  var feedbackIsActionable = actionable;
4858
4958
 
4859
4959
  // src/code-recipe-dependencies.ts
4860
- var import_promises14 = require("fs/promises");
4960
+ var import_promises15 = require("fs/promises");
4861
4961
  var import_node_path13 = require("path");
4862
4962
  var RESERVED_MOUNTS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
4863
4963
  function withRecipeDependencies(executor, dependencies) {
@@ -4873,21 +4973,21 @@ function withRecipeDependencies(executor, dependencies) {
4873
4973
  const target = (0, import_node_path13.join)(input.workspaceDir, mountAs);
4874
4974
  let linked = false;
4875
4975
  try {
4876
- const existing = await (0, import_promises14.lstat)(target).catch(() => null);
4976
+ const existing = await (0, import_promises15.lstat)(target).catch(() => null);
4877
4977
  if (!existing) {
4878
- await (0, import_promises14.symlink)(dependencies.source, target, "dir");
4978
+ await (0, import_promises15.symlink)(dependencies.source, target, "dir");
4879
4979
  linked = true;
4880
4980
  }
4881
4981
  return await executor.run(input);
4882
4982
  } finally {
4883
- if (linked) await (0, import_promises14.rm)(target, { force: true, recursive: false }).catch(() => void 0);
4983
+ if (linked) await (0, import_promises15.rm)(target, { force: true, recursive: false }).catch(() => void 0);
4884
4984
  }
4885
4985
  }
4886
4986
  };
4887
4987
  }
4888
4988
  async function installedDependencies(repoRoot) {
4889
4989
  const source = (0, import_node_path13.join)(repoRoot, "node_modules");
4890
- const info = await (0, import_promises14.lstat)(source).catch(() => null);
4990
+ const info = await (0, import_promises15.lstat)(source).catch(() => null);
4891
4991
  return info?.isDirectory() ? { source } : null;
4892
4992
  }
4893
4993
  // Annotate the CommonJS export names for ESM import in node: