@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
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/code-runtime-cli.ts
5
5
  var import_node_os4 = require("os");
6
- var import_promises12 = require("fs/promises");
6
+ var import_promises13 = require("fs/promises");
7
7
 
8
8
  // src/code-runtime-client-validation.ts
9
9
  var import_code = require("@odla-ai/camel/code");
@@ -2071,10 +2071,12 @@ Prefer these over listing the tree \u2014 a full listing of a real repository is
2071
2071
  of thousands of tokens and you will carry it for the rest of the session.
2072
2072
 
2073
2073
  Then odla_search for a literal string, odla_read for a bounded range, and
2074
- odla_apply_git_diff to change something. A patch must start with
2075
- "diff --git a/<path> b/<path>", include matching "---" and "+++" headers and
2076
- numbered "@@" hunks with at least one line of surrounding context, and must never
2077
- use "*** Begin Patch" wrappers.
2074
+ odla_edit_file to change an existing file: give it the exact current text
2075
+ (copied verbatim from odla_read) and the text that replaces it; it must match
2076
+ once. Use odla_apply_git_diff only to create or delete whole files. A patch must
2077
+ start with "diff --git a/<path> b/<path>", include matching "---" and "+++"
2078
+ headers and numbered "@@" hunks with at least one line of surrounding context,
2079
+ and must never use "*** Begin Patch" wrappers.
2078
2080
 
2079
2081
  The workspace, model, and tool effects are controlled by the host broker.
2080
2082
  Never claim a build or test passed unless odla_run_recipe returned that result.`;
@@ -2134,6 +2136,21 @@ function codeSkill(opts) {
2134
2136
  },
2135
2137
  handler: (input, ctx) => call("sandbox.apply_patch", input, ctx.signal)
2136
2138
  };
2139
+ const editFile = {
2140
+ name: "odla_edit_file",
2141
+ 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.",
2142
+ inputSchema: {
2143
+ type: "object",
2144
+ required: ["path", "oldText", "newText"],
2145
+ properties: {
2146
+ path: { type: "string", minLength: 1, maxLength: 1024 },
2147
+ oldText: { type: "string", minLength: 1, maxLength: 262144 },
2148
+ newText: { type: "string", maxLength: 262144 }
2149
+ },
2150
+ additionalProperties: false
2151
+ },
2152
+ handler: (input, ctx) => call("sandbox.edit", input, ctx.signal)
2153
+ };
2137
2154
  const runRecipe = {
2138
2155
  name: "odla_run_recipe",
2139
2156
  description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
@@ -2220,8 +2237,9 @@ function codeSkill(opts) {
2220
2237
  true
2221
2238
  )
2222
2239
  ];
2223
- const effects = opts.readOnly ? [] : [applyPatch, runRecipe];
2224
- const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2240
+ const runsRecipes = opts.recipes ?? !opts.readOnly;
2241
+ const effects = [...opts.readOnly ? [] : [applyPatch], ...runsRecipes ? [runRecipe] : []];
2242
+ const tools = opts.surface === "v3" ? [...orientation, searchFiles, read2, ...opts.readOnly ? [] : [editFile], ...effects] : opts.surface === "v2" ? [listFiles, searchFiles, read2, ...effects] : [read2, ...effects];
2225
2243
  return { name: "code", tools };
2226
2244
  }
2227
2245
 
@@ -2278,6 +2296,7 @@ async function runCodeAgentAttempt(options) {
2278
2296
  model: "brokered",
2279
2297
  surface,
2280
2298
  ...options.readOnly === void 0 ? {} : { readOnly: options.readOnly },
2299
+ ...options.recipes === void 0 ? {} : { recipes: options.recipes },
2281
2300
  ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
2282
2301
  ...options.extraSkills ? { extraSkills: options.extraSkills } : {},
2283
2302
  ...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
@@ -2511,6 +2530,10 @@ function codeToolRequestPresentation(request) {
2511
2530
  if (request.tool === "sandbox.apply_patch") {
2512
2531
  return { kind: "patch", ...patchStats(input.patch) };
2513
2532
  }
2533
+ if (request.tool === "sandbox.edit") {
2534
+ const lines = (value) => typeof value === "string" ? value.slice(0, 262144).split("\n").length : 0;
2535
+ return { kind: "patch", additions: lines(input.newText), deletions: lines(input.oldText) };
2536
+ }
2514
2537
  const recipeId = text(input.recipeId, 120);
2515
2538
  return recipeId ? { kind: "recipe", recipeId } : void 0;
2516
2539
  }
@@ -2638,6 +2661,11 @@ var PATCH = descriptor("sandbox.apply_patch", "reversible_mutation", {
2638
2661
  authority: "authority",
2639
2662
  patch: "payload"
2640
2663
  });
2664
+ var EDIT = descriptor("sandbox.edit", "reversible_mutation", {
2665
+ workspace: "destination",
2666
+ authority: "authority",
2667
+ patch: "payload"
2668
+ });
2641
2669
  var RECIPE = descriptor("sandbox.run_recipe", "code_execution", {
2642
2670
  workspace: "destination",
2643
2671
  authority: "authority",
@@ -2705,6 +2733,14 @@ function createCodePolicyGate(options) {
2705
2733
  patch: { role: "payload", value: patch2 }
2706
2734
  }, []);
2707
2735
  },
2736
+ edit: async (input) => {
2737
+ const base = await environment(input, options, "sandbox.edit");
2738
+ const patch2 = unsafe(base, input.patch, "patch");
2739
+ return authorize(input, options, base, EDIT, {
2740
+ ...base.fixedArgs,
2741
+ patch: { role: "payload", value: patch2 }
2742
+ }, []);
2743
+ },
2708
2744
  recipe: async (input) => {
2709
2745
  const base = await environment(input, options, "sandbox.run_recipe");
2710
2746
  const conversions = await conversionRegistry([
@@ -2849,13 +2885,134 @@ function response(request, ok, content, details) {
2849
2885
  return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
2850
2886
  }
2851
2887
 
2888
+ // src/code-tool-edit.ts
2889
+ var import_promises10 = require("fs/promises");
2890
+
2891
+ // src/code-tool-graph.ts
2892
+ var import_promises9 = require("fs/promises");
2893
+ var import_node_path10 = require("path");
2894
+ var import_graph = require("@odla-ai/graph");
2895
+ var import_code4 = require("@odla-ai/graph/code");
2896
+ var cache = /* @__PURE__ */ new Map();
2897
+ function workspaceGraphs(workspaceDir, paths2) {
2898
+ const existing = cache.get(workspaceDir);
2899
+ if (existing) return existing;
2900
+ const read2 = (path) => (0, import_promises9.readFile)((0, import_node_path10.join)(workspaceDir, path), "utf8");
2901
+ const built = (async () => ({
2902
+ // No knownTables: a staged workspace may not carry migrations, and a filter
2903
+ // that silently drops every table is worse than an unfiltered one. Callers
2904
+ // with ground truth should build the graph themselves.
2905
+ graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2906
+ }))();
2907
+ cache.set(workspaceDir, built);
2908
+ return built;
2909
+ }
2910
+ function forgetWorkspaceGraphs(workspaceDir) {
2911
+ cache.delete(workspaceDir);
2912
+ }
2913
+ var shortId = (id) => id.slice(id.indexOf(":") + 1);
2914
+ function renderOverview(graphs, prefix) {
2915
+ const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
2916
+ if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
2917
+ const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
2918
+ const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
2919
+ return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
2920
+ }
2921
+ function renderWhereIs(graphs, symbol) {
2922
+ const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
2923
+ path: shortId(id),
2924
+ pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
2925
+ dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
2926
+ })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
2927
+ if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
2928
+ return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
2929
+ }
2930
+ function renderWhoImports(graphs, path) {
2931
+ const id = (0, import_graph.nodeId)(import_code4.FILE, path);
2932
+ const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
2933
+ if (importers.length === 0) {
2934
+ return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
2935
+ }
2936
+ return importers.slice(0, 40).map(shortId).sort().join("\n");
2937
+ }
2938
+ function renderWhoTouches(graphs, query) {
2939
+ const needle = query.toLowerCase();
2940
+ const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
2941
+ if (hits.length === 0) return `No table or namespace matching "${query}".`;
2942
+ return hits.map((hit) => {
2943
+ const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
2944
+ return [
2945
+ `${hit.name} (${hit.kind})`,
2946
+ ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
2947
+ ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
2948
+ ].join("\n");
2949
+ }).join("\n\n");
2950
+ }
2951
+
2952
+ // src/code-tool-edit.ts
2953
+ var MAX_TEXT_BYTES = 256 * 1024;
2954
+ function editAsDiff(path, oldText, newText) {
2955
+ const removed = oldText.split("\n").map((line) => `-${line}`);
2956
+ const added = newText.split("\n").map((line) => `+${line}`);
2957
+ return `diff --git a/${path} b/${path}
2958
+ --- a/${path}
2959
+ +++ b/${path}
2960
+ @@ -1,${removed.length} +1,${added.length} @@
2961
+ ${[...removed, ...added].join("\n")}
2962
+ `;
2963
+ }
2964
+ function occurrences(haystack, needle) {
2965
+ let count = 0;
2966
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) count += 1;
2967
+ return count;
2968
+ }
2969
+ async function editCodeFile(workspaceDir, path, oldText, newText) {
2970
+ if (!oldText) throw new TypeError("oldText must not be empty; to create a file use sandbox.apply_patch with a new-file diff");
2971
+ if (Buffer.byteLength(oldText) > MAX_TEXT_BYTES || Buffer.byteLength(newText) > MAX_TEXT_BYTES) {
2972
+ throw new TypeError(`oldText and newText must each stay under ${MAX_TEXT_BYTES} bytes; split the change`);
2973
+ }
2974
+ if (oldText.includes("\0") || newText.includes("\0")) throw new TypeError("edit text contains NUL bytes; use plain text");
2975
+ const target = resolveCodePath(workspaceDir, path);
2976
+ const current = await (0, import_promises10.readFile)(target, "utf8");
2977
+ const found = occurrences(current, oldText);
2978
+ if (found === 0) {
2979
+ throw new TypeError(`oldText was not found in "${path}"; sandbox.read the current lines and copy them exactly, including indentation and blank lines`);
2980
+ }
2981
+ if (found > 1) {
2982
+ throw new TypeError(`oldText occurs ${found} times in "${path}"; include more of the surrounding lines so it matches exactly once`);
2983
+ }
2984
+ const at = current.indexOf(oldText);
2985
+ await (0, import_promises10.writeFile)(target, `${current.slice(0, at)}${newText}${current.slice(at + oldText.length)}`, "utf8");
2986
+ return { deletions: oldText.split("\n").length, additions: newText.split("\n").length };
2987
+ }
2988
+ async function edit(context, request, options, policy, registry) {
2989
+ exactKeys(request.input, ["path", "oldText", "newText"]);
2990
+ const path = stringField(request.input, "path");
2991
+ const oldText = stringField(request.input, "oldText");
2992
+ const newText = typeof request.input.newText === "string" ? request.input.newText : stringField(request.input, "newText");
2993
+ const paths2 = await registry.files(context.workspaceDir);
2994
+ if (!paths2.includes(path)) {
2995
+ 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.`);
2996
+ }
2997
+ if (options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) {
2998
+ throw new TypeError("edit targets a read-only reference source");
2999
+ }
3000
+ const patch2 = editAsDiff(path, oldText, newText);
3001
+ const allowed = await policy.edit(policyContext(context, request, options, { patch: patch2 }));
3002
+ if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3003
+ const changed = await editCodeFile(context.workspaceDir, path, oldText, newText);
3004
+ registry.invalidate(context.workspaceDir);
3005
+ forgetWorkspaceGraphs(context.workspaceDir);
3006
+ return response(request, true, `Edited ${path}: -${changed.deletions} +${changed.additions} line(s).`, { paths: [path], ...changed });
3007
+ }
3008
+
2852
3009
  // src/code-tool-reads.ts
2853
- var import_promises11 = require("fs/promises");
3010
+ var import_promises12 = require("fs/promises");
2854
3011
 
2855
3012
  // src/code-tool-discovery.ts
2856
3013
  var import_node_child_process5 = require("child_process");
2857
- var import_promises9 = require("fs/promises");
2858
- var import_node_path10 = require("path");
3014
+ var import_promises11 = require("fs/promises");
3015
+ var import_node_path11 = require("path");
2859
3016
  var DEFAULT_MAX_FILES = 2e4;
2860
3017
  var DEFAULT_MAX_RESULTS = 100;
2861
3018
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
@@ -2880,13 +3037,13 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
2880
3037
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2881
3038
  const paths2 = [];
2882
3039
  const walk = async (directory) => {
2883
- for (const entry of await (0, import_promises9.readdir)(directory, { withFileTypes: true })) {
3040
+ for (const entry of await (0, import_promises11.readdir)(directory, { withFileTypes: true })) {
2884
3041
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
2885
3042
  if (entry.isSymbolicLink()) throw new TypeError("workspace contains a symbolic link");
2886
- const target = (0, import_node_path10.resolve)(directory, entry.name);
3043
+ const target = (0, import_node_path11.resolve)(directory, entry.name);
2887
3044
  if (entry.isDirectory()) await walk(target);
2888
3045
  else if (entry.isFile()) {
2889
- const path = (0, import_node_path10.relative)(root, target).split("\\").join("/");
3046
+ const path = (0, import_node_path11.relative)(root, target).split("\\").join("/");
2890
3047
  try {
2891
3048
  validateRelativePath(path);
2892
3049
  } catch {
@@ -2897,7 +3054,7 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
2897
3054
  }
2898
3055
  }
2899
3056
  };
2900
- await walk((0, import_node_path10.resolve)(root));
3057
+ await walk((0, import_node_path11.resolve)(root));
2901
3058
  return paths2.sort();
2902
3059
  }
2903
3060
  function listWorkspace(paths2, options = {}) {
@@ -3011,7 +3168,7 @@ async function fallbackSearch(root, scoped, options) {
3011
3168
  if (matches.length >= options.maxResults) break;
3012
3169
  let source;
3013
3170
  try {
3014
- source = await (0, import_promises9.readFile)((0, import_node_path10.resolve)(root, path));
3171
+ source = await (0, import_promises11.readFile)((0, import_node_path11.resolve)(root, path));
3015
3172
  } catch {
3016
3173
  continue;
3017
3174
  }
@@ -3028,67 +3185,6 @@ async function fallbackSearch(root, scoped, options) {
3028
3185
  return matches;
3029
3186
  }
3030
3187
 
3031
- // src/code-tool-graph.ts
3032
- var import_promises10 = require("fs/promises");
3033
- var import_node_path11 = require("path");
3034
- var import_graph = require("@odla-ai/graph");
3035
- var import_code4 = require("@odla-ai/graph/code");
3036
- var cache = /* @__PURE__ */ new Map();
3037
- function workspaceGraphs(workspaceDir, paths2) {
3038
- const existing = cache.get(workspaceDir);
3039
- if (existing) return existing;
3040
- const read2 = (path) => (0, import_promises10.readFile)((0, import_node_path11.join)(workspaceDir, path), "utf8");
3041
- const built = (async () => ({
3042
- // No knownTables: a staged workspace may not carry migrations, and a filter
3043
- // that silently drops every table is worse than an unfiltered one. Callers
3044
- // with ground truth should build the graph themselves.
3045
- graph: await (0, import_code4.buildCodeGraph)({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
3046
- }))();
3047
- cache.set(workspaceDir, built);
3048
- return built;
3049
- }
3050
- function forgetWorkspaceGraphs(workspaceDir) {
3051
- cache.delete(workspaceDir);
3052
- }
3053
- var shortId = (id) => id.slice(id.indexOf(":") + 1);
3054
- function renderOverview(graphs, prefix) {
3055
- const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
3056
- if (rows.length === 0) return prefix ? `No source under "${prefix}".` : "No source files.";
3057
- const lines = rows.slice(0, 60).map((row) => `${row.prefix} (${row.count}) e.g. ${row.examples[0] ?? ""}`);
3058
- const total = (0, import_graph.nodesOfKind)(graphs.graph, import_code4.FILE).length;
3059
- return [`${total} source files. Directories, largest first \u2014 read one with sandbox.list --prefix.`, ...lines].join("\n");
3060
- }
3061
- function renderWhereIs(graphs, symbol) {
3062
- const sites = (0, import_graph.neighbors)(graphs.graph, (0, import_graph.nodeId)(import_code4.SYMBOL, symbol), { direction: "in", kinds: ["exports"] }).map((id) => ({
3063
- path: shortId(id),
3064
- pkg: (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: ["contains"] })[0],
3065
- dependents: (0, import_graph.incident)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] }).length
3066
- })).sort((left, right) => right.dependents - left.dependents || left.path.localeCompare(right.path));
3067
- if (sites.length === 0) return `No exported symbol named "${symbol}". Try sandbox.search for a textual match.`;
3068
- return sites.slice(0, 20).map((site) => `${site.path}${site.pkg ? ` [${shortId(site.pkg)}]` : ""} ${site.dependents} dependents`).join("\n");
3069
- }
3070
- function renderWhoImports(graphs, path) {
3071
- const id = (0, import_graph.nodeId)(import_code4.FILE, path);
3072
- const importers = (0, import_graph.neighbors)(graphs.graph, id, { direction: "in", kinds: [import_code4.IMPORTS] });
3073
- if (importers.length === 0) {
3074
- return graphs.graph.nodes.has(id) ? `Nothing imports ${path}. It is a leaf.` : `${path} is not a source file in this workspace.`;
3075
- }
3076
- return importers.slice(0, 40).map(shortId).sort().join("\n");
3077
- }
3078
- function renderWhoTouches(graphs, query) {
3079
- const needle = query.toLowerCase();
3080
- const hits = [...graphs.graph.nodes.values()].filter((node) => (node.kind === "table" || node.kind === "namespace") && node.name.toLowerCase().includes(needle)).slice(0, 10);
3081
- if (hits.length === 0) return `No table or namespace matching "${query}".`;
3082
- return hits.map((hit) => {
3083
- const side = (kind) => (0, import_graph.neighbors)(graphs.graph, hit.id, { direction: "in", kinds: [kind] }).map(shortId).sort().slice(0, 8);
3084
- return [
3085
- `${hit.name} (${hit.kind})`,
3086
- ` writes: ${side(import_code4.WRITES).join(", ") || "(none)"}`,
3087
- ` reads: ${side(import_code4.READS).join(", ") || "(none)"}`
3088
- ].join("\n");
3089
- }).join("\n\n");
3090
- }
3091
-
3092
3188
  // src/code-tool-reads.ts
3093
3189
  var GRAPH_TOOLS = /* @__PURE__ */ new Set([
3094
3190
  "sandbox.overview",
@@ -3111,11 +3207,11 @@ async function read(context, request, options, policy, registry) {
3111
3207
  const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
3112
3208
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
3113
3209
  const target = resolveCodePath(context.workspaceDir, path);
3114
- const info = await (0, import_promises11.stat)(target);
3210
+ const info = await (0, import_promises12.stat)(target);
3115
3211
  if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
3116
3212
  throw new TypeError("file is not a bounded regular source file");
3117
3213
  }
3118
- const source = await (0, import_promises11.readFile)(target);
3214
+ const source = await (0, import_promises12.readFile)(target);
3119
3215
  if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
3120
3216
  const lines = source.toString("utf8").split("\n");
3121
3217
  const content = lines.slice(startLine - 1, endLine).join("\n");
@@ -3230,6 +3326,7 @@ async function route(context, request, options, recipes, policy, registry) {
3230
3326
  if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
3231
3327
  if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
3232
3328
  if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
3329
+ if (request.tool === "sandbox.edit") return await edit(context, request, options, policy, registry);
3233
3330
  return await recipe(context, request, options, recipes, policy);
3234
3331
  } catch (reason) {
3235
3332
  return response(request, false, toolFailureMessage(reason));
@@ -3916,6 +4013,9 @@ var TheseusRuntimeEngine = class {
3916
4013
  prompt: metadata.prompt,
3917
4014
  signal: active.abort.signal,
3918
4015
  readOnly: metadata.readOnly,
4016
+ // A review-role session is read-only and still runs the proof; a planner
4017
+ // marked read-only by its payload runs nothing.
4018
+ recipes: metadata.role === "review" || !metadata.readOnly,
3919
4019
  recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
3920
4020
  ...extraSkills.length ? { extraSkills } : {}
3921
4021
  });
@@ -4025,7 +4125,7 @@ function parse(argv) {
4025
4125
  };
4026
4126
  }
4027
4127
  async function readPolicy(path) {
4028
- const value = JSON.parse(await (0, import_promises12.readFile)(path, "utf8"));
4128
+ const value = JSON.parse(await (0, import_promises13.readFile)(path, "utf8"));
4029
4129
  if (!value || Object.keys(value).some((key) => !["recipes", "recipeAuthorization"].includes(key)) || !Array.isArray(value.recipes) || !value.recipes.length || value.recipeAuthorization !== void 0 && value.recipeAuthorization !== "registered_recipe" && value.recipeAuthorization !== "exact_approval") throw new TypeError("invalid Code build policy file");
4030
4130
  const recipes = value.recipes;
4031
4131
  for (const recipe2 of recipes) assertCodeBuildRecipe(recipe2);