@odla-ai/harness 0.6.0 → 0.7.1

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.
@@ -1149,7 +1149,7 @@ var CodeRuntimeCheckpointManager = class {
1149
1149
  await this.options.event(command, { type: "message", actor: "system", body: prepared.note }, active.conversationRefs).catch(() => void 0);
1150
1150
  await active.workspace.cleanup();
1151
1151
  await this.options.event(command, { type: "status", status: "checkpointed" }, active.conversationRefs).catch(() => void 0);
1152
- return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Pi stopped at a portable checkpoint" };
1152
+ return { status: "checkpointed", checkpoint: prepared.checkpoint, message: "Theseus stopped at a portable checkpoint" };
1153
1153
  }
1154
1154
  async acknowledged(command, result) {
1155
1155
  if (command.kind !== "checkpoint_stop" || result.status !== "checkpointed") return false;
@@ -1391,7 +1391,7 @@ var import_ai2 = require("@odla-ai/ai");
1391
1391
  var import_ai = require("@odla-ai/ai");
1392
1392
 
1393
1393
  // src/code-agent-skill.ts
1394
- var V1_SYSTEM_PROMPT = `You are Pi, the coding agent inside an odla Code harness.
1394
+ var V1_SYSTEM_PROMPT = `You are Theseus, the coding agent inside an odla Code harness.
1395
1395
  Use only the odla_read, odla_apply_git_diff, and odla_run_recipe tools.
1396
1396
  For mutations, call odla_apply_git_diff with raw git diff text. It must start
1397
1397
  with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
@@ -1401,7 +1401,10 @@ Never claim a build or test passed unless odla_run_recipe returned that result.`
1401
1401
  var V2_SYSTEM_PROMPT = `You are the coding agent inside an odla Code harness.
1402
1402
  Start by orienting: odla_list shows the files in the workspace and odla_search
1403
1403
  finds a literal string across them. Prefer those over guessing a path.
1404
- Then odla_read a bounded range, and odla_apply_git_diff to mutate.
1404
+ Then odla_read a bounded range, and odla_apply_git_diff to mutate. When you
1405
+ need several independent searches or file ranges, issue those read-only calls
1406
+ together in one turn; their results stay ordered and the harness overlaps them.
1407
+ Never issue odla_apply_git_diff or odla_run_recipe alongside another tool call.
1405
1408
  For mutations, call odla_apply_git_diff with raw git diff text. It must start
1406
1409
  with "diff --git a/<path> b/<path>", include matching "---" and "+++" file
1407
1410
  headers and numbered "@@" hunks, and never use "*** Begin Patch" wrappers.
@@ -1432,18 +1435,26 @@ var SYSTEM_PROMPT_FOR = {
1432
1435
  };
1433
1436
  function codeSkill(opts) {
1434
1437
  let seq = 0;
1438
+ let nextCompletion = 1;
1439
+ const completed = /* @__PURE__ */ new Map();
1435
1440
  const call = async (tool, input, signal) => {
1441
+ const sequence = ++seq;
1436
1442
  const startedAt = Date.now();
1437
1443
  const response2 = await opts.broker.execute(
1438
1444
  { lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
1439
- { requestId: `bench-${tool}-${++seq}`, tool, input }
1445
+ { requestId: `bench-${tool}-${sequence}`, tool, input }
1440
1446
  );
1441
- opts.onToolCall?.({
1447
+ completed.set(sequence, {
1442
1448
  tool,
1443
1449
  ok: response2.ok,
1444
1450
  durationMs: Date.now() - startedAt,
1445
1451
  ...response2.ok ? {} : { error: String(response2.content).slice(0, 300) }
1446
1452
  });
1453
+ while (completed.has(nextCompletion)) {
1454
+ const completion = completed.get(nextCompletion);
1455
+ completed.delete(nextCompletion++);
1456
+ opts.onToolCall?.(completion);
1457
+ }
1447
1458
  return { content: response2.content, isError: !response2.ok };
1448
1459
  };
1449
1460
  const read2 = {
@@ -1459,6 +1470,7 @@ function codeSkill(opts) {
1459
1470
  },
1460
1471
  additionalProperties: false
1461
1472
  },
1473
+ concurrency: "parallel",
1462
1474
  handler: (input, ctx) => call("sandbox.read", input, ctx.signal)
1463
1475
  };
1464
1476
  const applyPatch = {
@@ -1474,11 +1486,17 @@ function codeSkill(opts) {
1474
1486
  };
1475
1487
  const runRecipe = {
1476
1488
  name: "odla_run_recipe",
1477
- description: "Run one app-registered build or test recipe through CaMeL policy.",
1489
+ description: `Run one app-registered build or test recipe through CaMeL policy.${opts.recipeIds?.length ? ` Available recipes: ${opts.recipeIds.join(", ")}.` : ""}`,
1478
1490
  inputSchema: {
1479
1491
  type: "object",
1480
1492
  required: ["recipeId"],
1481
- properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
1493
+ properties: { recipeId: {
1494
+ type: "string",
1495
+ minLength: 1,
1496
+ maxLength: 120,
1497
+ pattern: "^[a-zA-Z0-9._:-]+$",
1498
+ ...opts.recipeIds?.length ? { enum: [...opts.recipeIds] } : {}
1499
+ } },
1482
1500
  additionalProperties: false
1483
1501
  },
1484
1502
  handler: (input, ctx) => call("sandbox.run_recipe", input, ctx.signal)
@@ -1494,6 +1512,7 @@ function codeSkill(opts) {
1494
1512
  },
1495
1513
  additionalProperties: false
1496
1514
  },
1515
+ concurrency: "parallel",
1497
1516
  handler: (input, ctx) => call("sandbox.list", input, ctx.signal)
1498
1517
  };
1499
1518
  const searchFiles = {
@@ -1510,11 +1529,13 @@ function codeSkill(opts) {
1510
1529
  },
1511
1530
  additionalProperties: false
1512
1531
  },
1532
+ concurrency: "parallel",
1513
1533
  handler: (input, ctx) => call("sandbox.search", input, ctx.signal)
1514
1534
  };
1515
1535
  const graphTool = (name, tool, description, required) => ({
1516
1536
  name,
1517
1537
  description,
1538
+ concurrency: "parallel",
1518
1539
  inputSchema: {
1519
1540
  type: "object",
1520
1541
  ...required ? { required: ["query"] } : {},
@@ -1562,6 +1583,7 @@ async function runCodeAgent(options) {
1562
1583
  lease: options.lease,
1563
1584
  workspaceDir: options.workspaceDir,
1564
1585
  surface,
1586
+ ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
1565
1587
  onToolCall: (call) => {
1566
1588
  toolCalls.push(call);
1567
1589
  options.onToolCall?.(call);
@@ -1592,7 +1614,7 @@ async function runCodeAgent(options) {
1592
1614
  // src/code-runtime-attempt.ts
1593
1615
  async function runCodeAgentAttempt(options) {
1594
1616
  try {
1595
- const surface = options.surface ?? "v2";
1617
+ const surface = options.surface ?? "v3";
1596
1618
  const { run } = await runCodeAgent({
1597
1619
  inference: options.inference,
1598
1620
  broker: options.broker,
@@ -1603,6 +1625,7 @@ async function runCodeAgentAttempt(options) {
1603
1625
  // id only labels the request the control plane is about to rewrite.
1604
1626
  model: "brokered",
1605
1627
  surface,
1628
+ ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
1606
1629
  ...options.maxSteps === void 0 ? {} : { maxSteps: options.maxSteps },
1607
1630
  ...options.budget ? { budget: options.budget } : {},
1608
1631
  ...options.signal ? { signal: options.signal } : {},
@@ -1942,11 +1965,30 @@ function response(request, ok, content, details) {
1942
1965
  var import_promises9 = require("fs/promises");
1943
1966
 
1944
1967
  // src/code-tool-discovery.ts
1968
+ var import_node_child_process5 = require("child_process");
1945
1969
  var import_promises7 = require("fs/promises");
1946
1970
  var import_node_path8 = require("path");
1947
1971
  var DEFAULT_MAX_FILES = 2e4;
1948
1972
  var DEFAULT_MAX_RESULTS = 100;
1949
1973
  var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
1974
+ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = registeredFiles) {
1975
+ const cache2 = /* @__PURE__ */ new Map();
1976
+ return {
1977
+ files(root) {
1978
+ const existing = cache2.get(root);
1979
+ if (existing) return existing;
1980
+ const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
1981
+ cache2.set(root, pending);
1982
+ void pending.catch(() => {
1983
+ if (cache2.get(root) === pending) cache2.delete(root);
1984
+ });
1985
+ return pending;
1986
+ },
1987
+ invalidate(root) {
1988
+ cache2.delete(root);
1989
+ }
1990
+ };
1991
+ }
1950
1992
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1951
1993
  const paths = [];
1952
1994
  const walk = async (directory) => {
@@ -1977,28 +2019,122 @@ function listWorkspace(paths, options = {}) {
1977
2019
  return scoped.slice(0, max);
1978
2020
  }
1979
2021
  async function searchWorkspace(root, paths, options) {
1980
- const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
1981
- if (!query) throw new TypeError("search query must be a non-empty string");
2022
+ options.signal?.throwIfAborted();
2023
+ if (!options.query) throw new TypeError("search query must be a non-empty string");
1982
2024
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
1983
2025
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
1984
2026
  const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
2027
+ if (scoped.length === 0) return [];
2028
+ try {
2029
+ return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
2030
+ } catch (error) {
2031
+ options.signal?.throwIfAborted();
2032
+ return fallbackSearch(root, scoped, { ...options, maxResults, maxFileBytes });
2033
+ }
2034
+ }
2035
+ var MAX_NATIVE_ARG_BYTES = 96 * 1024;
2036
+ async function nativeSearch(root, paths, options) {
2037
+ const batches = [];
2038
+ let batch = [];
2039
+ let bytes = 0;
2040
+ for (const path of paths) {
2041
+ const size = Buffer.byteLength(path) + 1;
2042
+ if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
2043
+ batches.push(batch);
2044
+ batch = [];
2045
+ bytes = 0;
2046
+ }
2047
+ batch.push(path);
2048
+ bytes += size;
2049
+ }
2050
+ if (batch.length > 0) batches.push(batch);
2051
+ const matches = [];
2052
+ for (const files of batches) {
2053
+ const remaining = options.maxResults - matches.length;
2054
+ if (remaining <= 0) break;
2055
+ matches.push(...await nativeSearchBatch(root, files, options, remaining));
2056
+ }
2057
+ return matches;
2058
+ }
2059
+ function nativeSearchBatch(root, paths, options, remaining) {
2060
+ return new Promise((resolveMatches, reject) => {
2061
+ const args = [
2062
+ "--fixed-strings",
2063
+ "--json",
2064
+ "--no-messages",
2065
+ "--sort=path",
2066
+ `--max-filesize=${options.maxFileBytes}`,
2067
+ "--max-columns=4096",
2068
+ "--max-columns-preview",
2069
+ options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
2070
+ "--",
2071
+ options.query,
2072
+ ...paths
2073
+ ];
2074
+ const child = (0, import_node_child_process5.spawn)("rg", args, {
2075
+ cwd: root,
2076
+ stdio: ["ignore", "pipe", "ignore"],
2077
+ ...options.signal ? { signal: options.signal } : {}
2078
+ });
2079
+ const matches = [];
2080
+ let carry = "";
2081
+ let stopped = false;
2082
+ const consume = (line) => {
2083
+ if (matches.length >= remaining) return;
2084
+ let event;
2085
+ try {
2086
+ event = JSON.parse(line);
2087
+ } catch {
2088
+ return;
2089
+ }
2090
+ const path = event.data?.path?.text;
2091
+ const lineNumber = event.data?.line_number;
2092
+ const source = event.data?.lines?.text;
2093
+ if (event.type !== "match" || path === void 0 || lineNumber === void 0 || source === void 0) return;
2094
+ matches.push({ path, line: lineNumber, text: source.trim().slice(0, 240) });
2095
+ if (matches.length >= remaining) {
2096
+ stopped = true;
2097
+ child.kill();
2098
+ }
2099
+ };
2100
+ child.stdout.setEncoding("utf8");
2101
+ child.stdout.on("data", (chunk) => {
2102
+ carry += chunk;
2103
+ let newline = carry.indexOf("\n");
2104
+ while (newline >= 0) {
2105
+ consume(carry.slice(0, newline));
2106
+ carry = carry.slice(newline + 1);
2107
+ newline = carry.indexOf("\n");
2108
+ }
2109
+ });
2110
+ child.once("error", reject);
2111
+ child.once("close", (code) => {
2112
+ if (carry) consume(carry);
2113
+ if (stopped || code === 0 || code === 1) resolveMatches(matches);
2114
+ else reject(new Error(`native search exited with status ${code ?? "unknown"}`));
2115
+ });
2116
+ });
2117
+ }
2118
+ async function fallbackSearch(root, scoped, options) {
2119
+ const query = options.caseSensitive === false ? options.query.toLowerCase() : options.query;
1985
2120
  const matches = [];
1986
2121
  for (const path of scoped) {
1987
- if (matches.length >= maxResults) break;
2122
+ options.signal?.throwIfAborted();
2123
+ if (matches.length >= options.maxResults) break;
1988
2124
  let source;
1989
2125
  try {
1990
2126
  source = await (0, import_promises7.readFile)((0, import_node_path8.resolve)(root, path));
1991
2127
  } catch {
1992
2128
  continue;
1993
2129
  }
1994
- if (source.byteLength > maxFileBytes || source.includes(0)) continue;
2130
+ if (source.byteLength > options.maxFileBytes || source.includes(0)) continue;
1995
2131
  const lines = source.toString("utf8").split("\n");
1996
2132
  for (let index = 0; index < lines.length; index += 1) {
1997
2133
  const raw = lines[index];
1998
2134
  const haystack = options.caseSensitive === false ? raw.toLowerCase() : raw;
1999
2135
  if (!haystack.includes(query)) continue;
2000
2136
  matches.push({ path, line: index + 1, text: raw.trim().slice(0, 240) });
2001
- if (matches.length >= maxResults) break;
2137
+ if (matches.length >= options.maxResults) break;
2002
2138
  }
2003
2139
  }
2004
2140
  return matches;
@@ -2023,6 +2159,9 @@ function workspaceGraphs(workspaceDir, paths) {
2023
2159
  cache.set(workspaceDir, built);
2024
2160
  return built;
2025
2161
  }
2162
+ function forgetWorkspaceGraphs(workspaceDir) {
2163
+ cache.delete(workspaceDir);
2164
+ }
2026
2165
  var shortId = (id) => id.slice(id.indexOf(":") + 1);
2027
2166
  function renderOverview(graphs, prefix) {
2028
2167
  const rows = (0, import_graph.rollup)(graphs.graph, import_code4.FILE, prefix === void 0 ? {} : { prefix });
@@ -2069,7 +2208,7 @@ var GRAPH_TOOLS = /* @__PURE__ */ new Set([
2069
2208
  "sandbox.who_imports",
2070
2209
  "sandbox.who_touches"
2071
2210
  ]);
2072
- async function read(context, request, options, policy) {
2211
+ async function read(context, request, options, policy, registry) {
2073
2212
  exactKeys(request.input, ["path", "startLine", "endLine"]);
2074
2213
  const path = stringField(request.input, "path");
2075
2214
  const startLine = optionalInteger(request.input.startLine) ?? 1;
@@ -2077,7 +2216,7 @@ async function read(context, request, options, policy) {
2077
2216
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
2078
2217
  throw new TypeError("requested line range exceeds its bound");
2079
2218
  }
2080
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2219
+ const paths = await registry.files(context.workspaceDir);
2081
2220
  if (!paths.includes(path)) {
2082
2221
  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.`);
2083
2222
  }
@@ -2097,13 +2236,13 @@ async function read(context, request, options, policy) {
2097
2236
  }
2098
2237
  return response(request, true, content, { path, startLine, endLine: Math.min(endLine, lines.length) });
2099
2238
  }
2100
- async function list(context, request, options, policy) {
2239
+ async function list(context, request, options, policy, registry) {
2101
2240
  exactKeys(request.input, ["prefix", "maxEntries"]);
2102
2241
  const raw = request.input.prefix;
2103
2242
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
2104
2243
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
2105
2244
  if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
2106
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2245
+ const paths = await registry.files(context.workspaceDir);
2107
2246
  const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
2108
2247
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2109
2248
  const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
@@ -2121,7 +2260,7 @@ async function list(context, request, options, policy) {
2121
2260
  { count: entries.length, truncated }
2122
2261
  );
2123
2262
  }
2124
- async function search(context, request, options, policy) {
2263
+ async function search(context, request, options, policy, registry) {
2125
2264
  exactKeys(request.input, ["query", "prefix", "maxResults", "caseSensitive"]);
2126
2265
  const query = stringField(request.input, "query");
2127
2266
  if (query.length > 512) throw new TypeError("search query exceeds its bound");
@@ -2130,21 +2269,22 @@ async function search(context, request, options, policy) {
2130
2269
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
2131
2270
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
2132
2271
  const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
2133
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2272
+ const paths = await registry.files(context.workspaceDir);
2134
2273
  const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
2135
2274
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2136
2275
  const matches = await searchWorkspace(context.workspaceDir, paths, {
2137
2276
  query,
2138
2277
  maxResults,
2139
2278
  caseSensitive,
2140
- ...prefix ? { prefix } : {}
2279
+ ...prefix ? { prefix } : {},
2280
+ ...context.signal ? { signal: context.signal } : {}
2141
2281
  });
2142
2282
  if (!matches.length) return response(request, true, `No match for "${query}".`, { count: 0 });
2143
2283
  return response(request, true, matches.map((match) => `${match.path}:${match.line}: ${match.text}`).join("\n"), {
2144
2284
  count: matches.length
2145
2285
  });
2146
2286
  }
2147
- async function graphQuery(context, request, options, policy) {
2287
+ async function graphQuery(context, request, options, policy, registry) {
2148
2288
  exactKeys(request.input, ["query"]);
2149
2289
  const raw = request.input.query;
2150
2290
  const query = typeof raw === "string" ? raw : "";
@@ -2154,7 +2294,7 @@ async function graphQuery(context, request, options, policy) {
2154
2294
  selector: query
2155
2295
  }));
2156
2296
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2157
- const paths = await registeredFiles(context.workspaceDir, 2e4);
2297
+ const paths = await registry.files(context.workspaceDir);
2158
2298
  const graphs = await workspaceGraphs(context.workspaceDir, paths);
2159
2299
  if (request.tool === "sandbox.overview") {
2160
2300
  return response(request, true, renderOverview(graphs, query || void 0));
@@ -2170,23 +2310,38 @@ function createCodeToolBroker(options) {
2170
2310
  validateOptions(options);
2171
2311
  const recipes = new Map(options.recipes.map((recipe2) => [recipe2.id, recipe2]));
2172
2312
  const policy = createCodePolicyGate(options);
2173
- let tail = Promise.resolve();
2313
+ const registry = createWorkspaceFileRegistry();
2314
+ let barrier = Promise.resolve();
2315
+ const activeReads = /* @__PURE__ */ new Set();
2174
2316
  return {
2175
2317
  execute(context, request) {
2176
- const result = tail.then(() => route(context, request, options, recipes, policy));
2177
- tail = result.then(() => void 0, () => void 0);
2318
+ if (isReadTool(request.tool)) {
2319
+ const result2 = barrier.then(() => route(context, request, options, recipes, policy, registry));
2320
+ const settled = result2.then(() => void 0, () => void 0);
2321
+ activeReads.add(settled);
2322
+ void settled.then(() => {
2323
+ activeReads.delete(settled);
2324
+ });
2325
+ return result2;
2326
+ }
2327
+ const earlierReads = [...activeReads];
2328
+ const result = barrier.then(() => Promise.all(earlierReads)).then(() => route(context, request, options, recipes, policy, registry));
2329
+ barrier = result.then(() => void 0, () => void 0);
2178
2330
  return result;
2179
2331
  }
2180
2332
  };
2181
2333
  }
2182
- async function route(context, request, options, recipes, policy) {
2334
+ function isReadTool(tool) {
2335
+ return tool === "sandbox.read" || tool === "sandbox.list" || tool === "sandbox.search" || GRAPH_TOOLS.has(tool);
2336
+ }
2337
+ async function route(context, request, options, recipes, policy, registry) {
2183
2338
  try {
2184
2339
  if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
2185
- if (request.tool === "sandbox.read") return await read(context, request, options, policy);
2186
- if (request.tool === "sandbox.list") return await list(context, request, options, policy);
2187
- if (request.tool === "sandbox.search") return await search(context, request, options, policy);
2188
- if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy);
2189
- if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy);
2340
+ if (request.tool === "sandbox.read") return await read(context, request, options, policy, registry);
2341
+ if (request.tool === "sandbox.list") return await list(context, request, options, policy, registry);
2342
+ if (request.tool === "sandbox.search") return await search(context, request, options, policy, registry);
2343
+ if (GRAPH_TOOLS.has(request.tool)) return await graphQuery(context, request, options, policy, registry);
2344
+ if (request.tool === "sandbox.apply_patch") return await patch(context, request, options, policy, registry);
2190
2345
  return await recipe(context, request, options, recipes, policy);
2191
2346
  } catch (reason) {
2192
2347
  return response(request, false, toolFailureMessage(reason));
@@ -2201,7 +2356,7 @@ function toolFailureMessage(reason) {
2201
2356
  if (code === "EACCES" || code === "EPERM") return "that path is not readable through this tool";
2202
2357
  return "tool failed closed";
2203
2358
  }
2204
- async function patch(context, request, options, policy) {
2359
+ async function patch(context, request, options, policy, registry) {
2205
2360
  exactKeys(request.input, ["patch"]);
2206
2361
  const value = stringField(request.input, "patch");
2207
2362
  const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
@@ -2211,6 +2366,8 @@ async function patch(context, request, options, policy) {
2211
2366
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
2212
2367
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2213
2368
  await applyCodePatch(context.workspaceDir, value, paths);
2369
+ registry.invalidate(context.workspaceDir);
2370
+ forgetWorkspaceGraphs(context.workspaceDir);
2214
2371
  return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2215
2372
  }
2216
2373
  async function recipe(context, request, options, recipes, policy) {
@@ -2579,7 +2736,7 @@ var digestRuntimeValue = (value) => `sha256:${(0, import_node_crypto4.createHash
2579
2736
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
2580
2737
 
2581
2738
  // src/code-runtime-engine.ts
2582
- var CodePiRuntimeEngine = class {
2739
+ var TheseusRuntimeEngine = class {
2583
2740
  constructor(options) {
2584
2741
  this.options = options;
2585
2742
  this.#attempt = options.runAgentAttempt ?? runCodeAgentAttempt;
@@ -2665,7 +2822,7 @@ var CodePiRuntimeEngine = class {
2665
2822
  await this.#failure(command, active, detail);
2666
2823
  return null;
2667
2824
  });
2668
- return { status: "running", message: resume ? "Pi resumed from a portable checkpoint" : "Pi started" };
2825
+ return { status: "running", message: resume ? "Theseus resumed from a portable checkpoint" : "Theseus started" };
2669
2826
  }
2670
2827
  /**
2671
2828
  * Pursue a goal: attempt, judge with the clean verifier, re-prompt from what
@@ -2753,7 +2910,7 @@ var CodePiRuntimeEngine = class {
2753
2910
  await this.#failure(command, active, detail);
2754
2911
  return null;
2755
2912
  });
2756
- return { status: "running", message: "Pi accepted the owner prompt" };
2913
+ return { status: "running", message: "Theseus accepted the owner prompt" };
2757
2914
  }
2758
2915
  async #runAttempt(command, metadata, active) {
2759
2916
  const lease = fakeCodeLease(command, metadata);
@@ -2778,7 +2935,8 @@ var CodePiRuntimeEngine = class {
2778
2935
  lease,
2779
2936
  workspaceDir: active.workspace.workspaceDir,
2780
2937
  prompt: metadata.prompt,
2781
- signal: active.abort.signal
2938
+ signal: active.abort.signal,
2939
+ recipeIds: this.options.recipes.map((recipe2) => recipe2.id)
2782
2940
  });
2783
2941
  const closing = result.finalText.trim();
2784
2942
  const completed = result.status === "completed" && Boolean(closing);
@@ -2842,7 +3000,7 @@ var CodePiRuntimeEngine = class {
2842
3000
  }
2843
3001
  }
2844
3002
  async #diagnostic(command, active, value) {
2845
- const detail = value.trim().slice(0, 2e3) || "Pi runtime failed";
3003
+ const detail = value.trim().slice(0, 2e3) || "Theseus runtime failed";
2846
3004
  this.options.onDiagnostic?.(detail);
2847
3005
  await this.#event(
2848
3006
  command,
@@ -2924,8 +3082,8 @@ async function main() {
2924
3082
  };
2925
3083
  const controller = new AbortController();
2926
3084
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => controller.abort(signal));
2927
- const control = createCodeRuntimeControlClient({ endpoint: options.endpoint, token, signal: controller.signal });
2928
- const commandEngine = new CodePiRuntimeEngine({
3085
+ const control = createCodeRuntimeControlClient({ endpoint: options.endpoint, token });
3086
+ const commandEngine = new TheseusRuntimeEngine({
2929
3087
  control,
2930
3088
  engine,
2931
3089
  recipes: policy.recipes,