@odla-ai/harness 0.10.3 → 0.11.0

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.
@@ -7,7 +7,7 @@ import {
7
7
  createCodeRuntimeControlClient,
8
8
  createCodeRuntimeSessionSkillLoader,
9
9
  runCodeRuntimeHeartbeatLoop
10
- } from "./chunk-TMBYA5JX.js";
10
+ } from "./chunk-IXYKIPRA.js";
11
11
  import "./chunk-KXFI3WM2.js";
12
12
  import {
13
13
  assertPinnedImage,
package/dist/node.cjs CHANGED
@@ -27,6 +27,7 @@ __export(node_exports, {
27
27
  DecompositionError: () => DecompositionError,
28
28
  MAX_MEMORY_BODY: () => MAX_MEMORY_BODY,
29
29
  MEASURED_PREMIUM: () => MEASURED_PREMIUM,
30
+ REPOSITORY_RECIPES_FILE: () => REPOSITORY_RECIPES_FILE,
30
31
  SYSTEM_PROMPT_FOR: () => SYSTEM_PROMPT_FOR,
31
32
  TheseusRuntimeEngine: () => TheseusRuntimeEngine,
32
33
  V1_SYSTEM_PROMPT: () => V1_SYSTEM_PROMPT,
@@ -48,6 +49,7 @@ __export(node_exports, {
48
49
  createCodeToolBroker: () => createCodeToolBroker,
49
50
  createCodeWorkspaceCheckpoint: () => createCodeWorkspaceCheckpoint,
50
51
  createContainerRecipeExecutor: () => createContainerRecipeExecutor,
52
+ describeCodeRecipes: () => describeCodeRecipes,
51
53
  describePatchFailure: () => describePatchFailure,
52
54
  digestStagedWorkspace: () => digestStagedWorkspace,
53
55
  feedbackIsActionable: () => feedbackIsActionable,
@@ -62,15 +64,19 @@ __export(node_exports, {
62
64
  materializeGitTree: () => materializeGitTree,
63
65
  outcomeCloses: () => outcomeCloses,
64
66
  outcomeMemory: () => outcomeMemory,
67
+ parseRepositoryRecipes: () => parseRepositoryRecipes,
65
68
  patchPaths: () => patchPaths,
66
69
  planReachCollisions: () => planReachCollisions,
67
70
  prepareRuntimeCheckpoint: () => prepareRuntimeCheckpoint,
68
71
  racedAttempt: () => racedAttempt,
72
+ readOnlyNotice: () => readOnlyNotice,
73
+ readRepositoryRecipes: () => readRepositoryRecipes,
69
74
  recallAbout: () => recallAbout,
70
75
  registeredFiles: () => registeredFiles,
71
76
  renderMemories: () => renderMemories,
72
77
  renderOutcome: () => renderOutcome,
73
78
  resolveCodePath: () => resolveCodePath,
79
+ resolveCodeRecipes: () => resolveCodeRecipes,
74
80
  restoreCodeWorkspaceCheckpoint: () => restoreCodeWorkspaceCheckpoint,
75
81
  runCodeAgent: () => runCodeAgent,
76
82
  runCodeAgentAttempt: () => runCodeAgentAttempt,
@@ -83,6 +89,7 @@ __export(node_exports, {
83
89
  safeWorkspaceLabel: () => safeWorkspaceLabel,
84
90
  selectContainerEngine: () => selectContainerEngine,
85
91
  selectWinner: () => selectWinner,
92
+ sessionRecipesFor: () => sessionRecipesFor,
86
93
  sessionSkillsFor: () => sessionSkillsFor,
87
94
  stageWorkspace: () => stageWorkspace,
88
95
  stageWorkspacePair: () => stageWorkspacePair,
@@ -1999,8 +2006,8 @@ var CodeRuntimeCheckpointManager = class {
1999
2006
  trustedBaseDigest: active.trustedBaseDigest,
2000
2007
  planningInputDigest: active.planningInputDigest,
2001
2008
  conversationRefs: active.conversationRefs,
2002
- fallbackPolicyDigest: this.options.fallbackPolicyDigest,
2003
- recipes: this.options.recipes,
2009
+ fallbackPolicyDigest: active.buildPolicyDigest ?? this.options.fallbackPolicyDigest,
2010
+ recipes: active.recipes ?? this.options.recipes,
2004
2011
  recipeExecutor: this.options.recipeExecutor,
2005
2012
  review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
2006
2013
  });
@@ -2831,6 +2838,10 @@ function codeSkill(opts) {
2831
2838
  }
2832
2839
 
2833
2840
  // src/code-agent.ts
2841
+ function readOnlyNotice(readOnly, recipes) {
2842
+ if (!readOnly) return "";
2843
+ return recipes ? "\n\nThis session is read-only: inspect and report; repository mutation is intentionally unavailable. The registered proof recipes are available through odla_run_recipe and must be run before any verdict." : "\n\nThis session is read-only. Inspect and report; repository mutation and recipe execution are intentionally unavailable.";
2844
+ }
2834
2845
  async function runCodeAgent(options) {
2835
2846
  const toolCalls = [];
2836
2847
  const surface = options.surface ?? "v1";
@@ -2840,6 +2851,7 @@ async function runCodeAgent(options) {
2840
2851
  workspaceDir: options.workspaceDir,
2841
2852
  surface,
2842
2853
  ...options.readOnly === void 0 ? {} : { readOnly: options.readOnly },
2854
+ ...options.recipes === void 0 ? {} : { recipes: options.recipes },
2843
2855
  ...options.recipeIds ? { recipeIds: options.recipeIds } : {},
2844
2856
  onToolCall: (call) => {
2845
2857
  toolCalls.push(call);
@@ -2852,7 +2864,7 @@ async function runCodeAgent(options) {
2852
2864
  {
2853
2865
  name: "odla-code",
2854
2866
  model: options.model,
2855
- system: options.system ?? `${SYSTEM_PROMPT_FOR[surface]}${options.readOnly ? "\n\nThis session is read-only. Inspect and report; repository mutation and recipe execution are intentionally unavailable." : ""}`,
2867
+ system: options.system ?? `${SYSTEM_PROMPT_FOR[surface]}${readOnlyNotice(options.readOnly, options.recipes)}`,
2856
2868
  skills: [skill, ...options.extraSkills ?? []],
2857
2869
  maxSteps: options.maxSteps ?? 24,
2858
2870
  maxTokens: options.maxTokens ?? 16384
@@ -2936,13 +2948,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
2936
2948
  ...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
2937
2949
  handler: async (input, context) => {
2938
2950
  if (!context.toolCallId) throw new TypeError("collaboration tool call identity is required");
2939
- return execute2(command.sessionId, {
2940
- commandId: command.commandId,
2941
- toolCallId: context.toolCallId,
2942
- skill: manifest.name,
2943
- tool: tool.name,
2944
- input
2945
- }, context.signal);
2951
+ try {
2952
+ return await execute2(command.sessionId, {
2953
+ commandId: command.commandId,
2954
+ toolCallId: context.toolCallId,
2955
+ skill: manifest.name,
2956
+ tool: tool.name,
2957
+ input
2958
+ }, context.signal);
2959
+ } catch (cause) {
2960
+ if (context.signal?.aborted) throw cause;
2961
+ const detail = (cause instanceof Error ? cause.message : String(cause)).slice(0, 500);
2962
+ return { content: `Tool "${tool.name}" failed: ${detail}`, isError: true };
2963
+ }
2946
2964
  }
2947
2965
  }))
2948
2966
  }));
@@ -4005,16 +4023,17 @@ function createCodeRuntimeToolBroker(input, lease, role) {
4005
4023
  readerId: `code-session:${lease.task.taskId}`,
4006
4024
  readOnlyPrefixes: [".odla-references"]
4007
4025
  });
4008
- const reviewReads = /* @__PURE__ */ new Set([
4026
+ const reviewTools = /* @__PURE__ */ new Set([
4009
4027
  "sandbox.read",
4010
4028
  "sandbox.list",
4011
4029
  "sandbox.search",
4012
4030
  "sandbox.overview",
4013
4031
  "sandbox.where_is",
4014
4032
  "sandbox.who_imports",
4015
- "sandbox.who_touches"
4033
+ "sandbox.who_touches",
4034
+ "sandbox.run_recipe"
4016
4035
  ]);
4017
- return role === "coding" ? broker : { execute: (context, request) => reviewReads.has(request.tool) ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
4036
+ return role === "coding" ? broker : { execute: (context, request) => reviewTools.has(request.tool) ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
4018
4037
  }
4019
4038
 
4020
4039
  // src/code-memory.ts
@@ -4410,6 +4429,96 @@ function observeCodeRuntimeSessionSkills(command, skills, emit) {
4410
4429
  }));
4411
4430
  }
4412
4431
 
4432
+ // src/code-repository-recipes.ts
4433
+ var import_promises15 = require("fs/promises");
4434
+ var import_node_path13 = require("path");
4435
+ var REPOSITORY_RECIPES_FILE = "odla.recipes.json";
4436
+ var MAX_RECIPES = 16;
4437
+ var DEFAULT_TIMEOUT_MS = 12e4;
4438
+ var MAX_TIMEOUT_MS = 15 * 6e4;
4439
+ var ID2 = /^[A-Za-z0-9._:-]{1,120}$/;
4440
+ var RECIPE_FIELDS = /* @__PURE__ */ new Set(["id", "command", "timeoutMs"]);
4441
+ var fault = (detail) => new TypeError(`${REPOSITORY_RECIPES_FILE} is malformed: ${detail}`);
4442
+ function parseRepositoryRecipes(text2, envelope) {
4443
+ let parsed;
4444
+ try {
4445
+ parsed = JSON.parse(text2);
4446
+ } catch {
4447
+ throw fault("not valid JSON");
4448
+ }
4449
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw fault("the document must be an object");
4450
+ const document = parsed;
4451
+ if (document.version !== 1) throw fault("version must be 1");
4452
+ const entries = document.recipes;
4453
+ if (!Array.isArray(entries) || entries.length < 1) throw fault("recipes must be a non-empty array");
4454
+ if (entries.length > MAX_RECIPES) throw fault(`at most ${MAX_RECIPES} recipes may be declared`);
4455
+ const recipes = [];
4456
+ const seen = /* @__PURE__ */ new Set();
4457
+ entries.forEach((entry, index) => {
4458
+ const label = `recipe ${index + 1}`;
4459
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw fault(`${label} must be an object`);
4460
+ const row = entry;
4461
+ const unknown = Object.keys(row).filter((key) => !RECIPE_FIELDS.has(key));
4462
+ if (unknown.length) throw fault(`${label} has unsupported field ${unknown[0]}; the host owns image and resource limits`);
4463
+ if (typeof row.id !== "string" || !ID2.test(row.id)) throw fault(`${label} needs an id of 1 to 120 letters, digits, . _ : or -`);
4464
+ if (seen.has(row.id)) throw fault(`${label} repeats id ${row.id}`);
4465
+ seen.add(row.id);
4466
+ if (!Array.isArray(row.command) || row.command.length < 1 || row.command.some((part) => typeof part !== "string" || !part)) {
4467
+ throw fault(`${label} (${row.id}) needs command as a non-empty array of non-empty strings`);
4468
+ }
4469
+ const timeoutMs = row.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4470
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
4471
+ throw fault(`${label} (${row.id}) timeoutMs must be an integer from 1 to ${MAX_TIMEOUT_MS}`);
4472
+ }
4473
+ const recipe2 = {
4474
+ id: row.id,
4475
+ command: [...row.command],
4476
+ timeoutMs,
4477
+ image: envelope.image,
4478
+ maxOutputBytes: envelope.maxOutputBytes,
4479
+ cpus: envelope.cpus,
4480
+ memory: envelope.memory,
4481
+ pids: envelope.pids
4482
+ };
4483
+ try {
4484
+ assertCodeBuildRecipe(recipe2);
4485
+ } catch (cause) {
4486
+ throw fault(`${label} (${row.id}) is not a runnable recipe: ${cause instanceof Error ? cause.message : String(cause)}`);
4487
+ }
4488
+ recipes.push(recipe2);
4489
+ });
4490
+ return recipes;
4491
+ }
4492
+ async function readRepositoryRecipes(baselineDir, envelope) {
4493
+ let text2;
4494
+ try {
4495
+ text2 = await (0, import_promises15.readFile)((0, import_node_path13.join)(baselineDir, REPOSITORY_RECIPES_FILE), "utf8");
4496
+ } catch (cause) {
4497
+ if (cause.code === "ENOENT") return null;
4498
+ throw cause;
4499
+ }
4500
+ return parseRepositoryRecipes(text2, envelope);
4501
+ }
4502
+ async function resolveCodeRecipes(baselineDir, release, envelope) {
4503
+ const declared = envelope ? await readRepositoryRecipes(baselineDir, envelope) : null;
4504
+ return declared ? { recipes: declared, source: "repository" } : { recipes: release, source: "release" };
4505
+ }
4506
+ function describeCodeRecipes(resolved) {
4507
+ const ids = resolved.recipes.map((recipe2) => recipe2.id).join(", ");
4508
+ return resolved.source === "repository" ? `Verification recipes: declared by the repository in ${REPOSITORY_RECIPES_FILE} \xB7 ${ids}` : `Verification recipes: release-owned (the repository declares none in ${REPOSITORY_RECIPES_FILE}) \xB7 ${ids}`;
4509
+ }
4510
+ async function sessionRecipesFor(workspace, options, resume) {
4511
+ const resolved = await resolveCodeRecipes(workspace.baselineDir, options.recipes, options.repositoryRecipes ?? null).catch(async (cause) => {
4512
+ await workspace.cleanup();
4513
+ throw cause;
4514
+ });
4515
+ return {
4516
+ ...resolved,
4517
+ buildPolicyDigest: digestRuntimeValue(JSON.stringify(resolved.recipes)),
4518
+ note: !resume && resolved.source === "repository" ? describeCodeRecipes(resolved) : null
4519
+ };
4520
+ }
4521
+
4413
4522
  // src/code-runtime-engine.ts
4414
4523
  var TheseusRuntimeEngine = class {
4415
4524
  constructor(options) {
@@ -4461,6 +4570,7 @@ var TheseusRuntimeEngine = class {
4461
4570
  control: this.options.control,
4462
4571
  ...this.options.localSource ? { localSource: this.options.localSource } : {}
4463
4572
  });
4573
+ const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
4464
4574
  const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
4465
4575
  const conversationRefs = [];
4466
4576
  const active = {
@@ -4469,6 +4579,8 @@ var TheseusRuntimeEngine = class {
4469
4579
  conversationRefs,
4470
4580
  acknowledged: false,
4471
4581
  startGate,
4582
+ recipes: resolvedRecipes.recipes,
4583
+ buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
4472
4584
  role: metadata.role,
4473
4585
  readOnly: metadata.readOnly,
4474
4586
  title: metadata.title,
@@ -4486,6 +4598,7 @@ var TheseusRuntimeEngine = class {
4486
4598
  done: Promise.resolve(null)
4487
4599
  };
4488
4600
  this.#active.set(command.sessionId, active);
4601
+ if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
4489
4602
  if (requestedLocal) {
4490
4603
  await this.#event(command, {
4491
4604
  type: "message",
@@ -4521,7 +4634,7 @@ var TheseusRuntimeEngine = class {
4521
4634
  const active = await this.#takeOver(command, "pursue requires an active Code session");
4522
4635
  active.done = startGoalPursuit({
4523
4636
  spec,
4524
- recipes: this.options.recipes,
4637
+ recipes: active.recipes,
4525
4638
  recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
4526
4639
  workspace: active.workspace,
4527
4640
  baseCommitSha: active.baseCommitSha,
@@ -4600,7 +4713,7 @@ var TheseusRuntimeEngine = class {
4600
4713
  async #runAttempt(command, metadata, active) {
4601
4714
  const lease = fakeCodeLease(command, metadata);
4602
4715
  const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
4603
- recipes: this.options.recipes,
4716
+ recipes: active.recipes,
4604
4717
  engine: this.options.engine,
4605
4718
  recipeAuthorization: this.options.recipeAuthorization
4606
4719
  }, lease, metadata.role));
@@ -4626,7 +4739,7 @@ var TheseusRuntimeEngine = class {
4626
4739
  // A review-role session is read-only and still runs the proof; a planner
4627
4740
  // marked read-only by its payload runs nothing.
4628
4741
  recipes: metadata.role === "review" || !metadata.readOnly,
4629
- recipeIds: this.options.recipes.map((recipe2) => recipe2.id),
4742
+ recipeIds: active.recipes.map((recipe2) => recipe2.id),
4630
4743
  ...extraSkills.length ? { extraSkills } : {}
4631
4744
  });
4632
4745
  const closing = result.finalText.trim();
@@ -4957,12 +5070,12 @@ function chooseStrategy(signals = {}) {
4957
5070
  var feedbackIsActionable = actionable;
4958
5071
 
4959
5072
  // src/code-recipe-dependencies.ts
4960
- var import_promises15 = require("fs/promises");
4961
- var import_node_path13 = require("path");
5073
+ var import_promises16 = require("fs/promises");
5074
+ var import_node_path14 = require("path");
4962
5075
  var RESERVED_MOUNTS = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage"]);
4963
5076
  function withRecipeDependencies(executor, dependencies) {
4964
5077
  const mountAs = dependencies.mountAs ?? "node_modules";
4965
- if (!(0, import_node_path13.isAbsolute)(dependencies.source)) {
5078
+ if (!(0, import_node_path14.isAbsolute)(dependencies.source)) {
4966
5079
  throw new TypeError("recipe dependency source must be an absolute path");
4967
5080
  }
4968
5081
  if (!RESERVED_MOUNTS.has(mountAs)) {
@@ -4970,24 +5083,24 @@ function withRecipeDependencies(executor, dependencies) {
4970
5083
  }
4971
5084
  return {
4972
5085
  run: async (input) => {
4973
- const target = (0, import_node_path13.join)(input.workspaceDir, mountAs);
5086
+ const target = (0, import_node_path14.join)(input.workspaceDir, mountAs);
4974
5087
  let linked = false;
4975
5088
  try {
4976
- const existing = await (0, import_promises15.lstat)(target).catch(() => null);
5089
+ const existing = await (0, import_promises16.lstat)(target).catch(() => null);
4977
5090
  if (!existing) {
4978
- await (0, import_promises15.symlink)(dependencies.source, target, "dir");
5091
+ await (0, import_promises16.symlink)(dependencies.source, target, "dir");
4979
5092
  linked = true;
4980
5093
  }
4981
5094
  return await executor.run(input);
4982
5095
  } finally {
4983
- if (linked) await (0, import_promises15.rm)(target, { force: true, recursive: false }).catch(() => void 0);
5096
+ if (linked) await (0, import_promises16.rm)(target, { force: true, recursive: false }).catch(() => void 0);
4984
5097
  }
4985
5098
  }
4986
5099
  };
4987
5100
  }
4988
5101
  async function installedDependencies(repoRoot) {
4989
- const source = (0, import_node_path13.join)(repoRoot, "node_modules");
4990
- const info = await (0, import_promises15.lstat)(source).catch(() => null);
5102
+ const source = (0, import_node_path14.join)(repoRoot, "node_modules");
5103
+ const info = await (0, import_promises16.lstat)(source).catch(() => null);
4991
5104
  return info?.isDirectory() ? { source } : null;
4992
5105
  }
4993
5106
  // Annotate the CommonJS export names for ESM import in node:
@@ -4999,6 +5112,7 @@ async function installedDependencies(repoRoot) {
4999
5112
  DecompositionError,
5000
5113
  MAX_MEMORY_BODY,
5001
5114
  MEASURED_PREMIUM,
5115
+ REPOSITORY_RECIPES_FILE,
5002
5116
  SYSTEM_PROMPT_FOR,
5003
5117
  TheseusRuntimeEngine,
5004
5118
  V1_SYSTEM_PROMPT,
@@ -5020,6 +5134,7 @@ async function installedDependencies(repoRoot) {
5020
5134
  createCodeToolBroker,
5021
5135
  createCodeWorkspaceCheckpoint,
5022
5136
  createContainerRecipeExecutor,
5137
+ describeCodeRecipes,
5023
5138
  describePatchFailure,
5024
5139
  digestStagedWorkspace,
5025
5140
  feedbackIsActionable,
@@ -5034,15 +5149,19 @@ async function installedDependencies(repoRoot) {
5034
5149
  materializeGitTree,
5035
5150
  outcomeCloses,
5036
5151
  outcomeMemory,
5152
+ parseRepositoryRecipes,
5037
5153
  patchPaths,
5038
5154
  planReachCollisions,
5039
5155
  prepareRuntimeCheckpoint,
5040
5156
  racedAttempt,
5157
+ readOnlyNotice,
5158
+ readRepositoryRecipes,
5041
5159
  recallAbout,
5042
5160
  registeredFiles,
5043
5161
  renderMemories,
5044
5162
  renderOutcome,
5045
5163
  resolveCodePath,
5164
+ resolveCodeRecipes,
5046
5165
  restoreCodeWorkspaceCheckpoint,
5047
5166
  runCodeAgent,
5048
5167
  runCodeAgentAttempt,
@@ -5055,6 +5174,7 @@ async function installedDependencies(repoRoot) {
5055
5174
  safeWorkspaceLabel,
5056
5175
  selectContainerEngine,
5057
5176
  selectWinner,
5177
+ sessionRecipesFor,
5058
5178
  sessionSkillsFor,
5059
5179
  stageWorkspace,
5060
5180
  stageWorkspacePair,