@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.
- package/dist/{chunk-U324RQ4N.js → chunk-CPV7ZVHH.js} +1 -1
- package/dist/{chunk-U324RQ4N.js.map → chunk-CPV7ZVHH.js.map} +1 -1
- package/dist/{chunk-OZBJNTML.js → chunk-ESLFS3FY.js} +4 -4
- package/dist/{chunk-5LRYJKUI.js → chunk-FUDTSAOZ.js} +3 -3
- package/dist/{chunk-VDY5V7ZG.js → chunk-JV45JAAW.js} +3 -3
- package/dist/chunk-JV45JAAW.js.map +1 -0
- package/dist/{chunk-FAN2R3GW.js → chunk-KXFI3WM2.js} +5 -1
- package/dist/chunk-KXFI3WM2.js.map +1 -0
- package/dist/{chunk-WM34GGTK.js → chunk-TMBYA5JX.js} +107 -13
- package/dist/chunk-TMBYA5JX.js.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/code-runtime-cli.cjs +179 -79
- package/dist/code-runtime-cli.cjs.map +1 -1
- package/dist/code-runtime-cli.js +5 -5
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/node.cjs +183 -83
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +11 -2
- package/dist/node.d.ts +11 -2
- package/dist/node.js +6 -6
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-_8y8vBDI.d.ts → types-ocy70g_p.d.cts} +1 -1
- package/dist/{types-_8y8vBDI.d.cts → types-ocy70g_p.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-FAN2R3GW.js.map +0 -1
- package/dist/chunk-VDY5V7ZG.js.map +0 -1
- package/dist/chunk-WM34GGTK.js.map +0 -1
- /package/dist/{chunk-OZBJNTML.js.map → chunk-ESLFS3FY.js.map} +0 -0
- /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
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
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
|
|
2811
|
-
const
|
|
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
|
|
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
|
|
3445
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
4976
|
+
const existing = await (0, import_promises15.lstat)(target).catch(() => null);
|
|
4877
4977
|
if (!existing) {
|
|
4878
|
-
await (0,
|
|
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,
|
|
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,
|
|
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:
|