@kylecheng3146/agent-ops 0.5.3 → 0.7.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.
Files changed (124) hide show
  1. package/README.md +103 -1
  2. package/dist/packages/cli/src/args.js +183 -3
  3. package/dist/packages/cli/src/bin.js +76 -19
  4. package/dist/packages/cli/src/cli.js +53 -8
  5. package/dist/packages/cli/src/codex-loop-process.js +19 -2
  6. package/dist/packages/cli/src/commands/advance.js +42 -5
  7. package/dist/packages/cli/src/commands/init.js +2 -0
  8. package/dist/packages/cli/src/commands/office.js +20 -0
  9. package/dist/packages/cli/src/commands/review-show.js +1 -1
  10. package/dist/packages/cli/src/commands/review.js +100 -9
  11. package/dist/packages/cli/src/commands/run.js +115 -0
  12. package/dist/packages/cli/src/commands/task.js +61 -17
  13. package/dist/packages/cli/src/commands/update.js +8 -0
  14. package/dist/packages/cli/src/commands/verify.js +1 -0
  15. package/dist/packages/cli/src/context.js +64 -1
  16. package/dist/packages/cli/src/hook-entry.js +2 -1
  17. package/dist/packages/cli/src/hook-process.js +54 -3
  18. package/dist/packages/cli/src/loop-entry.js +2 -1
  19. package/dist/packages/cli/src/office-entry.js +230 -0
  20. package/dist/packages/cli/src/owned-run-step.js +65 -0
  21. package/dist/packages/cli/src/parallel-deps.js +18 -5
  22. package/dist/packages/cli/src/run-deps.js +321 -0
  23. package/dist/packages/cli/src/run-entry.js +952 -0
  24. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  25. package/dist/packages/cli/src/run-step-entry.js +58 -0
  26. package/dist/packages/cli/src/wizard.js +14 -2
  27. package/dist/runtime/src/adapters/agy/config.js +4 -3
  28. package/dist/runtime/src/adapters/agy/events.js +9 -0
  29. package/dist/runtime/src/adapters/claude/config.js +3 -3
  30. package/dist/runtime/src/adapters/claude/events.js +8 -0
  31. package/dist/runtime/src/adapters/codex/config.js +3 -3
  32. package/dist/runtime/src/adapters/codex/events.js +8 -0
  33. package/dist/runtime/src/adapters/opencode/config.js +5 -4
  34. package/dist/runtime/src/adapters/opencode/events.js +8 -0
  35. package/dist/runtime/src/config/explain.js +11 -0
  36. package/dist/runtime/src/config/hash.js +1 -1
  37. package/dist/runtime/src/config/merge.js +9 -1
  38. package/dist/runtime/src/contracts.js +2 -2
  39. package/dist/runtime/src/hooks/codex-loop.js +4 -0
  40. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  41. package/dist/runtime/src/install/doctor.js +2 -0
  42. package/dist/runtime/src/install/harness.js +44 -1
  43. package/dist/runtime/src/install/plan.js +16 -11
  44. package/dist/runtime/src/install/preauth.js +44 -18
  45. package/dist/runtime/src/install/profiles.js +8 -2
  46. package/dist/runtime/src/install/update.js +1 -0
  47. package/dist/runtime/src/office/collect.js +233 -0
  48. package/dist/runtime/src/office/page.js +873 -0
  49. package/dist/runtime/src/office/scene.js +168 -0
  50. package/dist/runtime/src/office/server.js +139 -0
  51. package/dist/runtime/src/office/sessions.js +115 -0
  52. package/dist/runtime/src/office/snapshot.js +107 -0
  53. package/dist/runtime/src/parallel/finish.js +85 -22
  54. package/dist/runtime/src/parallel/guard.js +12 -2
  55. package/dist/runtime/src/parallel/integrate.js +288 -10
  56. package/dist/runtime/src/parallel/manage.js +4 -1
  57. package/dist/runtime/src/parallel/receipt.js +71 -5
  58. package/dist/runtime/src/parallel/service.js +41 -1
  59. package/dist/runtime/src/review/attestation.js +36 -11
  60. package/dist/runtime/src/review/execute.js +24 -1
  61. package/dist/runtime/src/review/packet.js +35 -1
  62. package/dist/runtime/src/review/runner.js +12 -1
  63. package/dist/runtime/src/review/scope.js +18 -1
  64. package/dist/runtime/src/review/slots.js +35 -10
  65. package/dist/runtime/src/run/authorization.js +60 -0
  66. package/dist/runtime/src/run/budget.js +30 -0
  67. package/dist/runtime/src/run/controls.js +139 -0
  68. package/dist/runtime/src/run/convergence.js +177 -0
  69. package/dist/runtime/src/run/hosts/claude.js +180 -0
  70. package/dist/runtime/src/run/hosts/codex.js +279 -0
  71. package/dist/runtime/src/run/hosts/index.js +4 -0
  72. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  73. package/dist/runtime/src/run/hosts/types.js +1 -0
  74. package/dist/runtime/src/run/hosts/util.js +136 -0
  75. package/dist/runtime/src/run/integration.js +204 -0
  76. package/dist/runtime/src/run/macOS.js +189 -0
  77. package/dist/runtime/src/run/ownership.js +22 -0
  78. package/dist/runtime/src/run/phase.js +37 -0
  79. package/dist/runtime/src/run/planning.js +25 -0
  80. package/dist/runtime/src/run/policy-transition.js +1076 -0
  81. package/dist/runtime/src/run/policy.js +176 -0
  82. package/dist/runtime/src/run/ratchet.js +71 -0
  83. package/dist/runtime/src/run/scheduler.js +260 -0
  84. package/dist/runtime/src/run/service.js +433 -0
  85. package/dist/runtime/src/run/supervisor.js +635 -0
  86. package/dist/runtime/src/run/transport.js +159 -0
  87. package/dist/runtime/src/run/types.js +1 -0
  88. package/dist/runtime/src/run/usage.js +81 -0
  89. package/dist/runtime/src/run/verification.js +64 -0
  90. package/dist/runtime/src/run/worker-gate.js +48 -0
  91. package/dist/runtime/src/schema/acceptance.js +91 -0
  92. package/dist/runtime/src/schema/validate.js +80 -9
  93. package/dist/runtime/src/task/completion.js +14 -2
  94. package/dist/runtime/src/task/contract.js +17 -0
  95. package/dist/runtime/src/task/pin-finding.js +81 -0
  96. package/dist/runtime/src/task/service.js +316 -13
  97. package/dist/runtime/src/task/store.js +57 -7
  98. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  99. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  100. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  101. package/dist/runtime/src/verify/adapters/index.js +5 -0
  102. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  103. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  104. package/dist/runtime/src/verify/adapters/node.js +254 -0
  105. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  106. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  107. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  108. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  109. package/dist/runtime/src/verify/evidence.js +5 -2
  110. package/dist/runtime/src/verify/service.js +24 -8
  111. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  112. package/dist/runtime/src/verify/spawn.js +13 -2
  113. package/docs/en/spec/acceptance-and-evidence.md +48 -0
  114. package/docs/en/spec/harness-adapters.md +113 -1
  115. package/docs/en/spec/loop-engineering.md +49 -0
  116. package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
  117. package/docs/zh-TW/spec/harness-adapters.md +93 -1
  118. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  119. package/package.json +1 -1
  120. package/schemas/config.schema.json +230 -19
  121. package/schemas/evidence.schema.json +129 -6
  122. package/schemas/task.schema.json +265 -6
  123. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  124. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -0,0 +1,952 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { recoverRunIntegrationAfterCleanup, validateRunIntegrationReceipt } from "../../../runtime/src/run/integration.js";
3
+ import { pendingFindingPins } from "../../../runtime/src/run/ratchet.js";
4
+ import { resolveReviewScope } from "../../../runtime/src/review/scope.js";
5
+ import { writeNoChangeDelivery, integrateSessionChildren } from "../../../runtime/src/parallel/integrate.js";
6
+ import { recordNativeRunUsage, recordReviewRunUsage, recordSavedReviewRunUsage } from "../../../runtime/src/run/usage.js";
7
+ import { recordNativeRunAuthorization } from "../../../runtime/src/run/authorization.js";
8
+ import { observeRunFailure } from "../../../runtime/src/run/verification.js";
9
+ import { criteriaProgress, recordRunPhase } from "../../../runtime/src/run/phase.js";
10
+ import { compareFailureObservations } from "../../../runtime/src/run/convergence.js";
11
+ import { RunControlService } from "../../../runtime/src/run/controls.js";
12
+ import { beginPolicyTransition, markPolicyTransitionStage, completePolicyTransition, discoverPendingPolicyTransition } from "../../../runtime/src/run/policy-transition.js";
13
+ import { readReviewReportArtifact } from "../../../runtime/src/review/attestation.js";
14
+ import { execFile, spawn } from "node:child_process";
15
+ import { promisify } from "node:util";
16
+ import { join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { setTimeout as delay } from "node:timers/promises";
19
+ import { AgentOpsError, resolveContainedPath } from "../../../runtime/src/fs/paths.js";
20
+ import { bindRunPolicy, readRunPolicy, runRuntimeHash, validateRunPolicyChange } from "../../../runtime/src/run/policy.js";
21
+ import { sha256 } from "../../../runtime/src/fs/hash.js";
22
+ import { calculateConfigHash, canonicalJson } from "../../../runtime/src/config/hash.js";
23
+ import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
24
+ import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
25
+ import { FileRunRepository } from "../../../runtime/src/run/service.js";
26
+ import { RunSupervisor } from "../../../runtime/src/run/supervisor.js";
27
+ import { NativeRunTransport, nativeProcessIdentity } from "../../../runtime/src/run/transport.js";
28
+ import { workerPlan } from "../../../runtime/src/run/planning.js";
29
+ import { RunScheduler } from "../../../runtime/src/run/scheduler.js";
30
+ import { activeWallTimeMs } from "../../../runtime/src/run/scheduler.js";
31
+ import { readBootIdentity, readGuiLoginIdentity, LaunchdController } from "../../../runtime/src/run/macOS.js";
32
+ import { readPrivateFile, writePrivateFile, withPrivateFileLock } from "../../../runtime/src/security/permissions.js";
33
+ import { TaskService } from "../../../runtime/src/task/service.js";
34
+ import { FileTaskStore } from "../../../runtime/src/task/store.js";
35
+ import { taskContractHash, treeContractHash } from "../../../runtime/src/task/contract.js";
36
+ import { readWorktreeRecord, writeWorktreeRecord, addWorktree, ensureSessionWorktree } from "../../../runtime/src/parallel/service.js";
37
+ import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
38
+ import { runDescriptor, runWorktreeDependencies, renewPendingRunPolicy } from "./run-deps.js";
39
+ import { worktreeDependencies, gitRunner } from "./parallel-deps.js";
40
+ import { readFinishedReview } from "./commands/review-show.js";
41
+ const exec = promisify(execFile);
42
+ const plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
43
+ const active = (worker) => ["assigned", "starting", "running", "idle", "handing-off"].includes(worker.status);
44
+ const cli = fileURLToPath(new URL("./bin.js", import.meta.url));
45
+ /** Recheck after confirmed death and after verification, before publishing the frozen SHA. */
46
+ export async function assertRunDeliverySnapshot(root, taskId, head, contract, tasks) {
47
+ const runner = gitRunner(root);
48
+ const actual = await runner.run(["rev-parse", "HEAD"]);
49
+ if (actual.exitCode !== 0 || actual.stdout.toString().trim() !== head ||
50
+ (await collectChangeSurface(runner)).paths.length > 0 || await tasks.treeContract(taskId) !== contract)
51
+ throw new AgentOpsError("RUN_DELIVERY_CHANGED", "Worker source or contract changed while stopping or verifying; take a new handoff snapshot.");
52
+ }
53
+ /** The launchd process owns leases. Native turn completion never completes a run. */
54
+ export async function superviseNativeRun(root, runId) {
55
+ const deps = worktreeDependencies();
56
+ const common = await deps.git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
57
+ if (common.exitCode !== 0)
58
+ throw new AgentOpsError("RUN_REPOSITORY_INVALID", "Cannot locate the run's Git common directory.");
59
+ const commonDir = common.stdout.trim();
60
+ const directory = join(commonDir, "agent-ops", "runs");
61
+ const repository = new FileRunRepository(directory, commonDir);
62
+ const initial = await repository.read(runId);
63
+ if (initial === null || initial.root !== root || initial.commonDir !== commonDir)
64
+ throw new AgentOpsError("RUN_REPOSITORY_INVALID", "Supervisor root must match the saved canonical run identity.");
65
+ await withPrivateFileLock(join(directory, runId, "supervisor.lock"), commonDir, async () => {
66
+ let state = (await repository.read(runId));
67
+ const launchd = new LaunchdController();
68
+ async function blockStartup(code, paused = false, closedAt = Date.now(), preserveStatus = false) {
69
+ await repository.mutate(runId, current => ({ ...current, ...(preserveStatus ? {} : { status: paused ? "paused" : "blocked", awaitingResume: paused }), disableRestart: true }));
70
+ await repository.appendEvent(runId, { type: "diagnostic", code, workerId: null, taskId: null,
71
+ detail: "Startup refused; current native ownership and worktrees are preserved for reconciliation." });
72
+ try {
73
+ await launchd.disableRestart(runDescriptor(state), code);
74
+ }
75
+ catch {
76
+ await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_SUPERVISOR_DISABLE_FAILED",
77
+ workerId: null, taskId: null, detail: "Launchd disable failed; stopping registered executors under the persisted disabled intent." });
78
+ }
79
+ const savedTransport = new NativeRunTransport(state.host === "codex"
80
+ ? new CodexGoalHost({ nativeVersion: state.nativeInstance }) : new ClaudeGoalHost({ nativeVersion: state.nativeInstance }), repository);
81
+ const savedSupervisor = new RunSupervisor({ repository, host: savedTransport });
82
+ for (const worker of state.workers)
83
+ await savedSupervisor.stopWorker(runId, worker.workerId, worker.generation, "crash");
84
+ if (state.proofProcess != null)
85
+ await savedTransport.stop({ nativeSessionId: "run-proof", nativeJobId: null,
86
+ ...state.proofProcess, instance: state.nativeInstance, generation: 1, reason: code });
87
+ await repository.mutate(runId, current => ({ ...current, proofProcess: null, budget: { ...current.budget,
88
+ accumulatedMs: activeWallTimeMs(current.budget.activeIntervals, closedAt), lastObservedAt: new Date().toISOString(),
89
+ activeIntervals: current.budget.activeIntervals.map(interval => ({ ...interval, endMs: interval.endMs ?? Math.max(interval.startMs, closedAt) })) } }));
90
+ }
91
+ if (state.disableRestart || state.status === "complete") {
92
+ await blockStartup("RUN_DISABLED_RECONCILED", false, Date.now(), true);
93
+ return;
94
+ }
95
+ const boot = await readBootIdentity();
96
+ const login = await readGuiLoginIdentity();
97
+ if (state.bootIdentity !== boot || state.loginDomain !== login) {
98
+ await blockStartup("RUN_BOOT_OR_LOGIN_CHANGED", true, state.bootIdentity !== boot ? Date.parse(state.budget.lastObservedAt) : Date.now());
99
+ return;
100
+ }
101
+ const lastResume = Math.max(0, ...state.controls.filter(c => c.action === "resume" && c.actor === "user").map(c => Date.parse(c.requestedAt)));
102
+ const restarts = state.events.filter(e => e.code === "RUN_SUPERVISOR_START" && Date.parse(e.at) >= lastResume && Date.now() - Date.parse(e.at) < 300000).length;
103
+ if (restarts >= 3) {
104
+ await blockStartup("RUN_RESTART_STORM");
105
+ return;
106
+ }
107
+ await repository.appendEvent(runId, { type: "status", code: "RUN_SUPERVISOR_START", workerId: null, taskId: null, detail: null });
108
+ let version;
109
+ try {
110
+ version = state.integration !== null ? state.nativeInstance ?? "recovery"
111
+ : (await exec(state.host, ["--version"], { timeout: 5000, maxBuffer: 8192 })).stdout.trim();
112
+ if (version.length === 0 || (state.nativeInstance !== null && state.nativeInstance !== version))
113
+ throw new AgentOpsError("RUN_NATIVE_VERSION_CHANGED", "Native version drift prevents automatic writer restart.");
114
+ }
115
+ catch (cause) {
116
+ await blockStartup(cause instanceof AgentOpsError ? cause.code : "RUN_NATIVE_UNAVAILABLE");
117
+ return;
118
+ }
119
+ state = await repository.mutate(runId, current => ({ ...current, nativeInstance: version }));
120
+ const transport = new NativeRunTransport(state.host === "codex" ? new CodexGoalHost({ nativeVersion: version }) : new ClaudeGoalHost({ nativeVersion: version }), repository, async (event) => {
121
+ await recordNativeRunUsage(repository, event);
122
+ try {
123
+ await recordNativeRunAuthorization(repository, event);
124
+ }
125
+ catch (cause) {
126
+ await repository.appendEvent(runId, { type: "diagnostic", code: cause instanceof AgentOpsError ? cause.code : "RUN_AUTHORIZATION_FAILED",
127
+ workerId: event.workerId, taskId: null, detail: "Native authorization could not be accepted; no alternate permission path is allowed." });
128
+ await repository.mutate(runId, current => ({ ...current, status: "blocked", disableRestart: true }));
129
+ }
130
+ if (event.nativeStatus === "usageLimited" || event.nativeStatus === "budgetLimited") {
131
+ await repository.mutate(runId, current => ({ ...current,
132
+ status: event.nativeStatus === "usageLimited" || event.nativeStatus === "budgetLimited" ? "budget-limited" : "blocked",
133
+ disableRestart: true }));
134
+ }
135
+ if (["completed", "error", "closed"].includes(event.type))
136
+ await repository.appendEvent(runId, { type: "diagnostic",
137
+ code: event.type === "completed" ? "RUN_NATIVE_COMPLETION_UNPROVEN" : "RUN_NATIVE_" + event.type.toUpperCase(),
138
+ workerId: event.workerId, taskId: null, detail: "Native lifecycle observation; current agent-ops proof is still required." });
139
+ });
140
+ const supervisor = new RunSupervisor({ repository, host: transport });
141
+ const scheduler = new RunScheduler({ repository });
142
+ async function instructions(worker, diagnostic) {
143
+ const current = (await repository.read(runId));
144
+ const answers = [];
145
+ for (const question of current.questions.filter(q => q.answeredAt !== null)) {
146
+ const source = await readPrivateFile(join(directory, runId, "answers", sha256(question.questionId) + ".json"), commonDir);
147
+ if (source === null)
148
+ throw new AgentOpsError("RUN_ANSWER_MISSING", "A recorded answer artifact is missing.");
149
+ const answer = JSON.parse(source);
150
+ if (!plain(answer) || answer.questionId !== question.questionId || typeof answer.answer !== "string" ||
151
+ sha256(answer.answer) !== question.answerDigest)
152
+ throw new AgentOpsError("RUN_ANSWER_CHANGED", "The saved answer no longer matches its recorded digest.");
153
+ answers.push({ questionId: question.questionId, prompt: question.prompt, answer: answer.answer });
154
+ }
155
+ const content = { runId, workerId: worker.workerId, generation: worker.generation, contractHash: current.currentContractHash,
156
+ originalGoal: current.goal, answers, rootTaskId: current.rootTaskId, taskId: worker.taskId, cli, ownerSessionId: worker.ownerSessionId,
157
+ policyConfigHash: current.policyBinding?.configHash,
158
+ instructions: ["Keep the original goal fixed. Read this repository's AGENTS.md and use its existing verifier authority.",
159
+ "Plan 2–5 substantive acceptance criteria. Submit a plan request before changing product files; the supervisor creates the task with the fixed goal.",
160
+ "For every mechanically testable requirement use a configured acceptance runner; explicit review-only fallback requires a recorded reason and fresh goal review.",
161
+ "Implement, commit and verify. Repair failed checks and pin mechanically testable review findings into regression criteria. Never disable a mandatory policy verifier.",
162
+ "Only registered run workers may write. Use isolated worktrees and supervisor-issued leases; do not launch an unregistered writing subagent.",
163
+ "Commit product changes before revising the contract. A contract revision stops existing generations; dirty recovery preserves the checkout and blocks takeover.",
164
+ "Write one request atomically to .agent-ops/tasks/run-request.json; wait for matching run-response.json before another request.",
165
+ "Requests require requestId (unique alphanumeric or hyphen, max 64), workerId, generation and current contractHash.",
166
+ "Plan fields: action=plan,title,intent,criteria. Worker fields: action=worker,title,intent,criteria,dependencies (known worker task IDs; omit all acceptance baselines until dependencies are delivered).",
167
+ "Only the coordinator may change execution policy: submit action=policy, configFile (relative proposed JSON), expectedPolicyHash, approvals (each changed capabilityId command:<id>/runner:<id>/setup:<index>, classification=non-dangerous, reason assessing the entire fixed definition including setup/build). Commit source changes and wait until every writer is clean before requesting policy synchronization. Never use init/update/trust grant to create permanent execution authorization. Explicit native or organization denial must not be bypassed.",
168
+ "Worker task baselines are frozen after dependency commits enter its checkout. Handoff fields: action=handoff,sourceFiles (explicit existing source paths when unchanged). Question fields: action=question,prompt. Coordinator finalize fields: action=finalize.",
169
+ "The supervisor, after stopping writers, runs final verification, two fresh reviews and finish. A native completed goal is never final proof."], diagnostic };
170
+ const path = join(worker.worktree, ".agent-ops", "tasks", "run-instructions.json");
171
+ await writePrivateFile(path, JSON.stringify(content, null, 2), worker.worktree);
172
+ return "Read .agent-ops/tasks/run-instructions.json and carry its immutable original goal through the evidence loop. Submit a plan if rootTaskId is null; otherwise implement and repair the assigned task. A child requests handoff after local green; the coordinator waits for all child deliveries, then requests finalization. Never substitute native goal completion for agent-ops evidence.";
173
+ }
174
+ async function start(worker, diagnostic) {
175
+ if (await discoverPendingPolicyTransition(repository, runId) !== null)
176
+ throw new AgentOpsError("RUN_POLICY_TRANSITION_PENDING", "Finish policy synchronization before acquiring a new writer generation.");
177
+ if (worker.worktree === null)
178
+ throw new AgentOpsError("RUN_WORKTREE_MISSING", "Worker has no isolated checkout.");
179
+ const config = await deps.loadConfig(worker.worktree);
180
+ if (await deps.trust.status(worker.worktree, config) !== "TRUSTED")
181
+ throw new AgentOpsError("RUN_REPO_UNTRUSTED", "Native activation requires current run policy, runtime and repository trust.");
182
+ if (worker.status !== "assigned") {
183
+ if ((await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
184
+ throw new AgentOpsError("RUN_RECOVERY_DIRTY", "A crashed writer left an uncommitted checkout; preserve it for explicit recovery.");
185
+ if (worker.nativeSessionId !== null) {
186
+ const observation = await transport.inspect({ nativeSessionId: worker.nativeSessionId, nativeJobId: worker.nativeJobId,
187
+ processId: worker.processId, processIdentity: worker.processIdentity, instance: state.nativeInstance });
188
+ if (observation.processAlive)
189
+ await supervisor.stopWorker(runId, worker.workerId, worker.generation, "crash");
190
+ }
191
+ worker = await supervisor.reassignWorker(runId, worker.workerId, worker.generation);
192
+ }
193
+ const record = await readWorktreeRecord(worker.worktree);
194
+ if (record === null || record.runId !== runId || record.workerId !== worker.workerId)
195
+ throw new AgentOpsError("RUN_WORKTREE_OWNERSHIP", "The worker checkout is not registered to this run.");
196
+ await writeWorktreeRecord({ ...record, workerGeneration: worker.generation });
197
+ const goal = await instructions(worker, diagnostic);
198
+ const running = await supervisor.startWorker(runId, worker.workerId, worker.generation, goal);
199
+ await recordRunPhase(repository, runId, worker.workerId, (await repository.read(runId)).rootTaskId === null ? "planning" : "implementing");
200
+ try {
201
+ await transport.activateRegistered(runId, running, goal);
202
+ }
203
+ catch (cause) {
204
+ await supervisor.stopWorker(runId, running.workerId, running.generation, "crash");
205
+ throw cause;
206
+ }
207
+ }
208
+ async function schedule() {
209
+ const current = (await repository.read(runId));
210
+ for (const node of await scheduler.next(runId)) {
211
+ if (node.planDigest === undefined || node.taskId === current.rootTaskId)
212
+ continue;
213
+ const existing = current.workers.find(worker => worker.taskId === node.taskId);
214
+ if (existing !== undefined && existing.worktree !== null &&
215
+ (await deps.tasks(existing.worktree).list()).some(r => r.task.id === node.taskId))
216
+ continue;
217
+ const planPath = join(directory, runId, "plans", node.taskId + ".json");
218
+ const source = await readPrivateFile(planPath, commonDir);
219
+ if (source === null || sha256(source) !== node.planDigest)
220
+ throw new AgentOpsError("RUN_PLAN_CHANGED", "Deferred worker plan is missing or changed.");
221
+ const plan = workerPlan(JSON.parse(source), node.taskId, current);
222
+ const ownerSessionId = existing?.ownerSessionId ?? (await import("node:crypto")).randomUUID();
223
+ const registration = existing ?? await supervisor.registerWorker(runId, { taskId: node.taskId, ownerSessionId });
224
+ try {
225
+ const coordinator = current.workers[0];
226
+ const from = (await deps.git(coordinator.worktree, ["rev-parse", "HEAD"])).stdout.trim();
227
+ const name = runId + "-" + sha256(node.taskId).slice(0, 8);
228
+ const plannedRoot = join(root, ".worktrees", name);
229
+ await repository.mutate(runId, saved => {
230
+ if (saved.status !== "active" || saved.disableRestart)
231
+ throw new AgentOpsError("RUN_START_DISABLED", "Stop fenced child provisioning before checkout creation.");
232
+ return { ...saved, workers: saved.workers.map(w => w.workerId === registration.workerId ? { ...w, worktree: plannedRoot } : w) };
233
+ });
234
+ const preserved = await readWorktreeRecord(plannedRoot);
235
+ if (preserved !== null && (preserved.runId !== runId || preserved.workerId !== registration.workerId))
236
+ throw new AgentOpsError("RUN_WORKTREE_OWNERSHIP", "Preserved child checkout belongs to another worker.");
237
+ const workerRoot = preserved?.path ?? (await addWorktree(await runWorktreeDependencies(deps, (await repository.read(runId))), { cwd: root, name,
238
+ sessionId: current.ownerSessionId, agentId: registration.workerId, from, targetBranch: current.targetBranch,
239
+ runOwnership: { runId, coordinatorId: current.coordinatorId, workerId: registration.workerId, ownerSessionId, generation: registration.generation } })).record.path;
240
+ await ensureSetup(workerRoot);
241
+ for (const dependency of node.dependencies) {
242
+ const delivered = current.tasks.find(task => task.taskId === dependency);
243
+ if (delivered?.sourceCommit == null || delivered.status !== "delivered")
244
+ throw new AgentOpsError("RUN_DEPENDENCY_UNDELIVERED", "Dependency version is not a frozen delivery.");
245
+ const merge = await deps.git(workerRoot, ["merge", "--no-edit", delivered.sourceCommit]);
246
+ if (merge.exitCode !== 0) {
247
+ await deps.git(workerRoot, ["merge", "--abort"]);
248
+ throw new AgentOpsError("RUN_DEPENDENCY_CONFLICT", "Dependency commits conflict; preserve the child checkout.");
249
+ }
250
+ }
251
+ const config = await deps.loadConfig(workerRoot);
252
+ const service = new TaskService(new FileTaskStore(join(workerRoot, ".agent-ops", "tasks", "state.json"), workerRoot), {
253
+ generateId: () => node.taskId, completion: { root: workerRoot, gitRunner: gitRunner(workerRoot), loadConfig: async () => config }
254
+ });
255
+ await service.create({ title: plan.title, intent: plan.intent, criteria: plan.criteria, goal: current.goal,
256
+ sessionId: ownerSessionId, policyConfigHash: calculateConfigHash(config) });
257
+ await start((await repository.read(runId)).workers.find(w => w.workerId === registration.workerId));
258
+ }
259
+ catch (cause) {
260
+ const latest = (await repository.read(runId));
261
+ if (latest.status !== "active" || latest.disableRestart) {
262
+ const saved = latest.workers.find(w => w.workerId === registration.workerId);
263
+ await supervisor.stopWorker(runId, saved.workerId, saved.generation, "stop");
264
+ return;
265
+ }
266
+ await repository.appendEvent(runId, { type: "diagnostic", code: cause instanceof AgentOpsError ? cause.code : "RUN_WORKER_START_FAILED",
267
+ workerId: registration.workerId, taskId: node.taskId, detail: "Child provisioning failed; the registered checkout and setup artifacts are preserved." });
268
+ await scheduler.blockTaskAndDependents(runId, node.taskId, cause instanceof AgentOpsError ? cause.code : "RUN_WORKER_START_FAILED", true);
269
+ await repository.mutate(runId, saved => saved.status !== "active" || saved.disableRestart ? saved :
270
+ { ...saved, workers: saved.workers.map(w => w.workerId === registration.workerId ? { ...w, status: "blocked" } : w) });
271
+ }
272
+ }
273
+ }
274
+ async function advance(saved) {
275
+ return await step(root, ["task", "advance", "--task", saved.rootTaskId, "--session", saved.ownerSessionId, "--yes"], saved);
276
+ }
277
+ async function recoverIntegration(saved) {
278
+ const coordinator = saved.workers.find(worker => worker.workerId === saved.coordinatorId);
279
+ if (coordinator?.worktree == null)
280
+ throw new AgentOpsError("RUN_WORKTREE_MISSING", "Recovery requires the saved coordinator identity.");
281
+ let absent = false;
282
+ try {
283
+ await stat(coordinator.worktree);
284
+ }
285
+ catch (cause) {
286
+ if (cause.code === "ENOENT")
287
+ absent = true;
288
+ else
289
+ throw cause;
290
+ }
291
+ if (absent)
292
+ return (await recoverRunIntegrationAfterCleanup(commonDir, runId, deps.git)).receiptPath;
293
+ const result = await advance(saved);
294
+ if (result.status !== "ok" || !plain(result.data) || typeof result.data.receipt !== "string")
295
+ throw new AgentOpsError("RUN_INTEGRATION_RECOVERY_REQUIRED", "Recover the sealed transaction before allowing any writer to resume.");
296
+ return result.data.receipt;
297
+ }
298
+ async function step(cwd, args, saved) {
299
+ const remaining = saved.budget.limitMs - activeWallTimeMs(saved.budget.activeIntervals, Date.now());
300
+ if (remaining <= 0)
301
+ throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active time remains for final proof.");
302
+ return await new Promise((resolve, reject) => {
303
+ const env = { ...process.env, AGENT_OPS_HOST: saved.host,
304
+ AGENT_OPS_SESSION_ID: saved.workers.find(worker => worker.worktree === cwd)?.ownerSessionId ?? saved.ownerSessionId,
305
+ AGENT_OPS_RUN_ID: saved.runId,
306
+ AGENT_OPS_WORKER_ID: saved.workers.find(worker => worker.worktree === cwd)?.workerId ?? saved.coordinatorId,
307
+ AGENT_OPS_WORKER_GENERATION: String(saved.workers.find(worker => worker.worktree === cwd)?.generation ?? saved.workers.find(worker => worker.workerId === saved.coordinatorId)?.generation) };
308
+ delete env.CODEX_THREAD_ID;
309
+ delete env.AGENT_OPS_AGENT_ID;
310
+ delete env.CODEX_SANDBOX_NETWORK_DISABLED;
311
+ const child = spawn(process.execPath, [fileURLToPath(new URL("./run-step-entry.js", import.meta.url)), cli, ...args, "--json"], { cwd, env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
312
+ let output = "";
313
+ let closed = false;
314
+ let escalation;
315
+ const terminate = () => {
316
+ if (child.pid === undefined || escalation !== undefined)
317
+ return;
318
+ try {
319
+ process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGTERM");
320
+ }
321
+ catch { }
322
+ escalation = setTimeout(() => {
323
+ if (!closed)
324
+ try {
325
+ process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
326
+ }
327
+ catch { }
328
+ }, 3000);
329
+ };
330
+ const timeout = setTimeout(terminate, remaining);
331
+ const registration = (async () => {
332
+ const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
333
+ if (identity === null || closed) {
334
+ terminate();
335
+ return;
336
+ }
337
+ await repository.mutate(runId, current => {
338
+ if (current.status !== "active" || current.disableRestart) {
339
+ terminate();
340
+ throw new AgentOpsError("RUN_PROOF_STOPPED", "Stop raced final proof registration.");
341
+ }
342
+ return { ...current, proofProcess: { processId: child.pid, processIdentity: identity } };
343
+ });
344
+ child.stdin.end("start\n");
345
+ })();
346
+ child.stdin.on("error", terminate);
347
+ void registration.catch(() => terminate());
348
+ let checking = false;
349
+ const control = setInterval(() => {
350
+ if (checking)
351
+ return;
352
+ checking = true;
353
+ void repository.read(runId).then(current => {
354
+ if (current === null || current.disableRestart || current.status !== "active")
355
+ terminate();
356
+ }).catch(terminate).finally(() => { checking = false; });
357
+ }, 250);
358
+ child.stdout.on("data", chunk => { output += chunk.toString(); if (Buffer.byteLength(output) > 4 * 1024 * 1024)
359
+ terminate(); });
360
+ child.stderr.resume();
361
+ child.once("error", cause => { closed = true; clearTimeout(timeout); clearInterval(control); reject(cause); });
362
+ child.once("close", async () => {
363
+ closed = true;
364
+ clearTimeout(timeout);
365
+ clearTimeout(escalation);
366
+ clearInterval(control);
367
+ try {
368
+ await registration;
369
+ await repository.mutate(runId, current => ({ ...current,
370
+ ...(current.proofProcess?.processId === child.pid ? { proofProcess: null } : {}) }));
371
+ const result = JSON.parse(output);
372
+ if (!plain(result) || !["ok", "error"].includes(String(result.status)) || typeof result.code !== "string")
373
+ throw new Error();
374
+ const coordinatorRoot = saved.workers[0]?.worktree;
375
+ if (result.status === "error" && coordinatorRoot != null)
376
+ await recordSavedReviewRunUsage(repository, runId, coordinatorRoot);
377
+ if (plain(result.data) && plain(result.data.result) && typeof result.data.result.sourceFingerprint === "string" &&
378
+ typeof result.data.result.taskId === "string") {
379
+ const artifact = await readReviewReportArtifact(cwd, result.data.result.sourceFingerprint, result.data.result.taskId);
380
+ if (artifact !== null)
381
+ await recordReviewRunUsage(repository, runId, artifact);
382
+ }
383
+ resolve(result);
384
+ }
385
+ catch {
386
+ reject(new AgentOpsError("RUN_FINAL_PROOF_INTERRUPTED", "Final gate did not return a complete result; inspect target and receipt before retrying."));
387
+ }
388
+ });
389
+ });
390
+ }
391
+ async function stopAll(reason, closeBudget = true) {
392
+ const current = (await repository.read(runId));
393
+ for (const worker of current.workers.filter(active))
394
+ await supervisor.stopWorker(runId, worker.workerId, worker.generation, reason);
395
+ if (!closeBudget)
396
+ return;
397
+ await repository.mutate(runId, saved => ({ ...saved, budget: { ...saved.budget,
398
+ accumulatedMs: activeWallTimeMs(saved.budget.activeIntervals, Date.now()), lastObservedAt: new Date().toISOString(),
399
+ activeIntervals: saved.budget.activeIntervals.map(interval => ({ ...interval, endMs: interval.endMs ?? Date.now() })) } }));
400
+ }
401
+ async function ensureSetup(workerRoot) {
402
+ const current = (await repository.read(runId));
403
+ const scoped = await runWorktreeDependencies(deps, current), config = await scoped.loadConfig(workerRoot);
404
+ const setupHash = sha256(JSON.stringify(config.worktree?.setup ?? []));
405
+ const complete = await readPrivateFile(join(workerRoot, ".agent-ops/tasks/run-setup-complete.json"), workerRoot);
406
+ if (complete !== null && JSON.parse(complete).setupHash === setupHash)
407
+ return;
408
+ const attemptPath = join(workerRoot, ".agent-ops/tasks/run-setup-attempt.json");
409
+ const attempt = await readPrivateFile(attemptPath, workerRoot);
410
+ if (attempt !== null && (complete === null || JSON.parse(attempt).setupHash === setupHash) &&
411
+ lastResume <= Date.parse(JSON.parse(attempt).startedAt))
412
+ throw new AgentOpsError("RUN_SETUP_RECOVERY_REQUIRED", "Interrupted setup requires explicit resume before replaying commands.");
413
+ if ((await collectChangeSurface(gitRunner(workerRoot))).paths.length > 0)
414
+ throw new AgentOpsError("RUN_RECOVERY_DIRTY", "Preserve setup source changes before recovery.");
415
+ await writePrivateFile(attemptPath, JSON.stringify({ startedAt: new Date().toISOString(), setupHash }), workerRoot);
416
+ for (const step of config.worktree?.setup ?? []) {
417
+ const result = await scoped.runSetup(workerRoot, { ...step, timeoutMs: step.timeoutMs ?? 600000 });
418
+ if (result.exitCode !== 0)
419
+ throw new AgentOpsError("RUN_SETUP_RECOVERY_REQUIRED", "Registered setup failed; its artifact and checkout are preserved.");
420
+ }
421
+ await writePrivateFile(join(workerRoot, ".agent-ops/tasks/run-setup-complete.json"), JSON.stringify({ setupHash }), workerRoot);
422
+ }
423
+ async function synchronizePolicy() {
424
+ const current = (await repository.read(runId));
425
+ const policy = await readRunPolicy(current, await runRuntimeHash());
426
+ if (policy === null)
427
+ return;
428
+ for (const worker of current.workers.filter(w => w.worktree !== null && w.status !== "delivered")) {
429
+ if (await readPrivateFile(join(worker.worktree, ".agent-ops/tasks/worktree.json"), worker.worktree) === null)
430
+ continue;
431
+ const configPath = join(worker.worktree, ".agent-ops/config.json");
432
+ const previous = await readPrivateFile(configPath, worker.worktree);
433
+ if (previous === null || calculateConfigHash(JSON.parse(previous)) !== current.policyBinding.configHash) {
434
+ if ((await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
435
+ throw new AgentOpsError("RUN_POLICY_DIRTY", "Policy synchronization preserves uncommitted worker edits for explicit recovery.");
436
+ await writePrivateFile(configPath, JSON.stringify(policy.config, null, 2) + "\n", worker.worktree);
437
+ const tracked = await deps.git(worker.worktree, ["ls-files", "--error-unmatch", ".agent-ops/config.json"]);
438
+ if (tracked.exitCode === 0) {
439
+ if ((await deps.git(worker.worktree, ["add", "--", ".agent-ops/config.json"])).exitCode !== 0 ||
440
+ (await deps.git(worker.worktree, ["commit", "-m", "Synchronize run verifier policy", "--", ".agent-ops/config.json"])).exitCode !== 0)
441
+ throw new AgentOpsError("RUN_POLICY_SYNC_FAILED", "Could not commit the scoped policy synchronization.");
442
+ }
443
+ }
444
+ await ensureSetup(worker.worktree);
445
+ const tasks = new TaskService(new FileTaskStore(join(worker.worktree, ".agent-ops/tasks/state.json"), worker.worktree), {
446
+ completion: { root: worker.worktree, gitRunner: gitRunner(worker.worktree), loadConfig: async () => policy.config }
447
+ });
448
+ for (const record of (await tasks.list()).filter(r => r.status === "active" && r.supersededBy === undefined))
449
+ if (record.policyConfigHash !== current.policyBinding.configHash)
450
+ await tasks.revise(record.task.id, {
451
+ expectedContractHash: taskContractHash(record.task), criteria: record.task.criteria,
452
+ reason: "Run execution policy changed; preserve the original goal and baselines, and require fresh proof.",
453
+ diagnostics: ["run-policy:" + current.policyBinding.artifactDigest]
454
+ });
455
+ }
456
+ if (current.rootTaskId !== null) {
457
+ const coordinator = current.workers.find(w => w.workerId === current.coordinatorId);
458
+ const contract = await deps.tasks(coordinator.worktree).treeContract(current.rootTaskId);
459
+ await repository.mutate(runId, saved => ({ ...saved, currentContractHash: contract,
460
+ contractRevision: saved.contractRevision + (saved.currentContractHash === contract ? 0 : 1) }));
461
+ }
462
+ }
463
+ async function transitionPolicy(journal) {
464
+ const proposal = await readPrivateFile(join(directory, runId, "policy-proposals", journal.transitionId + ".json"), commonDir);
465
+ if (proposal === null || "policy-" + sha256(proposal) !== journal.transitionId)
466
+ throw new AgentOpsError("RUN_POLICY_PROPOSAL_CHANGED", "Policy proposal is missing or changed; preserve the pending journal.");
467
+ const input = JSON.parse(proposal);
468
+ await stopAll("stop", false);
469
+ let current = (await repository.read(runId));
470
+ for (const writer of current.workers.filter(w => w.worktree !== null && w.status !== "delivered"))
471
+ if ((await collectChangeSurface(gitRunner(writer.worktree))).paths.length > 0)
472
+ throw new AgentOpsError("RUN_POLICY_DIRTY", "Every writer must commit before a policy transition.");
473
+ if (journal.stages.at(-1).stage === "prepared") {
474
+ journal = await markPolicyTransitionStage(repository, runId, journal.transitionId, { stage: "fenced" });
475
+ }
476
+ if (journal.stages.at(-1).stage === "fenced") {
477
+ if (current.policyBinding.artifactDigest === journal.oldPolicyArtifact.artifactDigest) {
478
+ const coordinator = current.workers.find(w => w.workerId === current.coordinatorId);
479
+ const old = (await readRunPolicy(current, await runRuntimeHash(), Date.now(), true));
480
+ if (current.rootTaskId !== null && journal.deliveredMarkers.length > 0) {
481
+ const record = await readWorktreeRecord(coordinator.worktree);
482
+ if (record === null)
483
+ throw new AgentOpsError("RUN_WORKTREE_MISSING", "Policy transition requires the registered coordinator.");
484
+ await integrateSessionChildren({ ...deps, loadConfig: async () => old.config }, record, current.rootTaskId, journal.deliveredMarkers.map(w => w.workerId));
485
+ }
486
+ await bindRunPolicy(repository, runId, { config: input.config, approvals: input.approvals, baseTrustBinding: old.baseTrustBinding,
487
+ runtimeHash: old.runtimeHash, expectedDigest: current.policyBinding.artifactDigest });
488
+ }
489
+ else if (current.policyBinding.configHash !== calculateConfigHash(input.config))
490
+ throw new AgentOpsError("RUN_POLICY_TRANSITION_STALE", "Policy no longer matches the pending assessment.");
491
+ journal = await markPolicyTransitionStage(repository, runId, journal.transitionId, { stage: "bound" });
492
+ }
493
+ if (journal.stages.at(-1).stage === "bound") {
494
+ journal = await renewPendingRunPolicy(repository, runId, journal);
495
+ await synchronizePolicy();
496
+ current = (await repository.read(runId));
497
+ const coordinator = current.workers.find(w => w.workerId === current.coordinatorId);
498
+ const control = new RunControlService(repository);
499
+ const policy = (await readRunPolicy(current, await runRuntimeHash()));
500
+ for (const assessment of policy.approvals) {
501
+ const [kind, id] = assessment.capabilityId.split(":");
502
+ const capability = kind === "command" ? policy.config.verification.commands.find(c => c.id === id) :
503
+ kind === "runner" ? policy.config.verification.acceptanceRunners?.find(c => c.id === id) : policy.config.worktree?.setup?.[Number(id)];
504
+ const command = canonicalJson(capability), resource = canonicalJson({ repository: root, capabilityId: assessment.capabilityId });
505
+ await control.recordAuthorization(runId, { authorizationId: journal.transitionId + ":" + assessment.capabilityId,
506
+ workerId: coordinator.workerId, generation: coordinator.generation, nativeSessionId: coordinator.nativeSessionId,
507
+ operation: "policy-capability", command, resource, commandDigest: sha256(command), resourceDigest: sha256(resource),
508
+ reason: assessment.reason, result: "auto-approved", source: "coordinator-policy-review", policyConfigHash: current.policyBinding.configHash,
509
+ runtimeHash: current.policyBinding.runtimeHash, expiresAt: current.policyBinding.expiresAt, policyArtifactDigest: current.policyBinding.artifactDigest });
510
+ }
511
+ journal = await markPolicyTransitionStage(repository, runId, journal.transitionId, { stage: "synchronized",
512
+ contractHash: current.currentContractHash, contractRevision: current.contractRevision, taskIds: current.tasks.map(t => t.taskId) });
513
+ }
514
+ await completePolicyTransition(repository, runId, journal.transitionId);
515
+ current = (await repository.read(runId));
516
+ if (Date.parse(current.policyBinding.expiresAt) <= Date.now()) {
517
+ const old = (await readRunPolicy(current, await runRuntimeHash(), Date.now(), true));
518
+ await bindRunPolicy(repository, runId, { config: old.config, runtimeHash: old.runtimeHash,
519
+ baseTrustBinding: old.baseTrustBinding, expectedDigest: current.policyBinding.artifactDigest });
520
+ }
521
+ return journal.resumeIds.map(w => w.workerId);
522
+ }
523
+ async function repair(worker, code, message) {
524
+ const current = (await repository.read(runId));
525
+ const saved = current.workers.find(w => w.workerId === worker.workerId);
526
+ const localTasks = await new TaskService(new FileTaskStore(join(worker.worktree, ".agent-ops", "tasks", "state.json"), worker.worktree)).list();
527
+ const pins = await pendingFindingPins(worker.worktree, localTasks, worker.taskId);
528
+ const observation = await observeRunFailure(current, worker.worktree, localTasks, worker.taskId, code, pins);
529
+ const failed = observation.checks.find(check => check.status !== "PASS");
530
+ const previousPath = join(directory, runId, "failure-observations", worker.workerId + ".json");
531
+ const previous = await readPrivateFile(previousPath, commonDir);
532
+ const priorDigest = previous === null ? null : JSON.parse(previous).digest;
533
+ if (priorDigest !== null && !/^[a-f0-9]{64}$/u.test(priorDigest))
534
+ throw new AgentOpsError("RUN_FAILURE_OBSERVATION_CHANGED", "Failure observation pointer is invalid.");
535
+ const prior = priorDigest === null ? null : await readPrivateFile(join(directory, runId, "failure-observations", priorDigest + ".json"), commonDir);
536
+ if (priorDigest !== null && (prior === null || sha256(prior) !== priorDigest))
537
+ throw new AgentOpsError("RUN_FAILURE_OBSERVATION_CHANGED", "Failure observation is missing or changed.");
538
+ const usefulProgress = prior !== null && compareFailureObservations(JSON.parse(prior), observation).usefulProgress;
539
+ const content = canonicalJson(observation), digest = sha256(content);
540
+ await writePrivateFile(join(directory, runId, "failure-observations", digest + ".json"), content, commonDir);
541
+ await writePrivateFile(previousPath, canonicalJson({ digest }), commonDir);
542
+ const result = await supervisor.recordFailure(runId, worker.workerId, worker.generation, {
543
+ checkId: failed?.checkId ?? code, pinId: failed?.pinId ?? null, phase: failed?.phase ?? "native", failureClass: failed?.failureClass ?? code,
544
+ fingerprint: observation.failureKey ?? sha256(canonicalJson({ taskId: worker.taskId, code })), progressDigest: observation.progressDigest,
545
+ diagnosticDigest: sha256(message), round: (saved.lastFailure?.round ?? 0) + 1, usefulProgress
546
+ });
547
+ if (result.noProgress) {
548
+ await scheduler.blockTaskAndDependents(runId, worker.taskId, code);
549
+ if (worker.workerId === current.coordinatorId)
550
+ await repository.mutate(runId, state => ({ ...state, status: "blocked", disableRestart: true }));
551
+ return;
552
+ }
553
+ const latest = result.state.workers.find(w => w.workerId === worker.workerId);
554
+ if (["fenced", "stopped"].includes(latest.status))
555
+ await start(latest, message);
556
+ else
557
+ await supervisor.sendRepair(runId, worker.workerId, worker.generation, message);
558
+ }
559
+ async function acceptReceipt(current, path) {
560
+ const source = await readPrivateFile(path, commonDir);
561
+ if (source === null)
562
+ throw new AgentOpsError("RUN_RECEIPT_REQUIRED", "Final receipt is missing.");
563
+ const receipt = JSON.parse(source);
564
+ const latest = (await repository.read(runId));
565
+ if (latest.integration === null || latest.integration.receiptPath !== path || latest.integration.receiptDigest !== sha256(source))
566
+ throw new AgentOpsError("RUN_INTEGRATION_RECEIPT_INVALID", "Receipt must be the exact sealed receipt of the current run transaction.");
567
+ validateRunIntegrationReceipt(latest.integration, receipt);
568
+ await recoverRunIntegrationAfterCleanup(commonDir, runId, deps.git);
569
+ if (receipt.sessionId !== current.ownerSessionId ||
570
+ !receipt.tasks.some(record => record.task.id === current.rootTaskId && record.task.goal === current.goal) ||
571
+ receipt.tasks.some(record => record.status !== "complete") ||
572
+ Object.values(receipt.verification).some(seal => sha256(JSON.stringify(seal.value)) !== seal.digest) ||
573
+ Object.entries(receipt.executionArtifacts ?? {}).some(([path, seal]) => typeof seal.value !== "string" ||
574
+ sha256(JSON.stringify(seal.value)) !== seal.digest || path !== ".agent-ops/tasks/acceptance/" + sha256(seal.value) + ".json") ||
575
+ Object.values(receipt.historicalReviews ?? {}).some(seal => typeof seal.value !== "string" || sha256(JSON.stringify(seal.value)) !== seal.digest) ||
576
+ Object.values(receipt.reviews).some(seals => sha256(JSON.stringify(seals.attestation.value)) !== seals.attestation.digest ||
577
+ sha256(JSON.stringify(seals.report.value)) !== seals.report.digest))
578
+ throw new AgentOpsError("RUN_RECEIPT_TARGET_CHANGED", "Receipt is not complete proof for the current run and target.");
579
+ await readFinishedReview(deps, root, commonDir, path);
580
+ for (const history of Object.values(receipt.historicalReviews ?? {}))
581
+ await recordReviewRunUsage(repository, runId, JSON.parse(history.value));
582
+ for (const review of Object.values(receipt.reviews))
583
+ await recordReviewRunUsage(repository, runId, review.report.value);
584
+ const finalContract = treeContractHash(receipt.tasks.map(record => record.task));
585
+ await repository.mutate(runId, current => ({ ...current, currentContractHash: finalContract,
586
+ contractRevision: current.contractRevision + (current.currentContractHash === finalContract ? 0 : 1) }));
587
+ await supervisor.finalize(runId, { targetCommit: receipt.candidateHead, candidateCommit: receipt.candidateHead,
588
+ verificationPass: true, reviewPass: true, taskStateComplete: true, receiptWritten: true, receiptDigest: sha256(source) });
589
+ try {
590
+ await stopAll("stop");
591
+ await launchd.disableRestart(runDescriptor(current), "verified final integration completed");
592
+ }
593
+ catch {
594
+ await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_COMPLETE_CLEANUP_PENDING",
595
+ workerId: null, taskId: null, detail: "Final receipt is valid; background cleanup needs reconciliation. Restart remains disabled." });
596
+ }
597
+ }
598
+ try {
599
+ if (!state.budget.activeIntervals.some(interval => interval.endMs === null))
600
+ await repository.mutate(runId, current => ({ ...current, budget: { ...current.budget,
601
+ activeIntervals: [...current.budget.activeIntervals, { startMs: Date.now(), endMs: null }] } }));
602
+ if (state.proofProcess != null) {
603
+ await transport.stop({ nativeSessionId: "run-proof", nativeJobId: null, ...state.proofProcess,
604
+ instance: state.nativeInstance, generation: 1, reason: "reconcile interrupted evidence process" });
605
+ await repository.mutate(runId, current => ({ ...current, proofProcess: null }));
606
+ }
607
+ await stopAll("crash", false);
608
+ state = (await repository.read(runId));
609
+ if (state.rootTaskId === null) {
610
+ const coordinator = state.workers.find(w => w.workerId === state.coordinatorId);
611
+ let record = await readWorktreeRecord(coordinator.worktree);
612
+ if (record === null) {
613
+ record = await ensureSessionWorktree(await runWorktreeDependencies(deps, state), { cwd: root, sessionId: state.ownerSessionId,
614
+ runOwnership: { runId, coordinatorId: state.coordinatorId, workerId: coordinator.workerId, ownerSessionId: coordinator.ownerSessionId, generation: coordinator.generation } });
615
+ }
616
+ else {
617
+ await ensureSetup(record.path);
618
+ }
619
+ await repository.mutate(runId, current => ({ ...current, workers: current.workers.map(w => w.workerId === coordinator.workerId ? { ...w, worktree: record.path } : w) }));
620
+ await writePrivateFile(join(record.path, ".agent-ops/tasks/run-goal.json"), JSON.stringify({ runId, goal: state.goal, goalHash: state.goalHash }), record.path);
621
+ state = (await repository.read(runId));
622
+ }
623
+ if (state.integration !== null) {
624
+ let receipt = state.integration.receiptPath;
625
+ if (state.integration.status !== "cleaned") {
626
+ receipt = await recoverIntegration(state);
627
+ }
628
+ if (receipt === null)
629
+ throw new AgentOpsError("RUN_RECEIPT_REQUIRED", "Recovered integration has no final receipt.");
630
+ await acceptReceipt(state, receipt);
631
+ return;
632
+ }
633
+ const pendingPolicy = await discoverPendingPolicyTransition(repository, runId);
634
+ if (pendingPolicy !== null)
635
+ await transitionPolicy(pendingPolicy);
636
+ else if (state.policyBinding !== undefined)
637
+ await synchronizePolicy();
638
+ state = (await repository.read(runId));
639
+ for (const worker of state.workers.filter(w => active(w) || w.status === "stopped")) {
640
+ if (worker.workerId !== state.coordinatorId && (worker.worktree === null ||
641
+ !(await deps.tasks(worker.worktree).list()).some(r => r.task.id === worker.taskId)))
642
+ continue;
643
+ try {
644
+ await start(worker);
645
+ }
646
+ catch (cause) {
647
+ await repository.appendEvent(runId, { type: "diagnostic", code: cause instanceof AgentOpsError ? cause.code : "RUN_RECOVERY_FAILED",
648
+ workerId: worker.workerId, taskId: worker.taskId, detail: "Writer recovery refused; preserve its checkout until the prerequisite is corrected." });
649
+ if (worker.nativeSessionId !== null)
650
+ await supervisor.stopWorker(runId, worker.workerId, worker.generation, "crash");
651
+ await scheduler.blockTaskAndDependents(runId, worker.taskId, cause instanceof AgentOpsError ? cause.code : "RUN_RECOVERY_FAILED");
652
+ await repository.mutate(runId, current => ({ ...current,
653
+ ...(worker.workerId === current.coordinatorId ? { status: "blocked", disableRestart: true } : {}),
654
+ workers: current.workers.map(w => w.workerId === worker.workerId ? { ...w, status: "blocked" } : w) }));
655
+ }
656
+ }
657
+ for (;;) {
658
+ state = (await repository.read(runId));
659
+ if (state.rootTaskId !== null && state.workers[0]?.worktree !== null) {
660
+ const coordinatorRoot = state.workers[0].worktree;
661
+ const tasks = new TaskService(new FileTaskStore(join(coordinatorRoot, ".agent-ops", "tasks", "state.json"), coordinatorRoot));
662
+ const rootTask = await tasks.status({ taskId: state.rootTaskId });
663
+ if (rootTask.task.goal !== state.goal)
664
+ throw new AgentOpsError("RUN_GOAL_CHANGED", "The task tree no longer binds the immutable user goal.");
665
+ const contract = await tasks.treeContract(state.rootTaskId);
666
+ if (contract !== state.currentContractHash) {
667
+ const writers = state.workers.filter(active).map(worker => worker.workerId);
668
+ await stopAll("stop", false);
669
+ state = await repository.mutate(runId, current => ({ ...current, currentContractHash: contract, contractRevision: current.contractRevision + 1 }));
670
+ for (const id of writers) {
671
+ const worker = state.workers.find(worker => worker.workerId === id);
672
+ await start(worker, "Contract changed. Preserve the original goal and rerun proof for the current criteria. Old generations and native completion are unproven.");
673
+ }
674
+ }
675
+ }
676
+ if (state.disableRestart || ["stopping", "paused", "budget-limited", "blocked"].includes(state.status)) {
677
+ await stopAll(state.status === "budget-limited" ? "budget" : "stop");
678
+ return;
679
+ }
680
+ const elapsed = activeWallTimeMs(state.budget.activeIntervals, Date.now());
681
+ if (elapsed >= state.budget.limitMs) {
682
+ await repository.mutate(runId, current => ({ ...current, status: "budget-limited", disableRestart: true }));
683
+ await launchd.disableRestart(runDescriptor(state), "active run time budget exhausted");
684
+ await stopAll("budget");
685
+ return;
686
+ }
687
+ await repository.mutate(runId, current => ({ ...current, budget: { ...current.budget,
688
+ accumulatedMs: activeWallTimeMs(current.budget.activeIntervals, Date.now()), lastObservedAt: new Date().toISOString() } }));
689
+ await schedule();
690
+ state = (await repository.read(runId));
691
+ for (const worker of state.workers.filter(w => ["running", "idle"].includes(w.status))) {
692
+ await supervisor.heartbeat(runId, worker.workerId, worker.generation);
693
+ const observation = await supervisor.observeWorker(runId, worker.workerId, worker.generation);
694
+ if (!observation.processAlive) {
695
+ try {
696
+ await repair(worker, "RUN_NATIVE_EXITED", "Native process exited without a verified delivery. Resume the assigned task and provide the current evidence.");
697
+ }
698
+ catch (cause) {
699
+ await scheduler.blockTaskAndDependents(runId, worker.taskId, cause instanceof AgentOpsError ? cause.code : "RUN_RECOVERY_FAILED");
700
+ await repository.mutate(runId, state => ({ ...state,
701
+ ...(worker.workerId === state.coordinatorId ? { status: "blocked", disableRestart: true } : {}),
702
+ workers: state.workers.map(w => w.workerId === worker.workerId ? { ...w, status: "blocked" } : w) }));
703
+ }
704
+ continue;
705
+ }
706
+ const requestPath = join(worker.worktree, ".agent-ops", "tasks", "run-request.json");
707
+ const source = await readPrivateFile(requestPath, worker.worktree);
708
+ if (source === null || Buffer.byteLength(source) > 128 * 1024) {
709
+ if (observation.nativeGoalState === "complete")
710
+ await repair(worker, "RUN_NATIVE_COMPLETION_UNPROVEN", "Native completion has no agent-ops delivery or final receipt. Read run-instructions.json, complete the evidence loop and submit handoff or finalize.");
711
+ continue;
712
+ }
713
+ async function rejectRequest(code) {
714
+ const digest = sha256(source);
715
+ const diagnostic = join(directory, runId, "invalid-requests", worker.workerId + "-" + worker.generation + "-" + digest + ".json");
716
+ if (await readPrivateFile(diagnostic, commonDir) === null) {
717
+ await writePrivateFile(diagnostic, canonicalJson({ code, workerId: worker.workerId, generation: worker.generation, requestDigest: digest }), commonDir);
718
+ await repository.appendEvent(runId, { type: "diagnostic", code, workerId: worker.workerId, taskId: worker.taskId,
719
+ detail: "Supervisor request is malformed or stale; a diagnostic response is available in the current checkout." });
720
+ }
721
+ await writePrivateFile(join(worker.worktree, ".agent-ops/tasks/run-response.json"), JSON.stringify({ code,
722
+ workerId: worker.workerId, generation: worker.generation, error: "Read current run-instructions.json and replace the malformed or stale request atomically." }), worker.worktree);
723
+ if (observation.nativeGoalState === "complete")
724
+ await repair(worker, code, "A malformed or stale request cannot establish completion. Read current run-instructions.json and replace run-request.json atomically.");
725
+ }
726
+ let request;
727
+ try {
728
+ request = JSON.parse(source);
729
+ }
730
+ catch {
731
+ await rejectRequest("RUN_REQUEST_INVALID");
732
+ continue;
733
+ }
734
+ if (!plain(request) || typeof request.requestId !== "string" || !/^[A-Za-z0-9-]{1,64}$/u.test(request.requestId)) {
735
+ await rejectRequest("RUN_REQUEST_INVALID");
736
+ continue;
737
+ }
738
+ if (request.workerId !== worker.workerId || request.generation !== worker.generation) {
739
+ await rejectRequest("RUN_REQUEST_STALE");
740
+ continue;
741
+ }
742
+ const donePath = join(directory, runId, "requests", sha256(request.requestId) + ".json");
743
+ const requestDigest = sha256(canonicalJson(request));
744
+ const completed = await readPrivateFile(donePath, commonDir);
745
+ if (completed !== null) {
746
+ const cached = JSON.parse(completed);
747
+ const response = plain(cached) && cached.requestDigest === requestDigest ? cached.response :
748
+ { requestId: request.requestId, code: "RUN_REQUEST_ID_CONFLICT", error: "A request ID cannot be reused for different content." };
749
+ await writePrivateFile(join(worker.worktree, ".agent-ops", "tasks", "run-response.json"), JSON.stringify(response), worker.worktree);
750
+ if (observation.nativeGoalState === "complete")
751
+ await repair(worker, "RUN_NATIVE_COMPLETION_UNPROVEN", "The previous request response is available, but current delivery or final proof is still missing. Continue the assigned goal and submit a new request.");
752
+ continue;
753
+ }
754
+ const current = (await repository.read(runId));
755
+ let response;
756
+ try {
757
+ if (current.status !== "active" || request.contractHash !== current.currentContractHash)
758
+ throw new AgentOpsError("RUN_CONTRACT_CHANGED", "Request must bind the current run contract.");
759
+ const fields = request.action === "policy" ? ["configFile", "expectedPolicyHash", "approvals"] : request.action === "plan" ? ["title", "intent", "criteria"] : request.action === "question" ? ["prompt"] : request.action === "worker" ? ["title", "intent", "criteria", "dependencies"] :
760
+ request.action === "handoff" || request.action === "finalize" ? ["sourceFiles"] : [];
761
+ if (Object.keys(request).some(key => !["requestId", "workerId", "generation", "contractHash", "action", ...fields].includes(key)))
762
+ throw new AgentOpsError("RUN_REQUEST_INVALID", "Unknown request fields are not accepted.");
763
+ if (request.action === "policy") {
764
+ if (worker.workerId !== current.coordinatorId || current.policyBinding === undefined ||
765
+ request.expectedPolicyHash !== current.policyBinding.configHash || typeof request.configFile !== "string" || !Array.isArray(request.approvals))
766
+ throw new AgentOpsError("RUN_POLICY_STALE", "Only the coordinator can assess the current run execution policy.");
767
+ const path = await resolveContainedPath(worker.worktree, request.configFile);
768
+ const source = await readPrivateFile(path, worker.worktree);
769
+ if (source === null || Buffer.byteLength(source) > 128 * 1024)
770
+ throw new AgentOpsError("RUN_POLICY_INVALID", "Proposed policy must be bounded private JSON.");
771
+ const policy = await readRunPolicy(current, await runRuntimeHash());
772
+ if (policy === null)
773
+ throw new AgentOpsError("RUN_POLICY_INVALID", "Run has no original policy binding.");
774
+ const candidate = validateRunPolicyChange(policy.config, JSON.parse(source), request.approvals);
775
+ const proposal = canonicalJson({ config: candidate, approvals: request.approvals, oldArtifactDigest: current.policyBinding.artifactDigest });
776
+ const transitionId = "policy-" + sha256(proposal);
777
+ await writePrivateFile(join(directory, runId, "policy-proposals", transitionId + ".json"), proposal, commonDir);
778
+ const journal = await beginPolicyTransition(repository, runId, { transitionId,
779
+ expectedOldArtifactDigest: current.policyBinding.artifactDigest, expectedContractHash: current.currentContractHash });
780
+ const writers = await transitionPolicy(journal);
781
+ const changed = (await repository.read(runId));
782
+ for (const id of writers)
783
+ await start(changed.workers.find(w => w.workerId === id), "Execution policy was assessed and synchronized. Read the current instructions, preserve original baselines, and obtain fresh evidence. Authorization expires with this run.");
784
+ response = { requestId: request.requestId, code: "RUN_POLICY_SYNCHRONIZED", policyConfigHash: changed.policyBinding.configHash,
785
+ contractHash: changed.currentContractHash };
786
+ }
787
+ else if (request.action === "plan") {
788
+ if (current.rootTaskId !== null || typeof request.title !== "string" || typeof request.intent !== "string" || !Array.isArray(request.criteria))
789
+ throw new AgentOpsError("RUN_PLAN_INVALID", "Initial plan requires a title, intent and valid criteria.");
790
+ const config = await deps.loadConfig(worker.worktree);
791
+ const service = new TaskService(new FileTaskStore(join(worker.worktree, ".agent-ops", "tasks", "state.json"), worker.worktree), { completion: { root: worker.worktree, gitRunner: gitRunner(worker.worktree), loadConfig: async () => config } });
792
+ const task = await service.create({ title: request.title, intent: request.intent, goal: current.goal,
793
+ criteria: request.criteria, sessionId: worker.ownerSessionId, policyConfigHash: calculateConfigHash(config) });
794
+ await repository.mutate(runId, saved => ({ ...saved, rootTaskId: task.task.id, currentContractHash: treeContractHash([task.task]),
795
+ tasks: saved.tasks.map(node => node.taskId === worker.taskId ? { ...node, taskId: task.task.id } : node),
796
+ workers: saved.workers.map(w => w.workerId === worker.workerId ? { ...w, taskId: task.task.id } : w) }));
797
+ response = { requestId: request.requestId, code: "RUN_PLAN_CREATED", task: task.task, contractHash: treeContractHash([task.task]) };
798
+ await recordRunPhase(repository, runId, worker.workerId, "implementing", { taskId: task.task.id, update: { passed: 0, total: task.task.criteria.length } });
799
+ await instructions((await repository.read(runId)).workers.find(w => w.workerId === worker.workerId));
800
+ }
801
+ else if (request.action === "worker") {
802
+ if (worker.workerId !== current.coordinatorId || current.rootTaskId === null)
803
+ throw new AgentOpsError("RUN_COORDINATOR_REQUIRED", "Only the registered coordinator can plan child workers.");
804
+ const taskId = "task-" + (await import("node:crypto")).randomUUID();
805
+ const plan = workerPlan({ title: request.title, intent: request.intent, criteria: request.criteria, dependencies: request.dependencies }, taskId, current);
806
+ const { taskId: _id, ...specification } = plan;
807
+ const content = JSON.stringify(specification);
808
+ await writePrivateFile(join(directory, runId, "plans", taskId + ".json"), content, commonDir);
809
+ await scheduler.addTasks(runId, [{ taskId, dependencies: plan.dependencies, planDigest: sha256(content), status: "planned",
810
+ workerId: null, deliveryDigest: null, sourceCommit: null, blockedReason: null }]);
811
+ response = { requestId: request.requestId, code: "RUN_WORKER_PLANNED", taskId };
812
+ }
813
+ else if (request.action === "handoff") {
814
+ if (worker.workerId === current.coordinatorId)
815
+ throw new AgentOpsError("RUN_COORDINATOR_FINALIZE_REQUIRED", "Coordinator requests finalization after all children deliver.");
816
+ const record = await readWorktreeRecord(worker.worktree);
817
+ if (record === null || (await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
818
+ throw new AgentOpsError("RUN_DELIVERY_DIRTY", "Local handoff requires a clean committed checkout.");
819
+ const tasks = new TaskService(new FileTaskStore(join(worker.worktree, ".agent-ops", "tasks", "state.json"), worker.worktree));
820
+ const contract = await tasks.treeContract(worker.taskId);
821
+ const head = (await deps.git(worker.worktree, ["rev-parse", "HEAD"])).stdout.trim();
822
+ if (head === record.base) {
823
+ const scope = await resolveReviewScope({ root: worker.worktree, runner: gitRunner(worker.worktree), base: head,
824
+ noChangePaths: request.sourceFiles });
825
+ await tasks.recordEvidence(worker.taskId, {}, undefined, scope.changedFiles);
826
+ }
827
+ const deliveryDigest = sha256(JSON.stringify({ runId, workerId: worker.workerId, generation: worker.generation, contract, head }));
828
+ const handoff = { workerId: worker.workerId, generation: worker.generation, deliveryDigest, contractDigest: contract, sourceCommit: head };
829
+ await supervisor.beginHandoff(runId, handoff);
830
+ const observation = await transport.inspect({ nativeSessionId: worker.nativeSessionId, nativeJobId: worker.nativeJobId,
831
+ processId: worker.processId, processIdentity: worker.processIdentity, instance: current.nativeInstance });
832
+ await supervisor.confirmHandoff(runId, handoff, { processDead: !observation.processAlive });
833
+ await assertRunDeliverySnapshot(worker.worktree, worker.taskId, head, contract, tasks);
834
+ const artifactRefs = [];
835
+ await recordRunPhase(repository, runId, worker.workerId, "verifying");
836
+ for (const localTask of (await tasks.list()).filter(task => task.supersededBy === undefined && task.status !== "archived")) {
837
+ if (head === record.base)
838
+ await tasks.recordEvidence(localTask.task.id, {}, undefined, (await tasks.status({ taskId: worker.taskId })).noChangePaths);
839
+ const result = await step(worker.worktree, ["verify", "--task", localTask.task.id, "--base", record.base], current);
840
+ if (localTask.task.id === worker.taskId)
841
+ await recordRunPhase(repository, runId, worker.workerId, "verifying", { taskId: worker.taskId,
842
+ update: { verify: result.status === "ok" ? "PASS" : "FAIL", ...criteriaProgress(localTask.task.criteria, plain(result.data) && plain(result.data.report) ? result.data.report : {}) } });
843
+ if (result.status !== "ok")
844
+ throw new AgentOpsError(result.code, "Local verifier did not pass. Preserve the failed check diagnostics for repair.");
845
+ artifactRefs.push(...Object.values((await tasks.status({ taskId: localTask.task.id })).evidence).flat());
846
+ }
847
+ await assertRunDeliverySnapshot(worker.worktree, worker.taskId, head, contract, tasks);
848
+ if (head === record.base) {
849
+ const scope = await resolveReviewScope({ root: worker.worktree, runner: gitRunner(worker.worktree), base: head,
850
+ noChangePaths: (await tasks.status({ taskId: worker.taskId })).noChangePaths });
851
+ await writeNoChangeDelivery(record, { schemaVersion: 1, deliveryKind: "no-change", sourceCommit: head,
852
+ deliveryDigest, contractDigest: contract, artifactRefs, reviewScope: JSON.stringify(scope), runId,
853
+ workerId: worker.workerId, generation: worker.generation });
854
+ }
855
+ await supervisor.publishDelivery(runId, handoff, { noChange: head === record.base, artifactRefs });
856
+ await scheduler.markDelivered(runId, worker.taskId, deliveryDigest, head);
857
+ await recordRunPhase(repository, runId, worker.workerId, "integrating");
858
+ response = { requestId: request.requestId, code: "RUN_WORKER_DELIVERED", taskId: worker.taskId, sourceCommit: head };
859
+ }
860
+ else if (request.action === "question") {
861
+ if (typeof request.prompt !== "string" || request.prompt.trim().length === 0 || request.prompt.length > 4096)
862
+ throw new AgentOpsError("RUN_QUESTION_INVALID", "Question must be bounded and substantive.");
863
+ await stopAll("stop");
864
+ await repository.mutate(runId, saved => ({ ...saved, status: "awaiting-input", questions: [...saved.questions,
865
+ { questionId: request.requestId, prompt: request.prompt, askedAt: new Date().toISOString(), answeredAt: null, answerDigest: null }] }));
866
+ response = { requestId: request.requestId, code: "RUN_AWAITING_INPUT" };
867
+ }
868
+ else if (request.action === "finalize") {
869
+ if (worker.workerId !== current.coordinatorId || current.tasks.some(task => task.taskId !== current.rootTaskId && task.status !== "delivered"))
870
+ throw new AgentOpsError("RUN_DELIVERIES_PENDING", "The coordinator must wait for every planned child delivery before final integration.");
871
+ if (current.rootTaskId === null)
872
+ throw new AgentOpsError("RUN_PLAN_REQUIRED", "Create the goal-bound task before requesting finalization.");
873
+ const pending = await pendingFindingPins(worker.worktree, await deps.tasks(worker.worktree).list(), current.rootTaskId);
874
+ if (pending.length > 0) {
875
+ await writePrivateFile(join(worker.worktree, ".agent-ops", "tasks", "run-pending-findings.json"), JSON.stringify({ findings: pending }), worker.worktree);
876
+ throw new AgentOpsError("RUN_FINDING_RATCHET_REQUIRED", "Pin the blocking findings in run-pending-findings.json; mechanically testable findings require regression checks, and review-only fallback needs an explicit recorded reason.");
877
+ }
878
+ if (request.sourceFiles !== undefined) {
879
+ const base = (await deps.git(root, ["rev-parse", current.targetBranch + "^{commit}"])).stdout.trim();
880
+ const scope = await resolveReviewScope({ root: worker.worktree, runner: gitRunner(worker.worktree), base,
881
+ noChangePaths: request.sourceFiles });
882
+ await new TaskService(new FileTaskStore(join(worker.worktree, ".agent-ops", "tasks", "state.json"), worker.worktree))
883
+ .recordEvidence(current.rootTaskId, {}, undefined, scope.changedFiles);
884
+ }
885
+ await stopAll("stop", false);
886
+ await recordRunPhase(repository, runId, worker.workerId, "verifying");
887
+ const result = await advance(current);
888
+ if (result.status !== "ok")
889
+ throw new AgentOpsError(result.code, "Final proof failed; inspect saved verifier and fresh review reports before repair.");
890
+ const data = result.data;
891
+ if (!plain(data) || typeof data.receipt !== "string")
892
+ throw new AgentOpsError("RUN_RECEIPT_REQUIRED", "Finalization requires the receipt returned by the existing finish gate.");
893
+ await acceptReceipt(current, data.receipt);
894
+ response = { requestId: request.requestId, code: "RUN_COMPLETE", receipt: data.receipt };
895
+ }
896
+ else
897
+ throw new AgentOpsError("RUN_ACTION_INVALID", "Unsupported supervisor request.");
898
+ }
899
+ catch (cause) {
900
+ response = { requestId: request.requestId, code: cause instanceof AgentOpsError ? cause.code : "RUN_REQUEST_FAILED", error: cause instanceof AgentOpsError ? cause.message : "Request failed; inspect local diagnostics." };
901
+ const latest = (await repository.read(runId));
902
+ const stopped = latest.workers.find(w => w.workerId === worker.workerId);
903
+ if (request.action === "finalize" && latest.integration !== null && (latest.integration.status !== "prepared" ||
904
+ (await deps.git(root, ["rev-parse", latest.targetBranch + "^{commit}"])).stdout.trim() === latest.integration.candidate)) {
905
+ const recovered = await recoverIntegration(latest);
906
+ await acceptReceipt(latest, recovered);
907
+ response = { requestId: request.requestId, code: "RUN_COMPLETE", receipt: recovered };
908
+ }
909
+ else if (request.action === "policy" && await discoverPendingPolicyTransition(repository, runId) !== null) {
910
+ await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_POLICY_RECOVERY_REQUIRED", workerId: worker.workerId, taskId: worker.taskId,
911
+ detail: "Policy transition is preserved; explicit resume must reconcile the pending journal before any writer starts." });
912
+ await repository.mutate(runId, state => ({ ...state, status: "paused", awaitingResume: true, disableRestart: true }));
913
+ await stopAll("stop");
914
+ await launchd.disableRestart(runDescriptor(latest), "pending policy transition needs reconciliation");
915
+ }
916
+ else if (["handoff", "finalize"].includes(String(request.action)) && latest.status === "active" && stopped !== undefined &&
917
+ ["stopped", "fenced"].includes(stopped.status)) {
918
+ await writePrivateFile(join(worker.worktree, ".agent-ops", "tasks", "run-last-failure.json"), JSON.stringify(response), worker.worktree);
919
+ await repair(stopped, cause instanceof AgentOpsError ? cause.code : "RUN_PROOF_FAILED", "Evidence did not pass. Read run-last-failure.json and saved review/verification reports, fix substantive findings, then request a new evidence gate.");
920
+ }
921
+ }
922
+ await writePrivateFile(donePath, JSON.stringify({ requestDigest, response }), commonDir);
923
+ const after = (await repository.read(runId));
924
+ if (after.status === "complete") {
925
+ await writePrivateFile(join(directory, runId, "final-response.json"), JSON.stringify(response), commonDir);
926
+ return;
927
+ }
928
+ await writePrivateFile(join(worker.worktree, ".agent-ops", "tasks", "run-response.json"), JSON.stringify(response), worker.worktree);
929
+ if ((await repository.read(runId)).status === "awaiting-input")
930
+ return;
931
+ }
932
+ await delay(1000);
933
+ }
934
+ }
935
+ catch (cause) {
936
+ await repository.appendEvent(runId, { type: "diagnostic", code: cause instanceof AgentOpsError ? cause.code : "RUN_SUPERVISOR_FAILED",
937
+ workerId: null, taskId: null, detail: "Supervisor stopped; worktree and proof artifacts are preserved." });
938
+ try {
939
+ await stopAll("crash");
940
+ }
941
+ finally {
942
+ await repository.mutate(runId, current => ({ ...current, status: current.status === "complete" ? "complete" : "blocked", disableRestart: true }));
943
+ await launchd.disableRestart(runDescriptor(state), "supervisor could not safely recover");
944
+ }
945
+ }
946
+ });
947
+ }
948
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
949
+ if (process.argv.length !== 4)
950
+ throw new AgentOpsError("RUN_SUPERVISOR_ARGUMENTS", "Supervisor requires canonical repository root and run ID.");
951
+ await superviseNativeRun(process.argv[2], process.argv[3]);
952
+ }