@odla-ai/harness 0.9.0 → 0.9.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-CR6RE3A2.js → chunk-5LRYJKUI.js} +3 -3
  2. package/dist/chunk-FAN2R3GW.js +166 -0
  3. package/dist/chunk-FAN2R3GW.js.map +1 -0
  4. package/dist/{chunk-NWNBSX56.js → chunk-OZBJNTML.js} +4 -4
  5. package/dist/{chunk-NG7AYYH3.js → chunk-Q4NL5XO3.js} +204 -217
  6. package/dist/chunk-Q4NL5XO3.js.map +1 -0
  7. package/dist/{chunk-RXNHCGWE.js → chunk-U324RQ4N.js} +1 -1
  8. package/dist/{chunk-RXNHCGWE.js.map → chunk-U324RQ4N.js.map} +1 -1
  9. package/dist/{chunk-Q7CKOT7T.js → chunk-VDY5V7ZG.js} +2 -2
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +4 -4
  12. package/dist/code-runtime-cli.cjs +311 -164
  13. package/dist/code-runtime-cli.cjs.map +1 -1
  14. package/dist/code-runtime-cli.js +12 -6
  15. package/dist/code-runtime-cli.js.map +1 -1
  16. package/dist/index.cjs +172 -6
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +24 -3
  19. package/dist/index.d.ts +24 -3
  20. package/dist/index.js +11 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/node.cjs +308 -162
  23. package/dist/node.cjs.map +1 -1
  24. package/dist/node.d.cts +44 -7
  25. package/dist/node.d.ts +44 -7
  26. package/dist/node.js +10 -5
  27. package/dist/node.js.map +1 -1
  28. package/dist/testing.cjs.map +1 -1
  29. package/dist/testing.d.cts +1 -1
  30. package/dist/testing.d.ts +1 -1
  31. package/dist/testing.js +1 -1
  32. package/dist/{types-BNJikP5h.d.ts → types-_8y8vBDI.d.cts} +19 -1
  33. package/dist/{types-BNJikP5h.d.cts → types-_8y8vBDI.d.ts} +19 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-NG7AYYH3.js.map +0 -1
  36. /package/dist/{chunk-CR6RE3A2.js.map → chunk-5LRYJKUI.js.map} +0 -0
  37. /package/dist/{chunk-NWNBSX56.js.map → chunk-OZBJNTML.js.map} +0 -0
  38. /package/dist/{chunk-Q7CKOT7T.js.map → chunk-VDY5V7ZG.js.map} +0 -0
@@ -1,13 +1,16 @@
1
+ import {
2
+ observedBroker
3
+ } from "./chunk-FAN2R3GW.js";
1
4
  import {
2
5
  SKIP_WORKSPACE_DIRS,
3
6
  assertPinnedImage,
4
7
  stageWorkspace,
5
8
  stageWorkspacePair,
6
9
  verifyContainerEngineBoundary
7
- } from "./chunk-CR6RE3A2.js";
10
+ } from "./chunk-5LRYJKUI.js";
8
11
  import {
9
12
  HARNESS_PROTOCOL_VERSION
10
- } from "./chunk-RXNHCGWE.js";
13
+ } from "./chunk-U324RQ4N.js";
11
14
 
12
15
  // src/workspace-digest.ts
13
16
  import { createHash } from "crypto";
@@ -186,12 +189,14 @@ async function runCodeRuntimeHeartbeatLoop(options) {
186
189
  } while (!options.signal?.aborted);
187
190
  }
188
191
  var CodeRuntimeReconciler = class {
189
- constructor(control, engine) {
192
+ constructor(control, engine, onDiagnostic) {
190
193
  this.control = control;
191
194
  this.engine = engine;
195
+ this.onDiagnostic = onDiagnostic;
192
196
  }
193
197
  control;
194
198
  engine;
199
+ onDiagnostic;
195
200
  results = /* @__PURE__ */ new Map();
196
201
  async reconcile(snapshot) {
197
202
  for (const command of snapshot.commands) {
@@ -209,7 +214,13 @@ var CodeRuntimeReconciler = class {
209
214
  }
210
215
  await this.control.acknowledge(command.commandId, completed.result);
211
216
  if (!completed.notified) {
212
- await this.engine.acknowledged?.(command, completed.result);
217
+ try {
218
+ await this.engine.acknowledged?.(command, completed.result);
219
+ } catch (error) {
220
+ this.onDiagnostic?.(
221
+ `command ${command.commandId} acknowledged handling failed \xB7 ${error instanceof Error ? error.message : String(error)}`
222
+ );
223
+ }
213
224
  completed.notified = true;
214
225
  }
215
226
  }
@@ -336,7 +347,7 @@ async function parseSource(value) {
336
347
  }
337
348
  function parseReview(value) {
338
349
  const review = record(record(value)?.review);
339
- if (!review || !["approved", "rejected"].includes(String(review.verdict)) || typeof review.reviewDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(review.reviewDigest) || typeof review.provider !== "string" || !review.provider || typeof review.model !== "string" || !review.model || !Number.isSafeInteger(review.policyVersion) || Number(review.policyVersion) < 1) throw invalid("review");
350
+ if (!review || !["approved", "rejected"].includes(String(review.verdict)) || typeof review.reviewDigest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(review.reviewDigest) || typeof review.provider !== "string" || !review.provider || typeof review.model !== "string" || !review.model || !Number.isSafeInteger(review.policyVersion) || Number(review.policyVersion) < 1 || !Number.isSafeInteger(review.score) || Number(review.score) < 0 || Number(review.score) > 100 || typeof review.summary !== "string" || !Array.isArray(review.findings) || review.findings.length > 12) throw invalid("review");
340
351
  return review;
341
352
  }
342
353
  function parseCandidate(value) {
@@ -443,10 +454,41 @@ var SECRET = /^(?:\.env(?:\..+)?|\.dev\.vars|credentials(?:\..+)?\.json|dev-toke
443
454
  var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
444
455
  var FORBIDDEN = /^(?:GIT binary patch|Binary files |rename (?:from|to) |copy (?:from|to) |similarity index |old mode |new mode |deleted file mode 160000|new file mode 160000)/m;
445
456
  function stripPatchEnvelope(patch2) {
446
- if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
457
+ if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return applyPatchDialectToDiff(patch2);
447
458
  const kept = patch2.split("\n").filter((line) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line));
448
459
  const stripped = kept.join("\n");
449
- return /^diff --git /m.test(stripped) ? stripped : patch2;
460
+ if (/^diff --git /m.test(stripped)) return stripped;
461
+ const translated = applyPatchDialectToDiff(stripped);
462
+ return translated === stripped ? patch2 : translated;
463
+ }
464
+ function applyPatchDialectToDiff(patch2) {
465
+ if (!/^\*\*\* (?:Update|Add|Delete) File: /m.test(patch2)) return patch2;
466
+ const out = [];
467
+ let open = false;
468
+ for (const line of patch2.split("\n")) {
469
+ const file = /^\*\*\* (Update|Add|Delete) File: (.+?)\s*$/.exec(line);
470
+ if (file) {
471
+ const [, verb, raw] = file;
472
+ const path = raw.trim();
473
+ if (!PATH.test(path)) return patch2;
474
+ out.push(`diff --git a/${path} b/${path}`);
475
+ if (verb === "Add") out.push("new file mode 100644", "--- /dev/null", `+++ b/${path}`);
476
+ else if (verb === "Delete") out.push(`--- a/${path}`, "+++ /dev/null");
477
+ else out.push(`--- a/${path}`, `+++ b/${path}`);
478
+ open = true;
479
+ continue;
480
+ }
481
+ if (/^\*\*\* /.test(line)) continue;
482
+ if (!open) continue;
483
+ if (/^@@/.test(line)) {
484
+ out.push("@@ -1 +1 @@");
485
+ continue;
486
+ }
487
+ out.push(line);
488
+ }
489
+ if (!open) return patch2;
490
+ return `${out.join("\n").replace(/\n+$/, "")}
491
+ `;
450
492
  }
451
493
  function validateCodePatch(rawPatch, maxBytes) {
452
494
  const patch2 = stripPatchEnvelope(rawPatch);
@@ -462,7 +504,7 @@ function validateCodePatch(rawPatch, maxBytes) {
462
504
  if (FORBIDDEN.test(patch2) || /(?:old|new)(?: file)? mode 120000/.test(patch2)) {
463
505
  throw new TypeError("patch uses a forbidden binary, link, mode, rename, or copy operation");
464
506
  }
465
- const paths2 = [];
507
+ const paths = [];
466
508
  const lines = patch2.split("\n");
467
509
  for (let index = 0; index < lines.length; index += 1) {
468
510
  const line = lines[index];
@@ -478,10 +520,10 @@ function validateCodePatch(rawPatch, maxBytes) {
478
520
  if (!validHeaderPath(oldPath, path, "a") || !validHeaderPath(newPath, path, "b")) {
479
521
  throw new TypeError("patch file headers do not match the declared path");
480
522
  }
481
- paths2.push(path);
523
+ paths.push(path);
482
524
  }
483
- if (!paths2.length || new Set(paths2).size !== paths2.length) throw new TypeError("patch has no diffs or repeats a path");
484
- return paths2;
525
+ if (!paths.length || new Set(paths).size !== paths.length) throw new TypeError("patch has no diffs or repeats a path");
526
+ return paths;
485
527
  }
486
528
  function validHeaderPath(value, path, prefix) {
487
529
  return value === "/dev/null" || value === `${prefix}/${path}`;
@@ -499,18 +541,21 @@ function resolveCodePath(workspaceDir, path) {
499
541
  if (target !== root && !target.startsWith(`${root}${sep}`)) throw new TypeError("path escapes the staged workspace");
500
542
  return target;
501
543
  }
544
+ function hasContextFreeHunk(patch2) {
545
+ const bodies = patch2.split(/^@@.*$/m).slice(1);
546
+ return bodies.some((body) => !body.split("\n").some((line) => line.startsWith(" ") && line.trim().length > 0));
547
+ }
502
548
  function describePatchFailure(patch2, detail) {
503
549
  const hunks = patch2.split("\n").filter((line) => line.startsWith("@@"));
504
- const bodies = patch2.split(/^@@.*$/m).slice(1);
505
- const contextless = bodies.some((body) => !body.split("\n").some((line) => line.startsWith(" ") && line.trim().length > 0));
506
- const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
550
+ const hint = hunks.length > 0 && hasContextFreeHunk(patch2) ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
507
551
  return `patch did not apply: ${detail}${hint}`;
508
552
  }
509
- async function applyCodePatch(workspaceDir, rawPatch, paths2) {
553
+ async function applyCodePatch(workspaceDir, rawPatch, paths) {
510
554
  const patch2 = stripPatchEnvelope(rawPatch);
511
- await gitApply(workspaceDir, patch2, true);
512
- await gitApply(workspaceDir, patch2, false);
513
- for (const path of paths2) {
555
+ const zero = hasContextFreeHunk(patch2);
556
+ await gitApply(workspaceDir, patch2, true, zero);
557
+ await gitApply(workspaceDir, patch2, false, zero);
558
+ for (const path of paths) {
514
559
  try {
515
560
  const info = await lstat(resolveCodePath(workspaceDir, path));
516
561
  if (info.isSymbolicLink() || !info.isFile() && !info.isDirectory()) {
@@ -521,9 +566,16 @@ async function applyCodePatch(workspaceDir, rawPatch, paths2) {
521
566
  }
522
567
  }
523
568
  }
524
- function gitApply(cwd, patch2, check) {
569
+ function gitApply(cwd, patch2, check, unidiffZero = false) {
525
570
  return new Promise((accept, reject) => {
526
- const args = ["apply", "--recount", "--whitespace=nowarn", ...check ? ["--check"] : [], "-"];
571
+ const args = [
572
+ "apply",
573
+ "--recount",
574
+ ...unidiffZero ? ["--unidiff-zero"] : [],
575
+ "--whitespace=nowarn",
576
+ ...check ? ["--check"] : [],
577
+ "-"
578
+ ];
527
579
  const child = spawn("git", args, {
528
580
  cwd,
529
581
  shell: false,
@@ -532,8 +584,8 @@ function gitApply(cwd, patch2, check) {
532
584
  });
533
585
  let stderr = "";
534
586
  child.stderr.setEncoding("utf8");
535
- child.stderr.on("data", (text2) => {
536
- if (stderr.length < 4e3) stderr += text2.slice(0, 4e3);
587
+ child.stderr.on("data", (text) => {
588
+ if (stderr.length < 4e3) stderr += text.slice(0, 4e3);
537
589
  });
538
590
  child.once("error", reject);
539
591
  child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
@@ -563,8 +615,8 @@ async function restoreCodeWorkspaceCheckpoint(input) {
563
615
  const workspace = await stageWorkspace(input.trustedBaseDir, input.stage);
564
616
  try {
565
617
  if (checkpoint.patch) {
566
- const paths2 = validateCodePatch(checkpoint.patch, 256 * 1024);
567
- await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths2);
618
+ const paths = validateCodePatch(checkpoint.patch, 256 * 1024);
619
+ await applyCodePatch(workspace.workspaceDir, checkpoint.patch, paths);
568
620
  }
569
621
  return { workspace, checkpoint };
570
622
  } catch (error) {
@@ -733,8 +785,8 @@ async function verifyCodeCandidate(input) {
733
785
  try {
734
786
  const baseDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
735
787
  if (baseDigest !== input.trustedBaseDigest) throw new TypeError("trusted base does not match its registered digest");
736
- const paths2 = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
737
- await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths2);
788
+ const paths = validateCodePatch(input.candidatePatch, policy.maximumPatchBytes);
789
+ await applyCodePatch(staged.workspaceDir, input.candidatePatch, paths);
738
790
  const sourceDigest = await digestStagedWorkspace(staged.workspaceDir, limits);
739
791
  const policyDigest = digestPolicy(policy);
740
792
  const patchDigest = digestBytes(input.candidatePatch);
@@ -743,7 +795,7 @@ async function verifyCodeCandidate(input) {
743
795
  trustedBaseDigest: input.trustedBaseDigest,
744
796
  patchDigest
745
797
  });
746
- const changedTests = changedTestPaths(paths2, policy);
798
+ const changedTests = changedTestPaths(paths, policy);
747
799
  if (changedTests.length > policy.maximumChangedTests) throw new TypeError("candidate changes too many test files");
748
800
  const recipes = [];
749
801
  const logs = [];
@@ -810,8 +862,8 @@ function validate(input) {
810
862
  }
811
863
  return result;
812
864
  }
813
- function changedTestPaths(paths2, policy) {
814
- return paths2.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
865
+ function changedTestPaths(paths, policy) {
866
+ return paths.filter((path) => policy.testPathSuffixes.some((suffix) => path.endsWith(suffix)) || policy.testPathPrefixes.some((prefix) => path.startsWith(prefix) || path.includes(`/${prefix}`))).sort();
815
867
  }
816
868
  function recipeReceipt(recipe2, result, artifacts) {
817
869
  const status = result.timedOut ? "timed_out" : result.outputLimitExceeded ? "output_limited" : result.exitCode === 0 && artifacts.every((item) => item.status === "verified") ? "passed" : "failed";
@@ -935,9 +987,9 @@ async function prepareRuntimeCheckpoint(input) {
935
987
  if (evidence.receipt.outcome === "passed") {
936
988
  verification = evidence.receipt;
937
989
  review = await input.review(patch2, verification);
938
- note = review.verdict === "approved" ? "Clean verification and independent review passed" : "Clean verification passed; independent review rejected the candidate";
990
+ note = describeReview(review);
939
991
  } else {
940
- note = `Clean verification failed: ${evidence.receipt.recipes.filter((recipe2) => recipe2.status !== "passed").map((recipe2) => `${recipe2.recipeId}=${recipe2.status}`).join(", ")}`;
992
+ note = describeGateFailure(evidence);
941
993
  }
942
994
  } catch (cause) {
943
995
  note = `Candidate verification or review failed closed: ${message(cause)}`;
@@ -962,6 +1014,23 @@ async function prepareRuntimeCheckpoint(input) {
962
1014
  });
963
1015
  return { checkpoint, verification, review, note };
964
1016
  }
1017
+ function describeReview(review) {
1018
+ const findings = review.findings.map((finding) => ` - [${finding.severity}] ${finding.detail}`).join("\n");
1019
+ const head = review.verdict === "approved" ? `Clean verification and independent review passed (score ${review.score}/100).` : `Clean verification passed; independent review REJECTED this candidate (score ${review.score}/100). Address the findings and checkpoint again.`;
1020
+ return [head, review.summary, findings].filter(Boolean).join("\n").slice(0, 4e3);
1021
+ }
1022
+ function describeGateFailure(evidence) {
1023
+ const failed = evidence.receipt.recipes.filter((recipe2) => recipe2.status !== "passed");
1024
+ const detail = failed.map((recipe2) => {
1025
+ const log = evidence.logs.find((entry) => entry.recipeId === recipe2.recipeId);
1026
+ const output = `${log?.stdout ?? ""}
1027
+ ${log?.stderr ?? ""}`.trim();
1028
+ return `${recipe2.recipeId}=${recipe2.status}${output ? `
1029
+ ${output.slice(0, 1500)}` : ""}`;
1030
+ }).join("\n\n");
1031
+ return `Clean verification failed. Fix this and checkpoint again:
1032
+ ${detail}`.slice(0, 4e3);
1033
+ }
965
1034
  var message = (value) => (value instanceof Error ? value.message : String(value)).slice(0, 500);
966
1035
 
967
1036
  // src/code-runtime-checkpoint-manager.ts
@@ -1618,7 +1687,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
1618
1687
  files(root) {
1619
1688
  const existing = cache2.get(root);
1620
1689
  if (existing) return existing;
1621
- const pending = enumerate(root, limit).then((paths2) => Object.freeze(paths2));
1690
+ const pending = enumerate(root, limit).then((paths) => Object.freeze(paths));
1622
1691
  cache2.set(root, pending);
1623
1692
  void pending.catch(() => {
1624
1693
  if (cache2.get(root) === pending) cache2.delete(root);
@@ -1631,7 +1700,7 @@ function createWorkspaceFileRegistry(limit = DEFAULT_MAX_FILES, enumerate = regi
1631
1700
  };
1632
1701
  }
1633
1702
  async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1634
- const paths2 = [];
1703
+ const paths = [];
1635
1704
  const walk = async (directory) => {
1636
1705
  for (const entry of await readdir2(directory, { withFileTypes: true })) {
1637
1706
  if (SKIP_WORKSPACE_DIRS.has(entry.name)) continue;
@@ -1645,26 +1714,26 @@ async function registeredFiles(root, limit = DEFAULT_MAX_FILES) {
1645
1714
  } catch {
1646
1715
  continue;
1647
1716
  }
1648
- paths2.push(path);
1649
- if (paths2.length > limit) throw new TypeError("workspace file registry exceeds its bound");
1717
+ paths.push(path);
1718
+ if (paths.length > limit) throw new TypeError("workspace file registry exceeds its bound");
1650
1719
  }
1651
1720
  }
1652
1721
  };
1653
1722
  await walk(resolve4(root));
1654
- return paths2.sort();
1723
+ return paths.sort();
1655
1724
  }
1656
- function listWorkspace(paths2, options = {}) {
1725
+ function listWorkspace(paths, options = {}) {
1657
1726
  const max = options.maxEntries ?? 1e3;
1658
1727
  const prefix = options.prefix?.replace(/\/+$/, "");
1659
- const scoped = prefix ? paths2.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths2];
1728
+ const scoped = prefix ? paths.filter((path) => path === prefix || path.startsWith(`${prefix}/`)) : [...paths];
1660
1729
  return scoped.slice(0, max);
1661
1730
  }
1662
- async function searchWorkspace(root, paths2, options) {
1731
+ async function searchWorkspace(root, paths, options) {
1663
1732
  options.signal?.throwIfAborted();
1664
1733
  if (!options.query) throw new TypeError("search query must be a non-empty string");
1665
1734
  const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
1666
1735
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
1667
- const scoped = listWorkspace(paths2, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths2.length });
1736
+ const scoped = listWorkspace(paths, { ...options.prefix ? { prefix: options.prefix } : {}, maxEntries: paths.length });
1668
1737
  if (scoped.length === 0) return [];
1669
1738
  try {
1670
1739
  return await nativeSearch(root, scoped, { ...options, maxResults, maxFileBytes });
@@ -1674,11 +1743,11 @@ async function searchWorkspace(root, paths2, options) {
1674
1743
  }
1675
1744
  }
1676
1745
  var MAX_NATIVE_ARG_BYTES = 96 * 1024;
1677
- async function nativeSearch(root, paths2, options) {
1746
+ async function nativeSearch(root, paths, options) {
1678
1747
  const batches = [];
1679
1748
  let batch = [];
1680
1749
  let bytes = 0;
1681
- for (const path of paths2) {
1750
+ for (const path of paths) {
1682
1751
  const size = Buffer.byteLength(path) + 1;
1683
1752
  if (batch.length > 0 && bytes + size > MAX_NATIVE_ARG_BYTES) {
1684
1753
  batches.push(batch);
@@ -1697,7 +1766,7 @@ async function nativeSearch(root, paths2, options) {
1697
1766
  }
1698
1767
  return matches;
1699
1768
  }
1700
- function nativeSearchBatch(root, paths2, options, remaining) {
1769
+ function nativeSearchBatch(root, paths, options, remaining) {
1701
1770
  return new Promise((resolveMatches, reject) => {
1702
1771
  const args = [
1703
1772
  "--fixed-strings",
@@ -1710,7 +1779,7 @@ function nativeSearchBatch(root, paths2, options, remaining) {
1710
1779
  options.caseSensitive === false ? "--ignore-case" : "--case-sensitive",
1711
1780
  "--",
1712
1781
  options.query,
1713
- ...paths2
1782
+ ...paths
1714
1783
  ];
1715
1784
  const child = spawn3("rg", args, {
1716
1785
  cwd: root,
@@ -1908,16 +1977,16 @@ function createCodePolicyGate(options) {
1908
1977
  }
1909
1978
  };
1910
1979
  }
1911
- function directoryPrefixes(paths2) {
1980
+ function directoryPrefixes(paths) {
1912
1981
  const prefixes = /* @__PURE__ */ new Set(["."]);
1913
- for (const path of paths2) {
1982
+ for (const path of paths) {
1914
1983
  const parts = path.split("/");
1915
1984
  for (let index = 1; index < parts.length; index += 1) prefixes.add(parts.slice(0, index).join("/"));
1916
1985
  }
1917
1986
  return [...prefixes].sort();
1918
1987
  }
1919
- async function safePrefix(base, paths2, prefix) {
1920
- const prefixes = directoryPrefixes(paths2);
1988
+ async function safePrefix(base, paths, prefix) {
1989
+ const prefixes = directoryPrefixes(paths);
1921
1990
  const conversions = await conversionRegistry(
1922
1991
  [await registeredPolicy("code.prefix.v1", "code.prefixes.v1", prefixes)],
1923
1992
  { "code.prefixes.v1": prefixes }
@@ -2027,8 +2096,14 @@ function optionalInteger(value) {
2027
2096
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
2028
2097
  return value;
2029
2098
  }
2099
+ var DEFAULT_FAILURE_REASON = {
2100
+ "tool denied by CaMeL policy": "tool denied by CaMeL policy",
2101
+ "review sessions are read-only": "review session is read-only; workspace changes are not permitted"
2102
+ };
2030
2103
  function response(request, ok, content, details) {
2031
- return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
2104
+ if (ok) return { requestId: request.requestId, ok, content, ...details ? { details } : {} };
2105
+ const failureReason = typeof details?.failureReason === "string" && details.failureReason || DEFAULT_FAILURE_REASON[content] || content || "tool request failed; inspect the tool input and workspace state";
2106
+ return { requestId: request.requestId, ok, content, details: { ...details, failureReason } };
2032
2107
  }
2033
2108
 
2034
2109
  // src/code-tool-reads.ts
@@ -2047,7 +2122,7 @@ import {
2047
2122
  } from "@odla-ai/graph";
2048
2123
  import { buildCodeGraph, FILE, IMPORTS, PACKAGE, READS, SYMBOL, WRITES } from "@odla-ai/graph/code";
2049
2124
  var cache = /* @__PURE__ */ new Map();
2050
- function workspaceGraphs(workspaceDir, paths2) {
2125
+ function workspaceGraphs(workspaceDir, paths) {
2051
2126
  const existing = cache.get(workspaceDir);
2052
2127
  if (existing) return existing;
2053
2128
  const read2 = (path) => readFile3(join3(workspaceDir, path), "utf8");
@@ -2055,7 +2130,7 @@ function workspaceGraphs(workspaceDir, paths2) {
2055
2130
  // No knownTables: a staged workspace may not carry migrations, and a filter
2056
2131
  // that silently drops every table is worse than an unfiltered one. Callers
2057
2132
  // with ground truth should build the graph themselves.
2058
- graph: await buildCodeGraph({ paths: paths2, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2133
+ graph: await buildCodeGraph({ paths, read: read2, data: { ignore: (path) => path.includes(".generated.") } })
2059
2134
  }))();
2060
2135
  cache.set(workspaceDir, built);
2061
2136
  return built;
@@ -2117,11 +2192,11 @@ async function read(context, request, options, policy, registry) {
2117
2192
  if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
2118
2193
  throw new TypeError("requested line range exceeds its bound");
2119
2194
  }
2120
- const paths2 = await registry.files(context.workspaceDir);
2121
- if (!paths2.includes(path)) {
2195
+ const paths = await registry.files(context.workspaceDir);
2196
+ if (!paths.includes(path)) {
2122
2197
  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.`);
2123
2198
  }
2124
- const allowed = await policy.read(policyContext(context, request, options, { paths: paths2, path, startLine, endLine }));
2199
+ const allowed = await policy.read(policyContext(context, request, options, { paths, path, startLine, endLine }));
2125
2200
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2126
2201
  const target = resolveCodePath(context.workspaceDir, path);
2127
2202
  const info = await stat(target);
@@ -2143,16 +2218,16 @@ async function list(context, request, options, policy, registry) {
2143
2218
  const prefix = typeof raw === "string" && raw.length > 0 ? raw : void 0;
2144
2219
  const maxEntries = optionalInteger(request.input.maxEntries) ?? 1e3;
2145
2220
  if (maxEntries > 5e3) throw new TypeError("maxEntries exceeds its bound");
2146
- const paths2 = await registry.files(context.workspaceDir);
2147
- const allowed = await policy.list(policyContext(context, request, options, { paths: paths2, ...prefix ? { prefix } : {} }));
2221
+ const paths = await registry.files(context.workspaceDir);
2222
+ const allowed = await policy.list(policyContext(context, request, options, { paths, ...prefix ? { prefix } : {} }));
2148
2223
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2149
- const entries = listWorkspace(paths2, { ...prefix ? { prefix } : {}, maxEntries });
2224
+ const entries = listWorkspace(paths, { ...prefix ? { prefix } : {}, maxEntries });
2150
2225
  if (!entries.length) {
2151
2226
  return response(request, true, prefix ? `No files under "${prefix}".` : "Workspace is empty.", { count: 0 });
2152
2227
  }
2153
- const truncated = entries.length < paths2.length && entries.length === maxEntries;
2154
- const hint = !prefix && paths2.length > 500 ? `
2155
- \u2026 ${paths2.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2228
+ const truncated = entries.length < paths.length && entries.length === maxEntries;
2229
+ const hint = !prefix && paths.length > 500 ? `
2230
+ \u2026 ${paths.length} files total. sandbox.overview is far cheaper for orientation; use a prefix here once you know the area.` : "";
2156
2231
  return response(
2157
2232
  request,
2158
2233
  true,
@@ -2170,10 +2245,10 @@ async function search(context, request, options, policy, registry) {
2170
2245
  const maxResults = optionalInteger(request.input.maxResults) ?? 100;
2171
2246
  if (maxResults > 500) throw new TypeError("maxResults exceeds its bound");
2172
2247
  const caseSensitive = request.input.caseSensitive === void 0 ? true : request.input.caseSensitive === true;
2173
- const paths2 = await registry.files(context.workspaceDir);
2174
- const allowed = await policy.search(policyContext(context, request, options, { paths: paths2, query, ...prefix ? { prefix } : {} }));
2248
+ const paths = await registry.files(context.workspaceDir);
2249
+ const allowed = await policy.search(policyContext(context, request, options, { paths, query, ...prefix ? { prefix } : {} }));
2175
2250
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2176
- const matches = await searchWorkspace(context.workspaceDir, paths2, {
2251
+ const matches = await searchWorkspace(context.workspaceDir, paths, {
2177
2252
  query,
2178
2253
  maxResults,
2179
2254
  caseSensitive,
@@ -2195,8 +2270,8 @@ async function graphQuery(context, request, options, policy, registry) {
2195
2270
  selector: query
2196
2271
  }));
2197
2272
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2198
- const paths2 = await registry.files(context.workspaceDir);
2199
- const graphs = await workspaceGraphs(context.workspaceDir, paths2);
2273
+ const paths = await registry.files(context.workspaceDir);
2274
+ const graphs = await workspaceGraphs(context.workspaceDir, paths);
2200
2275
  if (request.tool === "sandbox.overview") {
2201
2276
  return response(request, true, renderOverview(graphs, query || void 0));
2202
2277
  }
@@ -2260,16 +2335,16 @@ function toolFailureMessage(reason) {
2260
2335
  async function patch(context, request, options, policy, registry) {
2261
2336
  exactKeys(request.input, ["patch"]);
2262
2337
  const value = stringField(request.input, "patch");
2263
- const paths2 = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2264
- if (paths2.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2338
+ const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
2339
+ if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
2265
2340
  throw new TypeError("patch targets a read-only reference source");
2266
2341
  }
2267
2342
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
2268
2343
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
2269
- await applyCodePatch(context.workspaceDir, value, paths2);
2344
+ await applyCodePatch(context.workspaceDir, value, paths);
2270
2345
  registry.invalidate(context.workspaceDir);
2271
2346
  forgetWorkspaceGraphs(context.workspaceDir);
2272
- return response(request, true, `Applied patch to ${paths2.length} file(s).`, { paths: paths2 });
2347
+ return response(request, true, `Applied patch to ${paths.length} file(s).`, { paths });
2273
2348
  }
2274
2349
  async function recipe(context, request, options, recipes, policy) {
2275
2350
  exactKeys(request.input, ["recipeId"]);
@@ -2660,122 +2735,6 @@ async function appendCodeRuntimeEvent(control, command, event, refs) {
2660
2735
  var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
2661
2736
  var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
2662
2737
 
2663
- // src/code-tool-presentation.ts
2664
- var text = (value, maximum) => {
2665
- if (typeof value !== "string") return void 0;
2666
- const bounded = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim();
2667
- return bounded ? bounded.slice(0, maximum) : void 0;
2668
- };
2669
- var integer2 = (value) => Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : void 0;
2670
- var record3 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2671
- var excerpt = (value, tail = false) => {
2672
- if (typeof value !== "string") return void 0;
2673
- const safe = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
2674
- const source = (tail ? safe.slice(-1e4) : safe.slice(0, 1e4)).trim();
2675
- if (!source) return void 0;
2676
- const lines = source.split("\n").filter((line) => line.trim()).map((line) => line.slice(0, 240));
2677
- const selected = tail ? lines.slice(-10) : lines.slice(0, 10);
2678
- return text(selected.join("\n"), 2400);
2679
- };
2680
- var paths = (value) => {
2681
- if (!Array.isArray(value)) return void 0;
2682
- const items = value.flatMap((item) => {
2683
- const path = text(item, 1024);
2684
- return path ? [path] : [];
2685
- }).slice(0, 12);
2686
- return items.length ? items : void 0;
2687
- };
2688
- function patchStats(value) {
2689
- if (typeof value !== "string") return {};
2690
- let additions = 0;
2691
- let deletions = 0;
2692
- for (const line of value.slice(0, 262144).split("\n")) {
2693
- if (line.startsWith("+++") || line.startsWith("---")) continue;
2694
- if (line.startsWith("+")) additions += 1;
2695
- else if (line.startsWith("-")) deletions += 1;
2696
- }
2697
- return { ...additions ? { additions } : {}, ...deletions ? { deletions } : {} };
2698
- }
2699
- function searchResults(value) {
2700
- if (typeof value !== "string") return void 0;
2701
- const results = value.split("\n").flatMap((line) => {
2702
- const match = /^([^:\n]{1,1024}):(\d+):\s?(.*)$/.exec(line);
2703
- if (!match) return [];
2704
- const lineNumber = Number(match[2]);
2705
- const itemText = text(match[3], 240);
2706
- if (!Number.isSafeInteger(lineNumber) || lineNumber < 1) return [];
2707
- return [{ path: match[1], line: lineNumber, ...itemText ? { text: itemText } : {} }];
2708
- }).slice(0, 5);
2709
- return results.length ? results : void 0;
2710
- }
2711
- function codeToolRequestPresentation(request) {
2712
- const input = request.input;
2713
- if (request.tool === "sandbox.read") {
2714
- const path = text(input.path, 1024);
2715
- if (!path) return void 0;
2716
- const startLine = integer2(input.startLine);
2717
- const endLine = integer2(input.endLine);
2718
- return { kind: "read", path, ...startLine ? { startLine } : {}, ...endLine ? { endLine } : {} };
2719
- }
2720
- if (request.tool === "sandbox.list") {
2721
- const scope = text(input.prefix, 1024);
2722
- return { kind: "list", ...scope ? { scope } : {} };
2723
- }
2724
- if (request.tool === "sandbox.search" || request.tool === "sandbox.overview" || request.tool === "sandbox.where_is" || request.tool === "sandbox.who_imports" || request.tool === "sandbox.who_touches") {
2725
- const query = text(input.query, 512);
2726
- const scope = request.tool === "sandbox.search" ? text(input.prefix, 1024) : void 0;
2727
- if (request.tool === "sandbox.search" && !query) return void 0;
2728
- return { kind: "query", ...query ? { query } : {}, ...scope ? { scope } : {} };
2729
- }
2730
- if (request.tool === "sandbox.apply_patch") {
2731
- return { kind: "patch", ...patchStats(input.patch) };
2732
- }
2733
- const recipeId = text(input.recipeId, 120);
2734
- return recipeId ? { kind: "recipe", recipeId } : void 0;
2735
- }
2736
- function codeToolResultPresentation(request, response2) {
2737
- const started = codeToolRequestPresentation(request);
2738
- if (!started || !response2.ok) return started;
2739
- const details = record3(response2.details);
2740
- if (started.kind === "read") {
2741
- return {
2742
- ...started,
2743
- ...integer2(details?.startLine) ? { startLine: integer2(details?.startLine) } : {},
2744
- ...integer2(details?.endLine) ? { endLine: integer2(details?.endLine) } : {},
2745
- ...excerpt(response2.content) ? { excerpt: excerpt(response2.content) } : {}
2746
- };
2747
- }
2748
- if (started.kind === "list") {
2749
- const listed = response2.content.split("\n").filter((line) => line && !line.startsWith("\u2026") && !line.startsWith("Workspace ")).map((line) => text(line, 1024)).filter((line) => Boolean(line)).slice(0, 8);
2750
- return {
2751
- ...started,
2752
- ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
2753
- ...listed.length ? { paths: listed } : {}
2754
- };
2755
- }
2756
- if (started.kind === "query") {
2757
- const results = request.tool === "sandbox.search" ? searchResults(response2.content) : void 0;
2758
- const resultExcerpt = request.tool === "sandbox.search" ? void 0 : excerpt(response2.content);
2759
- return {
2760
- ...started,
2761
- ...integer2(details?.count) !== void 0 ? { count: integer2(details?.count) } : {},
2762
- ...results ? { results } : {},
2763
- ...resultExcerpt ? { excerpt: resultExcerpt } : {}
2764
- };
2765
- }
2766
- if (started.kind === "patch") {
2767
- return { ...started, ...paths(details?.paths) ? { paths: paths(details?.paths) } : {} };
2768
- }
2769
- const output = response2.content.replace(/^Recipe [^\n]*\.?\s*/u, "");
2770
- return {
2771
- ...started,
2772
- ...integer2(details?.exitCode) !== void 0 ? { exitCode: integer2(details?.exitCode) } : {},
2773
- ...typeof details?.timedOut === "boolean" ? { timedOut: details.timedOut } : {},
2774
- ...typeof details?.outputLimitExceeded === "boolean" ? { outputLimitExceeded: details.outputLimitExceeded } : {},
2775
- ...excerpt(output, true) ? { excerpt: excerpt(output, true) } : {}
2776
- };
2777
- }
2778
-
2779
2738
  // src/code-runtime-acknowledgement-gate.ts
2780
2739
  function codeRuntimeAcknowledgementGate(signal) {
2781
2740
  let settle;
@@ -2795,6 +2754,57 @@ function codeRuntimeAcknowledgementGate(signal) {
2795
2754
  return { ready, release };
2796
2755
  }
2797
2756
 
2757
+ // src/code-runtime-session-activity.ts
2758
+ function observeCodeRuntimeSessionSkills(command, skills, emit) {
2759
+ return skills.map((skill) => ({
2760
+ ...skill,
2761
+ tools: skill.tools.map((tool) => {
2762
+ if (!tool.handler) return tool;
2763
+ const handler = tool.handler;
2764
+ return {
2765
+ ...tool,
2766
+ handler: async (input, context) => {
2767
+ const startedAt = Date.now();
2768
+ const operationId = digestRuntimeValue(
2769
+ `${command.commandId}:${skill.name}:${tool.name}:${context.toolCallId ?? "missing"}`
2770
+ );
2771
+ await emit({
2772
+ type: "collaboration",
2773
+ phase: "started",
2774
+ skill: skill.name,
2775
+ tool: tool.name,
2776
+ operationId
2777
+ }).catch(() => void 0);
2778
+ try {
2779
+ const output = await handler(input, context);
2780
+ await emit({
2781
+ type: "collaboration",
2782
+ phase: "completed",
2783
+ skill: skill.name,
2784
+ tool: tool.name,
2785
+ operationId,
2786
+ ok: output.isError !== true,
2787
+ durationMs: Date.now() - startedAt
2788
+ }).catch(() => void 0);
2789
+ return output;
2790
+ } catch (cause) {
2791
+ await emit({
2792
+ type: "collaboration",
2793
+ phase: "completed",
2794
+ skill: skill.name,
2795
+ tool: tool.name,
2796
+ operationId,
2797
+ ok: false,
2798
+ durationMs: Date.now() - startedAt
2799
+ }).catch(() => void 0);
2800
+ throw cause;
2801
+ }
2802
+ }
2803
+ };
2804
+ })
2805
+ }));
2806
+ }
2807
+
2798
2808
  // src/code-runtime-engine.ts
2799
2809
  var TheseusRuntimeEngine = class {
2800
2810
  constructor(options) {
@@ -2992,7 +3002,7 @@ var TheseusRuntimeEngine = class {
2992
3002
  event: (event) => this.#event(command, event, active.conversationRefs)
2993
3003
  });
2994
3004
  await this.#event(command, { type: "status", status: "running" }, active.conversationRefs);
2995
- const extraSkills = await sessionSkillsFor(this.options, command);
3005
+ const extraSkills = observeCodeRuntimeSessionSkills(command, await sessionSkillsFor(this.options, command), (event) => this.#event(command, event, active.conversationRefs));
2996
3006
  const result = await this.#attempt({
2997
3007
  inference,
2998
3008
  broker,
@@ -3031,36 +3041,11 @@ var TheseusRuntimeEngine = class {
3031
3041
  }
3032
3042
  /** Report every brokered effect as it starts and finishes. */
3033
3043
  #observed(command, active, broker) {
3034
- return {
3035
- execute: async (context, request) => {
3036
- const startedAt = Date.now();
3037
- const operationId = digestRuntimeValue(`${command.commandId}:${request.requestId}`);
3038
- const startedPresentation = codeToolRequestPresentation(request);
3039
- await this.#event(
3040
- command,
3041
- {
3042
- type: "tool",
3043
- phase: "started",
3044
- tool: request.tool,
3045
- operationId,
3046
- ...startedPresentation ? { presentation: startedPresentation } : {}
3047
- },
3048
- active.conversationRefs
3049
- ).catch(() => void 0);
3050
- const response2 = await broker.execute(context, request);
3051
- const completedPresentation = codeToolResultPresentation(request, response2);
3052
- await this.#event(command, {
3053
- type: "tool",
3054
- phase: "completed",
3055
- tool: request.tool,
3056
- ok: response2.ok,
3057
- durationMs: Date.now() - startedAt,
3058
- operationId,
3059
- ...completedPresentation ? { presentation: completedPresentation } : {}
3060
- }, active.conversationRefs).catch(() => void 0);
3061
- return response2;
3062
- }
3063
- };
3044
+ return observedBroker({
3045
+ broker,
3046
+ operationIdFor: (requestId) => digestRuntimeValue(`${command.commandId}:${requestId}`),
3047
+ emit: (event) => this.#event(command, event, active.conversationRefs).catch(() => void 0)
3048
+ });
3064
3049
  }
3065
3050
  async #checkpoint(command) {
3066
3051
  const active = this.#active.get(command.sessionId);
@@ -3097,9 +3082,11 @@ export {
3097
3082
  runCodeRuntimeHeartbeatLoop,
3098
3083
  CodeRuntimeReconciler,
3099
3084
  stripPatchEnvelope,
3085
+ applyPatchDialectToDiff,
3100
3086
  validateCodePatch,
3101
3087
  validateRelativePath,
3102
3088
  resolveCodePath,
3089
+ hasContextFreeHunk,
3103
3090
  describePatchFailure,
3104
3091
  applyCodePatch,
3105
3092
  createCodeWorkspaceCheckpoint,
@@ -3134,4 +3121,4 @@ export {
3134
3121
  runGoal,
3135
3122
  TheseusRuntimeEngine
3136
3123
  };
3137
- //# sourceMappingURL=chunk-NG7AYYH3.js.map
3124
+ //# sourceMappingURL=chunk-Q4NL5XO3.js.map