@odla-ai/harness 0.10.1 → 0.10.2

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-VIZALA6O.js} +101 -12
  10. package/dist/chunk-VIZALA6O.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 +173 -78
  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 +177 -82
  23. package/dist/node.cjs.map +1 -1
  24. package/dist/node.d.cts +2 -2
  25. package/dist/node.d.ts +2 -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(", ")}.` : ""}`,
@@ -2808,7 +2825,7 @@ function codeSkill(opts) {
2808
2825
  )
2809
2826
  ];
2810
2827
  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];
2828
+ const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2812
2829
  return { name: "code", tools };
2813
2830
  }
2814
2831
 
@@ -3098,6 +3115,10 @@ function codeToolRequestPresentation(request) {
3098
3115
  if (request.tool === "sandbox.apply_patch") {
3099
3116
  return { kind: "patch", ...patchStats(input.patch) };
3100
3117
  }
3118
+ if (request.tool === "sandbox.edit") {
3119
+ const lines = (value) => typeof value === "string" ? value.slice(0, 262144).split("\n").length : 0;
3120
+ return { kind: "patch", additions: lines(input.newText), deletions: lines(input.oldText) };
3121
+ }
3101
3122
  const recipeId = text(input.recipeId, 120);
3102
3123
  return recipeId ? { kind: "recipe", recipeId } : void 0;
3103
3124
  }
@@ -3225,6 +3246,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
3225
3246
  authority: "authority",
3226
3247
  patch: "payload"
3227
3248
  });
3249
+ var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
3250
+ workspace: "destination",
3251
+ authority: "authority",
3252
+ patch: "payload"
3253
+ });
3228
3254
  var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
3229
3255
  workspace: "destination",
3230
3256
  authority: "authority",
@@ -3292,6 +3318,14 @@ function createCodePolicyGate(options) {
3292
3318
  patch: { role: "payload", value: patch2 }
3293
3319
  }, []);
3294
3320
  },
3321
+ edit: async (input) => {
3322
+ const base = await environment(input, options, "sandbox.edit");
3323
+ const patch2 = unsafe(base, input.patch, "patch");
3324
+ return authorize(input, options, base, EDIT, {
3325
+ ...base.fixedArgs,
3326
+ patch: { role: "payload", value: patch2 }
3327
+ }, []);
3328
+ },
3295
3329
  recipe: async (input) => {
3296
3330
  const base = await environment(input, options, "sandbox.run_recipe");
3297
3331
  const conversions = await conversionRegistry([
@@ -3436,13 +3470,134 @@ function response(request, ok, content, details) {
3436
3470
  return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
3437
3471
  }
3438
3472
 
3473
+ // src/code-tool-edit.ts
3474
+ var import_promises12 = require("fs/promises");
3475
+
3476
+ // src/code-tool-graph.ts
3477
+ var import_promises11 = require("fs/promises");
3478
+ var import_node_path11 = require("path");
3479
+ var import_graph = require("@odla-ai/graph");
3480
+ var import_code4 = require("@odla-ai/graph/code");
3481
+ var cache = /* @__PURE__ */ new Map();
3482
+ function workspaceGraphs(workspaceDir, paths2) {
3483
+ const existing = cache.get(workspaceDir);
3484
+ if (existing) return existing;
3485
+ const read2 = (path) => (0, import_promises11.readFile)((0, import_node_path11.join)(workspaceDir, path), "utf8");
3486
+ const built = (async () => ({
3487
+ // No knownTables: a staged workspace may not carry migrations, and a filter
3488
+ // that silently drops every table is worse than an unfiltered one. Callers
3489
+ // with ground truth should build the graph themselves.
3490
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
3491
+ }))();
3492
+ cache.set(workspaceDir, built);
3493
+ return built;
3494
+ }
3495
+ function forgetWorkspaceGraphs(workspaceDir) {
3496
+ cache.delete(workspaceDir);
3497
+ }
3498
+ var shortId = (id) => id.slice(id.indexOf(":") + 1);
3499
+ function renderOverview(graphs, prefix) {
3500
+ const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
3501
+ if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
3502
+ const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
3503
+ const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
3504
+ return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
3505
+ }
3506
+ function renderWhereIs(graphs, symbol) {
3507
+ const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
3508
+ path: shortId(id),
3509
+ pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
3510
+ dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
3511
+ })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
3512
+ if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
3513
+ return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
3514
+ }
3515
+ function renderWhoImports(graphs, path) {
3516
+ const id = (0, import_graph.nodeId)(import_code4.FILE, path);
3517
+ const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
3518
+ if (importers.length === 0) {
3519
+ return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
3520
+ }
3521
+ return importers.slice(0, 40).map(shortId).sort().join("\n");
3522
+ }
3523
+ function renderWhoTouches(graphs, query) {
3524
+ const needle = query.toLowerCase();
3525
+ const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
3526
+ if (hits.length === 0) return `No table or namespace matching "${query}".`;
3527
+ return hits.map((hit) => {
3528
+ const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
3529
+ return [
3530
+ `${hit.name} (${hit.kind})`,
3531
+ ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
3532
+ ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
3533
+ ].join("\n");
3534
+ }).join("\n\n");
3535
+ }
3536
+
3537
+ // src/code-tool-edit.ts
3538
+ var MAX_TEXT_BYTES = 256 * 1024;
3539
+ function editAsDiff(path, oldText, newText) {
3540
+ const removed = oldText.split("\n").map((line) => `-${line}`);
3541
+ const added = newText.split("\n").map((line) => `+${line}`);
3542
+ return `diff --git a/${path} b/${path}
3543
+ --- a/${path}
3544
+ +++ b/${path}
3545
+ @@ -1,${removed.length} +1,${added.length} @@
3546
+ ${[...removed, ...added].join("\n")}
3547
+ `;
3548
+ }
3549
+ function occurrences(haystack, needle) {
3550
+ let count = 0;
3551
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
3552
+ return count;
3553
+ }
3554
+ async function editCodeFile(workspaceDir, path, oldText, newText) {
3555
+ if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
3556
+ if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
3557
+ throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
3558
+ }
3559
+ if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
3560
+ const target = resolveCodePath(workspaceDir, path);
3561
+ const current = await (0, import_promises12.readFile)(target, "utf8");
3562
+ const found = occurrences(current, oldText);
3563
+ if (found === 0) {
3564
+ throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
3565
+ }
3566
+ if (found > 1) {
3567
+ throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
3568
+ }
3569
+ const at = current.indexOf(oldText);
3570
+ await (0, import_promises12.writeFile)(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
3571
+ return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
3572
+ }
3573
+ async function edit(context, request, options, policy, registry) {
3574
+ exactKeys(request.input, ["path", "oldText", "newText"]);
3575
+ const path = stringField(request.input, "path");
3576
+ const oldText = stringField(request.input, "oldText");
3577
+ const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
3578
+ const paths2 = await registry.files(context.workspaceDir);
3579
+ if (!paths2.includes(path)) {
3580
+ 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.`);
3581
+ }
3582
+ if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
3583
+ throw new TypeError("edit targets a read-only reference source");
3584
+ }
3585
+ const patch2 = editAsDiff(path, oldText, newText);
3586
+ const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
3587
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3588
+ const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
3589
+ registry.invalidate(context.workspaceDir);
3590
+ forgetWorkspaceGraphs(context.workspaceDir);
3591
+ return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
3592
+ }
3593
+
3439
3594
  // src/code-tool-reads.ts
3440
- var import_promises13 = require("fs/promises");
3595
+ var import_promises14 = require("fs/promises");
3441
3596
 
3442
3597
  // src/code-tool-discovery.ts
3443
3598
  var import_node_child_process6 = require("child_process");
3444
- var import_promises11 = require("fs/promises");
3445
- var import_node_path11 = require("path");
3599
+ var import_promises13 = require("fs/promises");
3600
+ var import_node_path12 = require("path");
3446
3601
  var DEFAULT_MAX_FILES = 2e4;
3447
3602
  var DEFAULT_MAX_RESULTS = 100;
3448
3603
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
@@ -3467,13 +3622,13 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
3467
3622
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
3468
3623
  const paths2 = [];
3469
3624
  const walk = async (directory) => {
3470
- for (const entry of await (0, import_promises11.readdir)(directory, { withFileTypes: true })) {
3625
+ for (const entry of await (0, import_promises13.readdir)(directory, { withFileTypes: true })) {
3471
3626
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
3472
3627
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
3473
- const target = (0, import_node_path11.resolve)(directory, entry.name);
3628
+ const target = (0, import_node_path12.resolve)(directory, entry.name);
3474
3629
  if (entry.isDirectory()) await walk(target);
3475
3630
  else if (entry.isFile()) {
3476
- const path = (0, import_node_path11.relative)(root, target).split("\\").join("/");
3631
+ const path = (0, import_node_path12.relative)(root, target).split("\\").join("/");
3477
3632
  try {
3478
3633
  validateRelativePath(path);
3479
3634
  } catch {
@@ -3484,7 +3639,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
3484
3639
  }
3485
3640
  }
3486
3641
  };
3487
- await walk((0, import_node_path11.resolve)(root));
3642
+ await walk((0, import_node_path12.resolve)(root));
3488
3643
  return paths2.sort();
3489
3644
  }
3490
3645
  function listWorkspace(paths2, options = {}) {
@@ -3598,7 +3753,7 @@ async function fallbackSearch(root, scoped, options) {
3598
3753
  if (matches.length >= options.maxResults) break;
3599
3754
  let source;
3600
3755
  try {
3601
- source = await (0, import_promises11.readFile)((0, import_node_path11.resolve)(root, path));
3756
+ source = await (0, import_promises13.readFile)((0, import_node_path12.resolve)(root, path));
3602
3757
  } catch {
3603
3758
  continue;
3604
3759
  }
@@ -3615,67 +3770,6 @@ async function fallbackSearch(root, scoped, options) {
3615
3770
  return matches;
3616
3771
  }
3617
3772
 
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
3773
  // src/code-tool-reads.ts
3680
3774
  var GRAPH_TOOLS = /* @__PURE__ */ new Set([
3681
3775
  "sandbox.overview",
@@ -3698,11 +3792,11 @@ async function read(context, request, options, policy, registry) {
3698
3792
  const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
3699
3793
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3700
3794
  const target = resolveCodePath(context.workspaceDir, path);
3701
- const info = await (0, import_promises13.stat)(target);
3795
+ const info = await (0, import_promises14.stat)(target);
3702
3796
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
3703
3797
  throw new TypeError("file is not a bounded regular source file");
3704
3798
  }
3705
- const source = await (0, import_promises13.readFile)(target);
3799
+ const source = await (0, import_promises14.readFile)(target);
3706
3800
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
3707
3801
  const lines = source.toString("utf8").split("\n");
3708
3802
  const content = lines.slice(startLine - 1, endLine).join("\n");
@@ -3817,6 +3911,7 @@ async function route(context, request, options, recipes, policy, registry) {
3817
3911
  if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
3818
3912
  if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
3819
3913
  if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
3914
+ if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
3820
3915
  return await recipe(context, request, options, recipes, policy);
3821
3916
  } catch (reason) {
3822
3917
  return response(request, false, toolFailureMessage(reason));
@@ -4857,7 +4952,7 @@ function chooseStrategy(signals = {}) {
4857
4952
  var feedbackIsActionable = actionable;
4858
4953
 
4859
4954
  // src/code-recipe-dependencies.ts
4860
- var import_promises14 = require("fs/promises");
4955
+ var import_promises15 = require("fs/promises");
4861
4956
  var import_node_path13 = require("path");
4862
4957
  var RESERVED_MOUNTS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
4863
4958
  function withRecipeDependencies(executor, dependencies) {
@@ -4873,21 +4968,21 @@ function withRecipeDependencies(executor, dependencies) {
4873
4968
  const target = (0, import_node_path13.join)(input.workspaceDir, mountAs);
4874
4969
  let linked = false;
4875
4970
  try {
4876
- const existing = await (0, import_promises14.lstat)(target).catch(() => null);
4971
+ const existing = await (0, import_promises15.lstat)(target).catch(() => null);
4877
4972
  if (!existing) {
4878
- await (0, import_promises14.symlink)(dependencies.source, target, "dir");
4973
+ await (0, import_promises15.symlink)(dependencies.source, target, "dir");
4879
4974
  linked = true;
4880
4975
  }
4881
4976
  return await executor.run(input);
4882
4977
  } finally {
4883
- if (linked) await (0, import_promises14.rm)(target, { force: true, recursive: false }).catch(() => void 0);
4978
+ if (linked) await (0, import_promises15.rm)(target, { force: true, recursive: false }).catch(() => void 0);
4884
4979
  }
4885
4980
  }
4886
4981
  };
4887
4982
  }
4888
4983
  async function installedDependencies(repoRoot) {
4889
4984
  const source = (0, import_node_path13.join)(repoRoot, "node_modules");
4890
- const info = await (0, import_promises14.lstat)(source).catch(() => null);
4985
+ const info = await (0, import_promises15.lstat)(source).catch(() => null);
4891
4986
  return info?.isDirectory() ? { source } : null;
4892
4987
  }
4893
4988
  // Annotate the CommonJS export names for ESM import in node: