@kylecheng3146/agent-ops 0.10.0 → 0.10.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 (49) hide show
  1. package/README.md +3 -2
  2. package/dist/packages/cli/src/args.js +10 -10
  3. package/dist/packages/cli/src/cli.js +14 -4
  4. package/dist/packages/cli/src/commands/advance.js +2 -1
  5. package/dist/packages/cli/src/commands/parallel.js +1 -1
  6. package/dist/packages/cli/src/commands/review.js +3 -1
  7. package/dist/packages/cli/src/commands/run.js +10 -3
  8. package/dist/packages/cli/src/context.js +22 -4
  9. package/dist/packages/cli/src/office-entry.js +8 -4
  10. package/dist/packages/cli/src/owned-run-step.js +4 -4
  11. package/dist/packages/cli/src/parallel-deps.js +0 -2
  12. package/dist/packages/cli/src/run-deps.js +12 -9
  13. package/dist/packages/cli/src/run-entry.js +76 -17
  14. package/dist/packages/cli/src/run-setup-entry.js +2 -2
  15. package/dist/runtime/src/install/harness.js +5 -2
  16. package/dist/runtime/src/office/scene.js +1 -1
  17. package/dist/runtime/src/office/server.js +43 -16
  18. package/dist/runtime/src/office/snapshot.js +1 -1
  19. package/dist/runtime/src/parallel/finish.js +139 -65
  20. package/dist/runtime/src/parallel/service.js +5 -1
  21. package/dist/runtime/src/review/execute.js +9 -7
  22. package/dist/runtime/src/review/packet.js +17 -6
  23. package/dist/runtime/src/review/runner.js +2 -0
  24. package/dist/runtime/src/run/controls.js +99 -1
  25. package/dist/runtime/src/run/hosts/codex.js +28 -1
  26. package/dist/runtime/src/run/hosts/protocol.js +5 -2
  27. package/dist/runtime/src/run/integration.js +35 -13
  28. package/dist/runtime/src/run/policy.js +65 -10
  29. package/dist/runtime/src/run/scheduler.js +10 -5
  30. package/dist/runtime/src/run/service.js +68 -21
  31. package/dist/runtime/src/run/supervisor.js +11 -2
  32. package/dist/runtime/src/run/transport.js +1 -0
  33. package/dist/runtime/src/security/permissions.js +85 -0
  34. package/dist/runtime/src/task/debug-state.js +9 -3
  35. package/dist/runtime/src/task/debug-user-event.js +82 -17
  36. package/dist/runtime/src/task/debug.js +35 -15
  37. package/docs/en/guides/configuration.md +8 -4
  38. package/docs/en/guides/task-debug.md +14 -2
  39. package/docs/en/spec/acceptance-and-evidence.md +4 -0
  40. package/docs/en/spec/harness-adapters.md +26 -1
  41. package/docs/en/spec/loop-engineering.md +34 -6
  42. package/docs/en/spec/review.md +6 -0
  43. package/docs/zh-TW/guides/configuration.md +6 -3
  44. package/docs/zh-TW/guides/task-debug.md +9 -2
  45. package/docs/zh-TW/spec/acceptance-and-evidence.md +2 -1
  46. package/docs/zh-TW/spec/harness-adapters.md +21 -1
  47. package/docs/zh-TW/spec/loop-engineering.md +9 -3
  48. package/docs/zh-TW/spec/review.md +5 -0
  49. package/package.json +1 -1
package/README.md CHANGED
@@ -32,7 +32,7 @@ timeouts produce UNKNOWN. Review examines the checks themselves and every fallba
32
32
  On macOS, a trusted clean main checkout can start an isolated native goal run:
33
33
 
34
34
  ```sh
35
- agent-ops run "Implement the requested behavior" --host codex --time-budget 60m --jobs 2
35
+ agent-ops run "Implement the requested behavior" --host codex --jobs 2
36
36
  agent-ops run status <run-id>
37
37
  agent-ops run logs <run-id>
38
38
  agent-ops run respond <run-id> --question-id <id> --answer "Clarification"
@@ -49,7 +49,8 @@ verification, two fresh reviews, completed tasks and a receipt bound to the targ
49
49
  Setup runs in the background under registered process groups. Stop cancels native
50
50
  writers, local verification and setup; interrupted setup requires explicit resume.
51
51
  The coordinator can assess additional non-dangerous fixed commands through a
52
- policy request. This authorization is bound to the run, runtime, lease and budget;
52
+ policy request. New runs have no total time cap; manual resume upgrades legacy finite runs while keeping accounting.
53
+ This finite authorization is bound to the run, runtime and lease;
53
54
  it creates no permanent repository trust. Policy transitions fence writers and
54
55
  journal delivery imports and contract synchronization before starting new leases.
55
56
  Native authorization decisions retain redacted command scope and rationale;
@@ -960,7 +960,7 @@ function parseRunArgs(argv) {
960
960
  positional = token;
961
961
  continue;
962
962
  }
963
- if (seen.has(token))
963
+ if (seen.has(token) && token !== "--retry-approval")
964
964
  duplicate(token);
965
965
  seen.add(token);
966
966
  if (token === "--json") {
@@ -971,10 +971,15 @@ function parseRunArgs(argv) {
971
971
  args.wait = true;
972
972
  continue;
973
973
  }
974
- if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file"].includes(token))
974
+ if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval"].includes(token))
975
975
  throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
976
976
  const value = readOptionValue(argv, i++, token);
977
977
  switch (token) {
978
+ case "--retry-approval":
979
+ if (!/^auth-[a-f0-9]{64}$/u.test(value) || args.retryApprovalIds?.includes(value))
980
+ invalidValue(token, value);
981
+ args.retryApprovalIds = [...(args.retryApprovalIds ?? []), value];
982
+ break;
978
983
  case "--host":
979
984
  if (value !== "claude" && value !== "codex")
980
985
  invalidValue(token, value);
@@ -998,17 +1003,12 @@ function parseRunArgs(argv) {
998
1003
  args.jobs = Number(value);
999
1004
  break;
1000
1005
  case "--time-budget": {
1001
- const match = value.match(/^([1-9][0-9]*)(s|m|h)$/u);
1002
- if (match === null)
1003
- invalidValue(token, value);
1004
- const ms = Number(match[1]) * ({ s: 1000, m: 60000, h: 3600000 }[match[2]]);
1005
- if (!Number.isSafeInteger(ms) || ms > 86400000)
1006
- invalidValue(token, value);
1007
- args.timeBudgetMs = ms;
1008
- break;
1006
+ throw new CliArgumentError("RUN_TIME_BUDGET_UNSUPPORTED", "New runs have no total time cap; omit --time-budget. Command and review timeouts remain effective.", token);
1009
1007
  }
1010
1008
  }
1011
1009
  }
1010
+ if (args.retryApprovalIds !== undefined && action !== "resume")
1011
+ throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-approval requires run resume.");
1012
1012
  if (action === "start") {
1013
1013
  args.goal = positional;
1014
1014
  if ((args.goal === undefined) === (args.goalFile === undefined))
@@ -98,13 +98,22 @@ browser once; later sessions reuse it. It exits after 10 minutes without active
98
98
  runs or sessions. Recently completed rooms remain for at most 2 hours.
99
99
  The URL carries an access token; do not share it.
100
100
  `,
101
- run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--time-budget <60m>] [--wait] [--json]
101
+ run: `Usage: agent-ops run [start] "<goal>" --host <claude|codex> [--jobs <1|2>] [--wait] [--json]
102
102
  agent-ops run [start] --goal-file <path> --host <claude|codex>
103
103
  agent-ops run <status|logs|resume|stop> <run-id> [--json]
104
+ agent-ops run resume <run-id> [--retry-approval <auth-event-id>]... [--json]
104
105
  agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
105
106
 
107
+ New runs have no total time cap. Manual resume upgrades legacy finite runs and keeps accounting.
108
+ Command/review timeouts, provider limits, DEBUG budgets and stop controls remain effective.
106
109
  Native goal state is observational. Completion requires current verification,
107
110
  two fresh reviews, task completion and a target-bound integration receipt.
111
+ --retry-approval explicitly retries an intact Codex approval-review usage-limit
112
+ failure. Original denials remain recorded; every command needs fresh approval.
113
+ Status includes phase, original cause, recorded sealed proof, cleanup state,
114
+ resumability and remediation. Resume revalidates authority and sealed artifacts;
115
+ receipt-bound recovery executes only missing cleanup steps. Resolve the saved
116
+ blocker before retrying: unchanged cleanup inputs are refused.
108
117
  `,
109
118
  init: `Usage: agent-ops init [options]
110
119
 
@@ -308,9 +317,10 @@ voiding each other's verification and review. Run it from the main checkout.
308
317
  before Git runs; see agent-ops worktree list.
309
318
  finish <name>
310
319
  Once its task is complete, fast-forward the main checkout's
311
- branch to the worktree's (after a clean rebase and
312
- re-verification if the branch moved), record the task in
313
- git notes, and remove the worktree. One finish runs at a time.
320
+ branch to the worktree's, record the task in git notes, and
321
+ remove the worktree. If the branch moved, it rebases the
322
+ worktree and stops: verify and review again with the printed
323
+ --base, then finish again. One finish runs at a time.
314
324
  commit Stage every change in the current worktree and commit it with
315
325
  --message. Refuses the main checkout and a worktree another
316
326
  session owns, and needs no Git in the shell.
@@ -90,7 +90,8 @@ export async function runAdvanceCommand(options) {
90
90
  throw new AgentOpsError("ADVANCE_WORKTREE_MISSING", `No coordinator worktree for session ${options.sessionId}.`);
91
91
  }
92
92
  const savedProof = await readRunIntegrationProof(commonDir, record);
93
- if (savedProof !== null && await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head) {
93
+ if (savedProof !== null && (await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]) === savedProof.head ||
94
+ (await deps.git(mainRoot, ["merge-base", "--is-ancestor", savedProof.head, record.targetBranch])).exitCode === 0)) {
94
95
  const finished = await finishWorktree(deps, { cwd: mainRoot, name, finalProof: { ...savedProof, recovery: true } });
95
96
  return okEnvelope("TASK_ADVANCED", { ...finished, text: "Recovered the sealed integration without moving the target again." });
96
97
  }
@@ -41,7 +41,7 @@ function addText(result) {
41
41
  function finishText(result) {
42
42
  const { record } = result;
43
43
  return [
44
- `Merged ${record.branch} into ${record.targetBranch} at ${result.mergedHead}${result.rebased ? " after a clean rebase and re-verification" : ""}.`,
44
+ `Merged ${record.branch} into ${record.targetBranch} at ${result.mergedHead}.`,
45
45
  `Completed and recorded in git notes (refs/notes/agent-ops): ${result.taskIds.join(", ")}.`,
46
46
  `Removed ${record.path}.`,
47
47
  ...(result.receipt === undefined ? [] : [`Receipt: ${result.receipt}`]),
@@ -816,12 +816,14 @@ export async function runReviewCommand(options) {
816
816
  contractArtifacts.push({ path: executionPath, digest: sha256(execution), content: execution });
817
817
  }
818
818
  }
819
+ // Compact: indentation alone pushed a 494 KiB 0.10.2 manifest past the 512 KiB bound.
819
820
  const content = redactSecrets(JSON.stringify({ binding: context.binding, coverage: context.coverage,
820
821
  executionPolicy: options.config,
822
+ ...(surfacePacket === undefined ? {} : { surface: surfacePacket }),
821
823
  artifacts: contractArtifacts.map(({ path, digest }) => ({ path, digest })),
822
824
  tasks: await Promise.all(context.records.map(async (r) => ({ task: r.task, revisions: r.revisions ?? [],
823
825
  ...(r.task.debug === undefined ? {} : { debugProof: await loadDebugProof(options.root, r.task.debug.rootTaskId) }) }))),
824
- superseded: context.historyRecords ?? [] }, null, 2));
826
+ superseded: context.historyRecords ?? [] }));
825
827
  const digest = sha256(content);
826
828
  const path = ".agent-ops/tasks/review-contracts/" + digest + ".json";
827
829
  contractManifest = { path, digest, content };
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { AgentOpsError, resolveContainedPath } from "../../../../runtime/src/fs/paths.js";
3
+ import { describeRunRecovery } from "../../../../runtime/src/run/service.js";
3
4
  import { errorEnvelope, okEnvelope } from "../output.js";
4
5
  function dynamic(args) {
5
6
  return args;
@@ -19,13 +20,18 @@ function runIdOf(args) {
19
20
  return runId;
20
21
  }
21
22
  function textFor(action, state, message) {
23
+ const recovery = describeRunRecovery(state);
22
24
  const active = state.workers.filter((worker) => ["assigned", "starting", "running", "idle", "handing-off"].includes(worker.status)).length;
23
25
  return [
24
26
  message,
25
27
  `Status: ${state.status}`,
28
+ `Phase: ${recovery.phase}`,
29
+ `Cause: ${recovery.cause === null ? "none recorded" : recovery.cause.code + ": " + recovery.cause.detail}`,
30
+ `Sealed proof: ${recovery.sealedProof}; cleanup: ${recovery.cleanup}; resumable: ${recovery.resumable ? "yes, after reconciliation" : "no"}`,
31
+ `Remediation: ${recovery.remediation}`,
26
32
  `Host: ${state.host}`,
27
33
  `Workers: ${active}/${state.jobs}`,
28
- `Budget: ${Math.max(0, state.budget.limitMs - state.budget.accumulatedMs)}ms remaining`,
34
+ state.budget.limitMs === null ? `Budget: unlimited; ${state.budget.accumulatedMs}ms active time recorded` : `Budget: ${Math.max(0, state.budget.limitMs - state.budget.accumulatedMs)}ms remaining (legacy; manual resume upgrades)`,
29
35
  `Usage: ${state.budget.usage.tokens ?? "unknown"} tokens; ${state.budget.usage.usd === null ? "unknown" : "$" + state.budget.usage.usd} cost (${state.budget.usage.completeness})`,
30
36
  action === "logs" ? "" : "Native goal completion is not final proof; verify, review, task state, and receipt are still required."
31
37
  ].filter((line) => line.length > 0).join("\n");
@@ -35,12 +41,13 @@ function data(action, result) {
35
41
  action,
36
42
  runId: result.state.runId,
37
43
  state: result.state,
44
+ recovery: describeRunRecovery(result.state),
38
45
  message: result.message,
39
46
  text: textFor(action, result.state, result.message)
40
47
  };
41
48
  }
42
49
  function stateData(action, state, message) {
43
- return { action, runId: state.runId, state, message, text: textFor(action, state, message) };
50
+ return { action, runId: state.runId, state, recovery: describeRunRecovery(state), message, text: textFor(action, state, message) };
44
51
  }
45
52
  async function goalFromArgs(args, root) {
46
53
  if (args.goal !== undefined && args.goalFile !== undefined)
@@ -99,7 +106,7 @@ export async function runRunCommand(options) {
99
106
  return okEnvelope("RUN_LOGS", { ...stateData(action, state, `Read ${events.length} event(s) for run ${runId}.`), events });
100
107
  }
101
108
  if (action === "resume")
102
- return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId)));
109
+ return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds)));
103
110
  if (action === "stop")
104
111
  return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
105
112
  const questionId = options.args.questionId;
@@ -9,7 +9,8 @@ import { mergeConfigLayers } from "../../../runtime/src/config/merge.js";
9
9
  import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
10
10
  import { localStatePaths, readPrivateFile } from "../../../runtime/src/security/permissions.js";
11
11
  import { FileRunRepository } from "../../../runtime/src/run/service.js";
12
- import { readRunPolicy, runRuntimeHash } from "../../../runtime/src/run/policy.js";
12
+ import { readRunPolicy, renewRunPolicy, runRuntimeHash } from "../../../runtime/src/run/policy.js";
13
+ import { CLI_VERSION } from "./version.js";
13
14
  import { calculateTrustBinding, FileTrustStore } from "../../../runtime/src/security/trust.js";
14
15
  export const DEFAULT_CONFIG = {
15
16
  schemaVersion: 3,
@@ -83,6 +84,10 @@ export async function loadEffectiveConfig(root, scope, projectOverride) {
83
84
  config: run.policy.config
84
85
  }]);
85
86
  }
87
+ return await loadRepositoryConfig(root, scope, projectOverride);
88
+ }
89
+ /** Read the real config layers when checking renewal, without consulting the run overlay. */
90
+ async function loadRepositoryConfig(root, scope, projectOverride) {
86
91
  const home = process.env.AGENT_OPS_HOME ?? homedir();
87
92
  const userPath = join(home, ".agent-ops", "config.json");
88
93
  const projectPath = join(scope === "user" ? root : projectConfigRoot(root), ".agent-ops", "config.json");
@@ -212,7 +217,7 @@ export async function repositoryTrust(root, config, cliVersion) {
212
217
  }
213
218
  }
214
219
  /** A run-private capability is usable only by its registered checkout or proof group. */
215
- export async function runPolicyContext(root) {
220
+ export async function runPolicyContext(root, renew = false) {
216
221
  let common;
217
222
  try {
218
223
  common = execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
@@ -250,10 +255,23 @@ export async function runPolicyContext(root) {
250
255
  throw new AgentOpsError("RUN_WORKER_STALE", "Run policy requires the current registered worker generation or proof process.");
251
256
  if (saved.status !== "active" || saved.disableRestart || saved.awaitingResume)
252
257
  throw new AgentOpsError("RUN_POLICY_DISABLED", "Run execution policy is inactive; explicit resume is required.");
253
- const policy = await readRunPolicy(saved, await runRuntimeHash());
258
+ const state = renew ? await ensureRunPolicy(repository, saved) : saved;
259
+ const policy = await readRunPolicy(state, await runRuntimeHash());
254
260
  if (policy === null)
255
261
  return null;
256
- return { repository, state: saved, policy, worker };
262
+ return { repository, state, policy, worker };
263
+ }
264
+ /** Only active execution paths call this; status, installation and recovery reads never renew. */
265
+ export async function ensureRunPolicy(repository, state) {
266
+ if (state.policyBinding === undefined || Date.parse(state.policyBinding.expiresAt) > Date.now())
267
+ return state;
268
+ return await renewRunPolicy(repository, state.runId, { authority: async () => {
269
+ const config = (await loadRepositoryConfig(state.root, "project")).config;
270
+ const baseTrustBinding = await repositoryTrustBinding(state.root, config, CLI_VERSION);
271
+ const paths = localStatePaths(process.env.AGENT_OPS_HOME ?? homedir());
272
+ const trusted = (await new FileTrustStore(paths.trustStore, paths.anchorDirectory).status(baseTrustBinding)).status === "TRUSTED";
273
+ return { config, baseTrustBinding, trusted, runtimeHash: await runRuntimeHash() };
274
+ } });
257
275
  }
258
276
  export async function repositoryTrustBinding(root, config, cliVersion) {
259
277
  return await calculateTrustBinding({
@@ -5,7 +5,7 @@ import { rm } from "node:fs/promises";
5
5
  import { createHash } from "node:crypto";
6
6
  import { spawn } from "node:child_process";
7
7
  import { collectOfficeBuilding } from "../../../runtime/src/office/collect.js";
8
- import { claimOffice, createOfficeServer, ensureOffice, legacyOfficeHome, officeRecordPath, readLiveOffice, officeUrl, releaseOffice, userOfficeHome } from "../../../runtime/src/office/server.js";
8
+ import { claimOffice, createOfficeServer, ensureOffice, legacyOfficeHome, officeRecordPath, readLiveOffice, readOfficeRecord, officeUrl, userOfficeHome } from "../../../runtime/src/office/server.js";
9
9
  import { registerOfficeRepo } from "../../../runtime/src/office/repos.js";
10
10
  import { recordOfficeSession, recordOfficeSessionPhase } from "../../../runtime/src/office/sessions.js";
11
11
  import { resolveCheckouts } from "../../../runtime/src/parallel/service.js";
@@ -254,7 +254,6 @@ export async function serveOffice(cwd, onUrl = () => { }, openBrowser) {
254
254
  }, 15_000);
255
255
  await idle;
256
256
  clearInterval(timer);
257
- await releaseOffice(home, record);
258
257
  await office.close();
259
258
  }
260
259
  /** Reuse the user's live server or start one under launchd; null where launchd is unavailable. */
@@ -267,10 +266,13 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
267
266
  const live = await readLiveOffice(home);
268
267
  if (live !== null)
269
268
  return officeUrl(live);
269
+ let previousUrl = null;
270
270
  let started = false;
271
271
  const args = [entry, mainRoot];
272
272
  if (!launchd.supported) {
273
273
  const url = await ensureOffice(home, async () => {
274
+ const previous = await readOfficeRecord(home);
275
+ previousUrl = previous === null ? null : officeUrl(previous);
274
276
  started = true;
275
277
  await new Promise((resolve, reject) => {
276
278
  const child = spawn(process.execPath, args, { cwd: mainRoot, detached: true, stdio: "ignore", windowsHide: true });
@@ -278,7 +280,7 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
278
280
  child.once("spawn", () => { child.unref(); resolve(); });
279
281
  });
280
282
  });
281
- if (started && openBrowser !== undefined)
283
+ if (started && url !== previousUrl && openBrowser !== undefined)
282
284
  void openBrowser(url).catch(() => undefined);
283
285
  return url;
284
286
  }
@@ -289,6 +291,8 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
289
291
  cwd: mainRoot, pathEnvironment: process.env.PATH
290
292
  });
291
293
  const url = await ensureOffice(home, async () => {
294
+ const previous = await readOfficeRecord(home);
295
+ previousUrl = previous === null ? null : officeUrl(previous);
292
296
  started = true;
293
297
  // The descriptor write anchors on this directory, so it must exist first.
294
298
  await ensurePrivateDirectory(privateDirectory, home.anchor);
@@ -297,7 +301,7 @@ export async function ensureBackgroundOffice(cwd, launchd = new LaunchdControlle
297
301
  await launchd.bootout(descriptor).catch(() => { });
298
302
  await launchd.bootstrap(descriptor);
299
303
  });
300
- if (started && openBrowser !== undefined)
304
+ if (started && url !== previousUrl && openBrowser !== undefined)
301
305
  void openBrowser(url).catch(() => undefined);
302
306
  return url;
303
307
  }
@@ -4,13 +4,13 @@ import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
4
4
  import { nativeProcessIdentity, NativeRunTransport } from "../../../runtime/src/run/transport.js";
5
5
  import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
6
6
  import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
7
- import { activeWallTimeMs } from "../../../runtime/src/run/scheduler.js";
7
+ import { remainingRunTimeMs } from "../../../runtime/src/run/scheduler.js";
8
8
  import { runPolicyContext } from "./context.js";
9
9
  /** Native-issued proof commands use the same dormant group as final evidence. */
10
10
  export async function runOwnedLocalProof(argv) {
11
11
  if (!["verify", "review", "batch"].includes(argv[0] ?? "") || argv.includes("--help") || process.env.AGENT_OPS_RUN_PROOF_PID !== undefined)
12
12
  return null;
13
- const context = await runPolicyContext(process.cwd());
13
+ const context = await runPolicyContext(process.cwd(), true);
14
14
  if (context === null)
15
15
  return null;
16
16
  const { repository, state, worker } = context;
@@ -22,7 +22,7 @@ export async function runOwnedLocalProof(argv) {
22
22
  if (existing.processAlive)
23
23
  throw new AgentOpsError("RUN_WORKER_PROOF_ACTIVE", "A registered proof command is already running for this worker.");
24
24
  }
25
- const remaining = state.budget.limitMs - activeWallTimeMs(state.budget.activeIntervals, Date.now());
25
+ const remaining = remainingRunTimeMs(state);
26
26
  if (remaining <= 0)
27
27
  throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active run time remains for local proof.");
28
28
  const cli = fileURLToPath(new URL("./bin.js", import.meta.url));
@@ -34,7 +34,7 @@ export async function runOwnedLocalProof(argv) {
34
34
  process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
35
35
  }
36
36
  catch { } };
37
- const deadline = setTimeout(kill, remaining);
37
+ const deadline = Number.isFinite(remaining) ? setTimeout(kill, remaining) : undefined;
38
38
  try {
39
39
  const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
40
40
  if (identity === null)
@@ -9,7 +9,6 @@ import { FileTrustStore } from "../../../runtime/src/security/trust.js";
9
9
  import { TaskService } from "../../../runtime/src/task/service.js";
10
10
  import { FileTaskStore } from "../../../runtime/src/task/store.js";
11
11
  import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
12
- import { registeredRunProofRunner } from "../../../runtime/src/verify/spawn.js";
13
12
  import { loadEffectiveConfig, repositoryTrust, runPolicyContext, repositoryTrustBinding } from "./context.js";
14
13
  import { CLI_VERSION } from "./version.js";
15
14
  // Shared by the CLI and the hook entry point: Claude Code runs hooks through
@@ -84,7 +83,6 @@ export function worktreeDependencies() {
84
83
  ...(noChangePaths === undefined ? {} : { noChangePaths }),
85
84
  loadConfig: async () => (await loadEffectiveConfig(root, "project")).config
86
85
  } }),
87
- processRunner: registeredRunProofRunner(),
88
86
  runSetup: async (cwd, step) => await new Promise((resolve) => {
89
87
  execFile(step.command, [...step.args], {
90
88
  cwd,
@@ -18,9 +18,9 @@ import { NativeRunTransport } from "../../../runtime/src/run/transport.js";
18
18
  import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
19
19
  import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
20
20
  import { RunSupervisor } from "../../../runtime/src/run/supervisor.js";
21
- import { activeWallTimeMs } from "../../../runtime/src/run/scheduler.js";
21
+ import { activeWallTimeMs, remainingRunTimeMs } from "../../../runtime/src/run/scheduler.js";
22
22
  import { bindRunPolicy, readRunPolicy, runRuntimeHash, runPolicyAssessment } from "../../../runtime/src/run/policy.js";
23
- import { repositoryTrustBinding } from "./context.js";
23
+ import { repositoryTrustBinding, ensureRunPolicy } from "./context.js";
24
24
  import { CLI_VERSION } from "./version.js";
25
25
  import { calculateConfigHash, canonicalJson } from "../../../runtime/src/config/hash.js";
26
26
  import { discoverPendingPolicyTransition, renewBoundPolicyTransition, currentPolicyArtifact } from "../../../runtime/src/run/policy-transition.js";
@@ -30,6 +30,7 @@ import { RunControlService } from "../../../runtime/src/run/controls.js";
30
30
  import { recordRunPhase } from "../../../runtime/src/run/phase.js";
31
31
  /** Worktree construction happens before a native lease exists; it must never write global trust. */
32
32
  export async function runWorktreeDependencies(deps, state) {
33
+ state = await ensureRunPolicy(new FileRunRepository(join(state.commonDir, "agent-ops/runs"), state.commonDir), state);
33
34
  const policy = await readRunPolicy(state, await runRuntimeHash());
34
35
  if (policy === null || (await trustStore().status(policy.baseTrustBinding)).status !== "TRUSTED")
35
36
  throw new AgentOpsError("RUN_REPO_UNTRUSTED", "Run worktree setup requires the original trusted repository and current scoped policy.");
@@ -40,7 +41,9 @@ export async function runWorktreeDependencies(deps, state) {
40
41
  if (index < 0)
41
42
  throw new AgentOpsError("RUN_SETUP_UNAUTHORIZED", "Setup must select an exact fixed policy capability.");
42
43
  const repository = new FileRunRepository(join(state.commonDir, "agent-ops/runs"), state.commonDir);
43
- const current = (await repository.read(state.runId));
44
+ const current = await ensureRunPolicy(repository, (await repository.read(state.runId)));
45
+ if (current.policyBinding?.configHash !== state.policyBinding.configHash)
46
+ throw new AgentOpsError("RUN_SETUP_STALE", "Policy capability changed after setup was selected.");
44
47
  const coordinator = current.workers.find(w => w.workerId === current.coordinatorId);
45
48
  const command = canonicalJson(step), resource = canonicalJson({ checkout: root, capabilityId: "setup:" + index });
46
49
  const approved = await runPolicyAssessment(current, policy, "setup:" + index);
@@ -49,11 +52,11 @@ export async function runWorktreeDependencies(deps, state) {
49
52
  command, resource, commandDigest: sha256(command), resourceDigest: sha256(resource), reason: approved?.reason ?? "The original user trust authorizes this fixed repository setup capability in an isolated checkout.",
50
53
  result: "auto-approved", source: approved === null ? "repo-trust" : "coordinator-policy-review", policyArtifactDigest: current.policyBinding.artifactDigest,
51
54
  policyConfigHash: current.policyBinding.configHash, runtimeHash: current.policyBinding.runtimeHash, expiresAt: current.policyBinding.expiresAt });
52
- const remaining = current.budget.limitMs - activeWallTimeMs(current.budget.activeIntervals, Date.now());
55
+ const remaining = remainingRunTimeMs(current);
53
56
  if (remaining <= 0)
54
57
  throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active time remains for setup.");
55
58
  const child = spawn(process.execPath, [fileURLToPath(new URL("./run-step-entry.js", import.meta.url)),
56
- fileURLToPath(new URL("./run-setup-entry.js", import.meta.url)), state.commonDir, state.runId, root, String(index), state.policyBinding.artifactDigest], { cwd: root, env: process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
59
+ fileURLToPath(new URL("./run-setup-entry.js", import.meta.url)), state.commonDir, state.runId, root, String(index), current.policyBinding.artifactDigest], { cwd: root, env: process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
57
60
  let output = "";
58
61
  const completion = new Promise((resolve, reject) => { child.once("error", reject); child.once("close", resolve); });
59
62
  const kill = () => { if (child.pid !== undefined)
@@ -70,7 +73,7 @@ export async function runWorktreeDependencies(deps, state) {
70
73
  if (identity === null)
71
74
  throw new AgentOpsError("RUN_PROCESS_IDENTITY_REQUIRED", "Cannot register setup process identity.");
72
75
  await repository.mutate(state.runId, saved => {
73
- if (saved.status !== "active" || saved.disableRestart || saved.proofProcess != null || saved.policyBinding?.artifactDigest !== state.policyBinding.artifactDigest)
76
+ if (saved.status !== "active" || saved.disableRestart || saved.proofProcess != null || saved.policyBinding?.artifactDigest !== current.policyBinding.artifactDigest)
74
77
  throw new AgentOpsError("RUN_SETUP_STALE", "Stop or another proof process raced setup registration.");
75
78
  return { ...saved, proofProcess: { processId: child.pid, processIdentity: identity } };
76
79
  });
@@ -206,7 +209,7 @@ export async function productionRunContext(cwd, options = {}) {
206
209
  throw cause;
207
210
  }
208
211
  };
209
- const resume = async (runId) => {
212
+ const resume = async (runId, retryApprovalIds = [], upgradeBudget = true) => {
210
213
  const before = await service.status(runId);
211
214
  const runtimeHash = await runRuntimeHash();
212
215
  const policy = await readRunPolicy(before, before.policyBinding?.runtimeHash ?? runtimeHash, Date.now(), true);
@@ -233,7 +236,7 @@ export async function productionRunContext(cwd, options = {}) {
233
236
  if (exists === null || (await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
234
237
  throw new AgentOpsError("RUN_RECOVERY_DIRTY", "Commit or preserve every writer checkout before explicit resume.");
235
238
  }
236
- const result = await service.resume(runId);
239
+ const result = await service.resume(runId, retryApprovalIds, upgradeBudget);
237
240
  try {
238
241
  if (pending !== null)
239
242
  await renewPendingRunPolicy(repository, runId, pending);
@@ -308,7 +311,7 @@ export async function productionRunContext(cwd, options = {}) {
308
311
  await writePrivateFile(join(state.commonDir, "agent-ops", "runs", state.runId, "answers", sha256(questionId) + ".json"), JSON.stringify({ questionId, answer: answer.trim() }), state.commonDir);
309
312
  const result = await service.respond(runId, questionId, answer);
310
313
  try {
311
- const resumed = await resume(runId);
314
+ const resumed = await resume(runId, [], false);
312
315
  return { ...result, state: resumed.state };
313
316
  }
314
317
  catch (cause) {