patchwarden 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.en.md +43 -2
  2. package/README.md +43 -2
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2197
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/doctor.d.ts +18 -1
  44. package/dist/doctor.js +579 -348
  45. package/dist/goal/goalReport.d.ts +54 -0
  46. package/dist/goal/goalReport.js +204 -0
  47. package/dist/goal/goalStatus.d.ts +6 -0
  48. package/dist/goal/specKitImport.d.ts +63 -0
  49. package/dist/goal/specKitImport.js +220 -0
  50. package/dist/goal/subgoalSync.js +2 -1
  51. package/dist/httpServer.js +15 -12
  52. package/dist/index.js +7 -4
  53. package/dist/logging.d.ts +7 -1
  54. package/dist/logging.js +8 -0
  55. package/dist/runner/changeCapture.d.ts +3 -3
  56. package/dist/runner/changeCapture.js +63 -39
  57. package/dist/runner/cli.js +7 -6
  58. package/dist/runner/postTaskCleanup.js +26 -2
  59. package/dist/runner/runTask.js +245 -221
  60. package/dist/runner/simpleProcess.js +4 -4
  61. package/dist/runner/watch.js +17 -14
  62. package/dist/security/contentRedaction.d.ts +6 -0
  63. package/dist/security/contentRedaction.js +22 -0
  64. package/dist/smoke-test.js +257 -251
  65. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  66. package/dist/test/unit/apply-patch.test.js +225 -0
  67. package/dist/test/unit/create-task.test.d.ts +1 -0
  68. package/dist/test/unit/create-task.test.js +197 -0
  69. package/dist/test/unit/direct-guards.test.js +124 -9
  70. package/dist/test/unit/evidence-pack.test.js +95 -1
  71. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  72. package/dist/test/unit/get-task-status.test.js +174 -0
  73. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  74. package/dist/test/unit/get-task-summary.test.js +146 -0
  75. package/dist/test/unit/goal-report.test.d.ts +1 -0
  76. package/dist/test/unit/goal-report.test.js +159 -0
  77. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  78. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  79. package/dist/test/unit/path-guard.test.js +24 -0
  80. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  81. package/dist/test/unit/spec-kit-import.test.js +341 -0
  82. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  83. package/dist/test/unit/wait-for-task.test.js +144 -0
  84. package/dist/tools/auditTask.d.ts +8 -63
  85. package/dist/tools/auditTask.js +12 -8
  86. package/dist/tools/createDirectSession.d.ts +1 -1
  87. package/dist/tools/createDirectSession.js +2 -2
  88. package/dist/tools/createTask.d.ts +2 -2
  89. package/dist/tools/createTask.js +4 -4
  90. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  91. package/dist/tools/dispatch/coreDispatch.js +282 -0
  92. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  93. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  94. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  95. package/dist/tools/dispatch/directDispatch.js +115 -0
  96. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  97. package/dist/tools/dispatch/goalDispatch.js +91 -0
  98. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  99. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  100. package/dist/tools/dispatch/types.d.ts +23 -0
  101. package/dist/tools/dispatch/types.js +15 -0
  102. package/dist/tools/evidencePack.d.ts +6 -0
  103. package/dist/tools/evidencePack.js +213 -6
  104. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  105. package/dist/tools/finalizeDirectSession.js +3 -3
  106. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  107. package/dist/tools/goalSubgoalTask.js +2 -2
  108. package/dist/tools/healthCheck.js +3 -3
  109. package/dist/tools/registry.d.ts +3 -3
  110. package/dist/tools/registry.js +60 -503
  111. package/dist/tools/retryTask.d.ts +2 -2
  112. package/dist/tools/retryTask.js +2 -2
  113. package/dist/tools/runTaskLoop.js +4 -4
  114. package/dist/tools/safeViews.d.ts +2 -2
  115. package/dist/tools/safeViews.js +2 -2
  116. package/dist/tools/toolRegistry.js +22 -0
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/docs/CODE_WIKI.md +983 -0
  120. package/docs/agentseal-integration.md +150 -0
  121. package/docs/architecture.md +63 -0
  122. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  123. package/docs/dashboard-overview.md +86 -0
  124. package/docs/demo.md +8 -0
  125. package/docs/direct-session-workflow.md +98 -0
  126. package/docs/evidence-pack-schema.md +215 -0
  127. package/docs/execution-plan-2026-07-09.md +315 -0
  128. package/docs/lineage-evidence-pack-workflow.md +127 -0
  129. package/docs/mcp-inspector-testing.md +200 -0
  130. package/docs/open-source-application.md +162 -0
  131. package/docs/opencode-worker.md +151 -0
  132. package/docs/openhands-worker.md +181 -0
  133. package/docs/release-evidence.md +72 -0
  134. package/docs/roadmap-execution-and-acceptance.md +365 -0
  135. package/docs/spec-kit-integration.md +131 -0
  136. package/docs/task-safe-review-workflow.md +98 -0
  137. package/docs/threat-model.md +79 -0
  138. package/docs/user-feedback.md +40 -0
  139. package/docs/why-patchwarden.md +110 -0
  140. package/package.json +2 -2
  141. package/scripts/checks/control-center-smoke.js +356 -0
  142. package/scripts/checks/lifecycle-smoke.js +29 -23
  143. package/scripts/checks/mcp-smoke.js +3 -1
  144. package/scripts/generate-demo-gif.py +320 -0
  145. package/src/assessments/agentAssessor.ts +3 -3
  146. package/src/assessments/assessmentStore.ts +2 -2
  147. package/src/assessments/confirmCli.ts +5 -4
  148. package/src/config.ts +37 -0
  149. package/src/control/middleware/auth.ts +17 -0
  150. package/src/control/middleware/static.ts +71 -0
  151. package/src/control/routes/audit.ts +321 -0
  152. package/src/control/routes/evidence.ts +107 -0
  153. package/src/control/routes/lineage.ts +92 -0
  154. package/src/control/routes/policy.ts +81 -0
  155. package/src/control/routes/process.ts +204 -0
  156. package/src/control/routes/sessions.ts +251 -0
  157. package/src/control/routes/status.ts +325 -0
  158. package/src/control/routes/taskActions.ts +248 -0
  159. package/src/control/routes/tasks.ts +323 -0
  160. package/src/control/routes/workspace.ts +203 -0
  161. package/src/control/runtime.ts +472 -0
  162. package/src/control/server.ts +471 -0
  163. package/src/control/shared.ts +294 -0
  164. package/src/controlCenter.ts +7 -2347
  165. package/src/direct/directGuards.ts +28 -7
  166. package/src/doctor.ts +741 -481
  167. package/src/goal/goalReport.ts +271 -0
  168. package/src/goal/goalStatus.ts +6 -0
  169. package/src/goal/specKitImport.ts +355 -0
  170. package/src/goal/subgoalSync.ts +4 -2
  171. package/src/httpServer.ts +17 -14
  172. package/src/index.ts +7 -4
  173. package/src/logging.ts +10 -1
  174. package/src/runner/changeCapture.ts +70 -42
  175. package/src/runner/cli.ts +7 -6
  176. package/src/runner/postTaskCleanup.ts +26 -2
  177. package/src/runner/runTask.ts +325 -223
  178. package/src/runner/simpleProcess.ts +4 -4
  179. package/src/runner/watch.ts +17 -14
  180. package/src/security/contentRedaction.ts +29 -0
  181. package/src/smoke-test.ts +252 -250
  182. package/src/test/unit/apply-patch.test.ts +293 -0
  183. package/src/test/unit/create-task.test.ts +255 -0
  184. package/src/test/unit/direct-guards.test.ts +178 -8
  185. package/src/test/unit/evidence-pack.test.ts +110 -1
  186. package/src/test/unit/get-task-status.test.ts +203 -0
  187. package/src/test/unit/get-task-summary.test.ts +173 -0
  188. package/src/test/unit/goal-report.test.ts +189 -0
  189. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  190. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  191. package/src/test/unit/path-guard.test.ts +30 -0
  192. package/src/test/unit/spec-kit-import.test.ts +429 -0
  193. package/src/test/unit/wait-for-task.test.ts +176 -0
  194. package/src/tools/auditTask.ts +99 -59
  195. package/src/tools/createDirectSession.ts +3 -3
  196. package/src/tools/createTask.ts +7 -7
  197. package/src/tools/dispatch/coreDispatch.ts +374 -0
  198. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  199. package/src/tools/dispatch/directDispatch.ts +167 -0
  200. package/src/tools/dispatch/goalDispatch.ts +127 -0
  201. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  202. package/src/tools/dispatch/types.ts +24 -0
  203. package/src/tools/evidencePack.ts +291 -6
  204. package/src/tools/finalizeDirectSession.ts +4 -4
  205. package/src/tools/goalSubgoalTask.ts +2 -2
  206. package/src/tools/healthCheck.ts +3 -3
  207. package/src/tools/registry.ts +68 -628
  208. package/src/tools/retryTask.ts +2 -2
  209. package/src/tools/runTaskLoop.ts +4 -4
  210. package/src/tools/safeViews.ts +2 -2
  211. package/src/tools/toolRegistry.ts +22 -0
  212. package/src/version.ts +1 -1
  213. package/ui/pages/audit.html +192 -3
  214. package/ui/pages/dashboard.html +959 -44
  215. package/ui/pages/direct-sessions.html +505 -53
  216. package/ui/pages/task-detail.html +456 -438
  217. package/ui/pages/tasks.html +598 -61
@@ -17,6 +17,16 @@ const GRACEFUL_KILL_MS = 2000;
17
17
  const MAX_CAPTURE_CHARS = 100_000;
18
18
  const ARTIFACT_COLLECTION_TIMEOUT_MS = 60_000;
19
19
  export async function runTask(taskId) {
20
+ const prepared = await prepareTask(taskId);
21
+ if (!("taskDir" in prepared))
22
+ return prepared;
23
+ const ctx = prepared;
24
+ const state = await executeAgent(ctx);
25
+ await runVerification(ctx, state);
26
+ const evidence = await collectArtifacts(ctx, state);
27
+ return finalizeTask(ctx, state, evidence);
28
+ }
29
+ async function prepareTask(taskId) {
20
30
  const config = getConfig();
21
31
  const tasksDir = getTasksDir(config);
22
32
  const plansDir = getPlansDir(config);
@@ -57,11 +67,10 @@ export async function runTask(taskId) {
57
67
  const message = `verification metadata validation failed: ${errorMessage(error)}`;
58
68
  return failBeforeExecution(taskId, taskDir, message, error);
59
69
  }
60
- // ── Assessment freshness revalidation before execution ──
61
70
  const assessmentId = String(initialStatus.assessment_id || "");
62
71
  if (assessmentId) {
63
72
  try {
64
- const preExecSnapshot = captureRepoSnapshot(repoPath);
73
+ const preExecSnapshot = await captureRepoSnapshot(repoPath);
65
74
  const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
66
75
  if (!validation.valid) {
67
76
  const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
@@ -82,10 +91,6 @@ export async function runTask(taskId) {
82
91
  current_command: null,
83
92
  error: null,
84
93
  });
85
- // v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
86
- // can detect PID reuse and orphaned tasks. watcher_instance_id comes from
87
- // the watcher process env; when run_task MCP tool is used directly, this
88
- // field is intentionally left undefined so ownership cannot be falsely claimed.
89
94
  writeTaskRuntime(taskDir, {
90
95
  task_started_at: new Date(startedAtMs).toISOString(),
91
96
  watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
@@ -95,136 +100,151 @@ export async function runTask(taskId) {
95
100
  let beforeWorkspaceSnapshot;
96
101
  let externalDirtyBaseline = [];
97
102
  try {
98
- beforeSnapshot = captureRepoSnapshot(repoPath);
99
- beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
103
+ beforeSnapshot = await captureRepoSnapshot(repoPath);
104
+ beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : await captureRepoSnapshot(wsRoot);
100
105
  writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
101
106
  writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
102
- // Phase 4: Record external dirty files as baseline before task execution
103
107
  externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
104
108
  }
105
109
  catch (error) {
106
110
  const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
107
111
  return failBeforeExecution(taskId, taskDir, message, error);
108
112
  }
109
- let agentResult = null;
110
- let testResult = skippedTest(testCommand, repoPath, "Agent did not complete successfully.");
111
- const verifyResults = [];
112
- let finalStatus = "failed";
113
- let finalError = null;
114
- let lastCaughtError = null;
113
+ return {
114
+ taskId, taskDir, statusFile, repoPath, wsRoot, config, plansDir, initialStatus,
115
+ planId, agentName, testCommand, changePolicy, verifyCommands, timeoutSeconds,
116
+ startedAtMs, deadlineMs, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
117
+ };
118
+ }
119
+ async function executeAgent(ctx) {
120
+ const state = {
121
+ agentResult: null,
122
+ testResult: skippedTest(ctx.testCommand, ctx.repoPath, "Agent did not complete successfully."),
123
+ verifyResults: [],
124
+ finalStatus: "failed",
125
+ finalError: null,
126
+ lastCaughtError: null,
127
+ };
115
128
  try {
116
- const planFile = resolve(plansDir, planId, "plan.md");
129
+ const planFile = resolve(ctx.plansDir, ctx.planId, "plan.md");
117
130
  if (!existsSync(planFile))
118
- throw new Error(`Plan not found: "${planId}". Save the plan first.`);
131
+ throw new Error(`Plan not found: "${ctx.planId}". Save the plan first.`);
119
132
  const planContent = readFileSync(planFile, "utf-8");
120
- const prompt = buildExecutionPrompt(planContent, repoPath, testCommand);
121
- const invocation = buildAgentInvocation(agentName, repoPath, prompt, config);
122
- const agentCommandLabel = invocation.commandLabel;
123
- setTaskPhase(taskDir, "executing_agent", agentCommandLabel);
124
- agentResult = await runManagedProcess({
133
+ const prompt = buildExecutionPrompt(planContent, ctx.repoPath, ctx.testCommand);
134
+ const invocation = buildAgentInvocation(ctx.agentName, ctx.repoPath, prompt, ctx.config);
135
+ setTaskPhase(ctx.taskDir, "executing_agent", invocation.commandLabel);
136
+ state.agentResult = await runManagedProcess({
125
137
  command: invocation.command,
126
138
  args: invocation.args,
127
- cwd: repoPath,
128
- taskDir,
129
- statusFile,
139
+ cwd: ctx.repoPath,
140
+ taskDir: ctx.taskDir,
141
+ statusFile: ctx.statusFile,
130
142
  phase: "executing_agent",
131
- currentCommand: agentCommandLabel,
132
- deadlineMs,
133
- stdoutPath: join(taskDir, "stdout.log"),
134
- stderrPath: join(taskDir, "stderr.log"),
143
+ currentCommand: invocation.commandLabel,
144
+ deadlineMs: ctx.deadlineMs,
145
+ stdoutPath: join(ctx.taskDir, "stdout.log"),
146
+ stderrPath: join(ctx.taskDir, "stderr.log"),
135
147
  });
148
+ const agentResult = state.agentResult;
136
149
  if (agentResult.terminationReason === "canceled" || agentResult.terminationReason === "killed") {
137
- finalStatus = "canceled";
138
- finalError = agentResult.terminationReason === "killed"
150
+ state.finalStatus = "canceled";
151
+ state.finalError = agentResult.terminationReason === "killed"
139
152
  ? "Task was terminated by kill_task."
140
153
  : "Task was canceled by user request.";
141
154
  }
142
155
  else if (agentResult.terminationReason === "timeout") {
143
- finalError = `Task timed out after ${timeoutSeconds} seconds during agent execution.`;
156
+ state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during agent execution.`;
144
157
  }
145
158
  else if (agentResult.spawnError) {
146
- finalError = `Agent spawn failed: ${agentResult.spawnError}`;
159
+ state.finalError = `Agent spawn failed: ${agentResult.spawnError}`;
147
160
  }
148
161
  else if (agentResult.exitCode !== 0) {
149
- finalError = `Agent exited with code ${agentResult.exitCode}.`;
162
+ state.finalError = `Agent exited with code ${agentResult.exitCode}.`;
150
163
  }
151
- else if (verifyCommands.length > 0) {
152
- for (const command of verifyCommands) {
153
- setTaskPhase(taskDir, "running_tests", command);
154
- const verification = await runTrustedTestCommand(command, repoPath, taskDir, statusFile, deadlineMs);
155
- verifyResults.push(verification);
156
- if (verification.terminationReason || Date.now() >= deadlineMs)
164
+ }
165
+ catch (error) {
166
+ state.lastCaughtError = error;
167
+ state.finalError = errorMessage(error);
168
+ }
169
+ return state;
170
+ }
171
+ async function runVerification(ctx, state) {
172
+ if (state.finalError !== null)
173
+ return state;
174
+ try {
175
+ if (ctx.verifyCommands.length > 0) {
176
+ for (const command of ctx.verifyCommands) {
177
+ setTaskPhase(ctx.taskDir, "running_tests", command);
178
+ const verification = await runTrustedTestCommand(command, ctx.repoPath, ctx.taskDir, ctx.statusFile, ctx.deadlineMs);
179
+ state.verifyResults.push(verification);
180
+ if (verification.terminationReason || Date.now() >= ctx.deadlineMs)
157
181
  break;
158
182
  }
159
- testResult = verifyResults[0] || skippedTest(testCommand, repoPath, "No verification command ran.");
160
- const interrupted = verifyResults.find((result) => result.terminationReason);
161
- const failedVerification = verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
183
+ state.testResult = state.verifyResults[0] || skippedTest(ctx.testCommand, ctx.repoPath, "No verification command ran.");
184
+ const interrupted = state.verifyResults.find((result) => result.terminationReason);
185
+ const failedVerification = state.verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
162
186
  if (interrupted?.terminationReason === "canceled" || interrupted?.terminationReason === "killed") {
163
- finalStatus = "canceled";
164
- finalError = interrupted.terminationReason === "killed"
187
+ state.finalStatus = "canceled";
188
+ state.finalError = interrupted.terminationReason === "killed"
165
189
  ? "Task was terminated by kill_task during verification."
166
190
  : "Task was canceled during verification.";
167
191
  }
168
- else if (interrupted?.terminationReason === "timeout" || Date.now() >= deadlineMs) {
169
- finalError = `Task timed out after ${timeoutSeconds} seconds during verification.`;
192
+ else if (interrupted?.terminationReason === "timeout" || Date.now() >= ctx.deadlineMs) {
193
+ state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during verification.`;
170
194
  }
171
195
  else if (failedVerification) {
172
- finalStatus = "failed_verification";
173
- finalError = failedVerification.spawnError
174
- ? `Verification command \"${failedVerification.command}\" could not start: ${failedVerification.spawnError}`
175
- : `Verification command \"${failedVerification.command}\" exited with code ${failedVerification.exitCode}.`;
196
+ state.finalStatus = "failed_verification";
197
+ state.finalError = failedVerification.spawnError
198
+ ? `Verification command "${failedVerification.command}" could not start: ${failedVerification.spawnError}`
199
+ : `Verification command "${failedVerification.command}" exited with code ${failedVerification.exitCode}.`;
176
200
  }
177
- else if (verifyResults.length === verifyCommands.length) {
178
- finalStatus = "done_by_agent";
201
+ else if (state.verifyResults.length === ctx.verifyCommands.length) {
202
+ state.finalStatus = "done_by_agent";
179
203
  }
180
204
  else {
181
- finalError = "Verification did not complete all configured commands.";
205
+ state.finalError = "Verification did not complete all configured commands.";
182
206
  }
183
207
  }
184
208
  else {
185
- testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
186
- finalStatus = "done_by_agent";
209
+ state.testResult = skippedTest(ctx.testCommand, ctx.repoPath, "No verification command configured.");
210
+ state.finalStatus = "done_by_agent";
187
211
  }
188
212
  }
189
213
  catch (error) {
190
- lastCaughtError = error;
191
- finalError = errorMessage(error);
214
+ state.lastCaughtError = error;
215
+ state.finalError = errorMessage(error);
192
216
  }
193
- let cleanupReport = {
194
- enabled: true,
195
- removed: [],
196
- skipped: [],
197
- source_files_touched: 0,
198
- };
199
- if (finalStatus !== "canceled") {
217
+ return state;
218
+ }
219
+ async function collectArtifacts(ctx, state) {
220
+ let cleanupReport = { enabled: true, removed: [], skipped: [], source_files_touched: 0 };
221
+ if (state.finalStatus !== "canceled") {
200
222
  try {
201
- cleanupReport = runPostTaskCleanup(repoPath, taskDir);
202
- updateStatus(taskDir, { cleanup: cleanupReport });
223
+ cleanupReport = runPostTaskCleanup(ctx.repoPath, ctx.taskDir);
224
+ updateStatus(ctx.taskDir, { cleanup: cleanupReport });
203
225
  }
204
226
  catch (error) {
205
227
  cleanupReport = {
206
- enabled: true,
207
- removed: [],
228
+ enabled: true, removed: [],
208
229
  skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
209
230
  source_files_touched: 0,
210
231
  };
211
- writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
212
- updateStatus(taskDir, { cleanup: cleanupReport });
232
+ writeFileSync(join(ctx.taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
233
+ updateStatus(ctx.taskDir, { cleanup: cleanupReport });
213
234
  }
214
235
  }
215
- setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
236
+ setTaskPhase(ctx.taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
216
237
  const artifactCollectionStartedAt = new Date().toISOString();
217
238
  let changes;
218
239
  let artifactStatus = "collected";
219
240
  let artifactCollectionError = null;
220
241
  let timeoutHandle;
221
242
  try {
222
- // Phase 5: Wrap artifact collection with a timeout
223
243
  const collectionResult = await Promise.race([
224
- Promise.resolve().then(() => {
225
- const afterSnapshot = captureRepoSnapshot(repoPath);
226
- writeSnapshot(taskDir, "git-after.json", afterSnapshot);
227
- return buildChangeArtifacts(repoPath, beforeSnapshot, afterSnapshot);
244
+ Promise.resolve().then(async () => {
245
+ const afterSnapshot = await captureRepoSnapshot(ctx.repoPath);
246
+ writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
247
+ return await buildChangeArtifacts(ctx.repoPath, ctx.beforeSnapshot, afterSnapshot);
228
248
  }),
229
249
  new Promise((_, reject) => {
230
250
  timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
@@ -237,14 +257,9 @@ export async function runTask(taskId) {
237
257
  catch (error) {
238
258
  if (timeoutHandle)
239
259
  clearTimeout(timeoutHandle);
240
- lastCaughtError = error;
260
+ state.lastCaughtError = error;
241
261
  artifactCollectionError = errorMessage(error);
242
- if (artifactCollectionError.includes("timed out")) {
243
- artifactStatus = "timeout";
244
- }
245
- else {
246
- artifactStatus = "failed";
247
- }
262
+ artifactStatus = artifactCollectionError.includes("timed out") ? "timeout" : "failed";
248
263
  changes = {
249
264
  changed_files: [],
250
265
  diff: `(change capture failed: ${artifactCollectionError})\n`,
@@ -254,17 +269,16 @@ export async function runTask(taskId) {
254
269
  additions: 0,
255
270
  deletions: 0,
256
271
  file_stats: [],
257
- workspace_dirty_before: beforeSnapshot.workspace_dirty,
258
- workspace_dirty_after: beforeSnapshot.workspace_dirty,
272
+ workspace_dirty_before: ctx.beforeSnapshot.workspace_dirty,
273
+ workspace_dirty_after: ctx.beforeSnapshot.workspace_dirty,
259
274
  patch_mode: "hash_only",
260
275
  unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
261
276
  artifact_hygiene: emptyArtifactHygiene(),
262
277
  };
263
- // Phase 5: Write partial_result.md when artifact collection fails
264
- writeFileSync(join(taskDir, "partial_result.md"), [
278
+ writeFileSync(join(ctx.taskDir, "partial_result.md"), [
265
279
  "# PatchWarden Partial Result",
266
280
  "",
267
- `Task: ${taskId}`,
281
+ `Task: ${ctx.taskId}`,
268
282
  "",
269
283
  "## Artifact Collection Status",
270
284
  artifactStatus,
@@ -273,62 +287,79 @@ export async function runTask(taskId) {
273
287
  artifactCollectionError,
274
288
  "",
275
289
  "## Agent Status",
276
- agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
290
+ state.agentResult ? `Exit code: ${state.agentResult.exitCode}` : "Agent did not run",
277
291
  "",
278
292
  "## Verification Status",
279
- verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
293
+ state.verifyResults.length > 0 ? state.verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
280
294
  "",
281
295
  "## Note",
282
296
  "Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
283
297
  "",
284
298
  ].join("\n"), "utf-8");
285
- // Don't override test failure with artifact failure
286
- if (finalStatus === "done_by_agent") {
287
- finalError = `Change capture failed: ${artifactCollectionError}`;
288
- finalStatus = "failed";
299
+ if (state.finalStatus === "done_by_agent") {
300
+ state.finalError = `Change capture failed: ${artifactCollectionError}`;
301
+ state.finalStatus = "failed";
289
302
  }
290
303
  else {
291
- finalError ||= `Change capture failed: ${artifactCollectionError}`;
304
+ state.finalError ||= `Change capture failed: ${artifactCollectionError}`;
292
305
  }
293
306
  }
294
307
  const artifactCollectionFinishedAt = new Date().toISOString();
295
308
  let outOfScopeChanges = [];
296
- let preexistingExternalDirty = externalDirtyBaseline;
309
+ const preexistingExternalDirty = ctx.externalDirtyBaseline;
297
310
  let newOutOfScopeChanges = [];
298
311
  try {
299
- const afterWorkspaceSnapshot = repoPath === wsRoot ? captureRepoSnapshot(repoPath) : captureRepoSnapshot(wsRoot);
300
- writeSnapshot(taskDir, "workspace-after.json", afterWorkspaceSnapshot);
301
- const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, repoPath, wsRoot);
302
- // Phase 4: Only NEW external dirty files (not in baseline) are scope violations
303
- newOutOfScopeChanges = findNewExternalDirtyFiles(externalDirtyBaseline, allExternalDirty);
304
- outOfScopeChanges = compareSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
305
- .filter((file) => !isPathInside(resolve(wsRoot, file.path), repoPath) ||
306
- Boolean(file.old_path && !isPathInside(resolve(wsRoot, file.old_path), repoPath)));
312
+ const afterWorkspaceSnapshot = ctx.repoPath === ctx.wsRoot ? await captureRepoSnapshot(ctx.repoPath) : await captureRepoSnapshot(ctx.wsRoot);
313
+ writeSnapshot(ctx.taskDir, "workspace-after.json", afterWorkspaceSnapshot);
314
+ const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, ctx.repoPath, ctx.wsRoot);
315
+ newOutOfScopeChanges = findNewExternalDirtyFiles(ctx.externalDirtyBaseline, allExternalDirty);
316
+ outOfScopeChanges = compareSnapshots(ctx.beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
317
+ .filter((file) => !isPathInside(resolve(ctx.wsRoot, file.path), ctx.repoPath) ||
318
+ Boolean(file.old_path && !isPathInside(resolve(ctx.wsRoot, file.old_path), ctx.repoPath)));
307
319
  }
308
320
  catch (error) {
309
- finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
310
- if (finalStatus === "done_by_agent")
311
- finalStatus = "failed";
321
+ state.finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
322
+ if (state.finalStatus === "done_by_agent")
323
+ state.finalStatus = "failed";
312
324
  }
313
- // Phase 4: Pre-existing external dirty files are warnings, not failures
314
325
  const preexistingWarnings = preexistingExternalDirty.length > 0
315
326
  ? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
316
327
  : [];
328
+ applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty);
329
+ writeFileSync(join(ctx.taskDir, "git.diff"), changes.diff, "utf-8");
330
+ writeFileSync(join(ctx.taskDir, "diff.patch"), changes.diff, "utf-8");
331
+ writeFileSync(join(ctx.taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
332
+ const artifactManifest = await buildArtifactManifest(changes.changed_files, ctx.repoPath, ctx.taskId);
333
+ writeFileSync(join(ctx.taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
334
+ const changedFileGroups = groupChangedFiles(changes.changed_files);
335
+ writeFileSync(join(ctx.taskDir, "file-stats.json"), JSON.stringify({
336
+ task_id: ctx.taskId,
337
+ additions: changes.additions,
338
+ deletions: changes.deletions,
339
+ files: changes.file_stats,
340
+ }, null, 2), "utf-8");
341
+ return {
342
+ changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
343
+ outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
344
+ artifactManifest, changedFileGroups,
345
+ };
346
+ }
347
+ function applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty) {
317
348
  if (newOutOfScopeChanges.length > 0) {
318
- finalStatus = "failed_scope_violation";
319
- finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
320
- writeFileSync(join(taskDir, "rollback-plan.json"), JSON.stringify({
321
- task_id: taskId,
349
+ state.finalStatus = "failed_scope_violation";
350
+ state.finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
351
+ writeFileSync(join(ctx.taskDir, "rollback-plan.json"), JSON.stringify({
352
+ task_id: ctx.taskId,
322
353
  status: "review_required",
323
354
  automatic_rollback_performed: false,
324
355
  warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
325
356
  out_of_scope_changes: newOutOfScopeChanges,
326
357
  preexisting_external_dirty_files: preexistingExternalDirty,
327
358
  }, null, 2), "utf-8");
328
- writeFileSync(join(taskDir, "rollback_scope_violation_plan.md"), [
359
+ writeFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
329
360
  "# Scope Violation Rollback Plan",
330
361
  "",
331
- `Task: ${taskId}`,
362
+ `Task: ${ctx.taskId}`,
332
363
  "",
333
364
  "PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
334
365
  "",
@@ -337,44 +368,30 @@ export async function runTask(taskId) {
337
368
  "",
338
369
  ].join("\n"), "utf-8");
339
370
  }
340
- else if (changePolicy === "no_changes" && changes.changed_files.length > 0) {
341
- finalStatus = "failed_policy_violation";
342
- finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
371
+ else if (ctx.changePolicy === "no_changes" && changes.changed_files.length > 0) {
372
+ state.finalStatus = "failed_policy_violation";
373
+ state.finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
343
374
  }
344
- writeFileSync(join(taskDir, "git.diff"), changes.diff, "utf-8");
345
- writeFileSync(join(taskDir, "diff.patch"), changes.diff, "utf-8");
346
- writeFileSync(join(taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
347
- // Phase 6: Generate artifact_manifest.json and group changed files
348
- const artifactManifest = buildArtifactManifest(changes.changed_files, repoPath, taskId);
349
- writeFileSync(join(taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
350
- const changedFileGroups = groupChangedFiles(changes.changed_files);
351
- writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
352
- task_id: taskId,
353
- additions: changes.additions,
354
- deletions: changes.deletions,
355
- files: changes.file_stats,
356
- }, null, 2), "utf-8");
357
- writeFileSync(join(taskDir, "test.log"), buildTestLog(testResult), "utf-8");
358
- const verifyJson = buildVerifyJson(verifyCommands, verifyResults, repoPath);
359
- // Phase 4: Use newOutOfScopeChanges (not outOfScopeChanges) for verify_status
360
- // to stay consistent with finalStatus. Pre-existing external dirty files
361
- // that didn't change during the task should NOT cause verify_status to fail.
375
+ }
376
+ function finalizeTask(ctx, state, evidence) {
377
+ const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport, artifactManifest, changedFileGroups } = evidence;
378
+ writeFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult), "utf-8");
379
+ const verifyJson = buildVerifyJson(ctx.verifyCommands, state.verifyResults, ctx.repoPath);
362
380
  if (newOutOfScopeChanges.length > 0) {
363
381
  verifyJson.status = "failed";
364
382
  verifyJson.failure_reason = "scope_violation";
365
383
  }
366
- else if (finalStatus === "failed_policy_violation") {
384
+ else if (state.finalStatus === "failed_policy_violation") {
367
385
  verifyJson.status = "failed";
368
386
  verifyJson.failure_reason = "change_policy_violation";
369
387
  }
370
- writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
371
- writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
372
- if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus))
373
- finalStatus = "failed";
374
- const finalPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
375
- const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
376
- // Phase 7: Run Android build environment diagnostics if android_app exists
377
- const androidDiagnostic = diagnoseAndroidBuild(repoPath);
388
+ writeFileSync(join(ctx.taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
389
+ writeFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
390
+ if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus))
391
+ state.finalStatus = "failed";
392
+ const finalPhase = state.finalStatus === "done_by_agent" ? "done_by_agent" : state.finalStatus;
393
+ const followup = buildFailureFollowup(state.finalStatus, state.finalError, verifyJson.commands);
394
+ const androidDiagnostic = diagnoseAndroidBuild(ctx.repoPath);
378
395
  let androidWarning = null;
379
396
  if (androidDiagnostic.status !== "skip") {
380
397
  if (androidDiagnostic.status === "fail") {
@@ -388,13 +405,13 @@ export async function runTask(taskId) {
388
405
  }
389
406
  }
390
407
  const resultMd = buildResultMarkdown({
391
- taskId,
392
- planId,
393
- agent: agentName,
394
- status: finalStatus,
395
- error: finalError,
396
- agentResult,
397
- testResult,
408
+ taskId: ctx.taskId,
409
+ planId: ctx.planId,
410
+ agent: ctx.agentName,
411
+ status: state.finalStatus,
412
+ error: state.finalError,
413
+ agentResult: state.agentResult,
414
+ testResult: state.testResult,
398
415
  verify: verifyJson,
399
416
  changes,
400
417
  outOfScopeChanges,
@@ -405,18 +422,72 @@ export async function runTask(taskId) {
405
422
  androidWarning,
406
423
  preexistingWarnings,
407
424
  });
408
- writeFileSync(join(taskDir, "result.md"), resultMd, "utf-8");
409
- writeFileSync(join(taskDir, "result.json"), JSON.stringify({
410
- task_id: taskId,
411
- status: finalStatus,
412
- agent: agentName,
413
- workspace_root: wsRoot,
414
- repo_path: initialStatus.repo_path,
415
- resolved_repo_path: repoPath,
416
- plan_source: initialStatus.plan_source || "saved",
417
- template: initialStatus.template || null,
418
- change_policy: changePolicy,
419
- summary: finalError || "Agent execution and configured verification completed successfully.",
425
+ writeFileSync(join(ctx.taskDir, "result.md"), resultMd, "utf-8");
426
+ const resultJson = buildResultJson({
427
+ ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning,
428
+ });
429
+ writeFileSync(join(ctx.taskDir, "result.json"), JSON.stringify(resultJson, null, 2), "utf-8");
430
+ if (state.finalError) {
431
+ const structuredError = errorPayload(state.lastCaughtError);
432
+ writeFileSync(join(ctx.taskDir, "error.log"), JSON.stringify({
433
+ summary: state.finalError,
434
+ ...structuredError,
435
+ }, null, 2), "utf-8");
436
+ }
437
+ const finishedAt = new Date().toISOString();
438
+ updateStatus(ctx.taskDir, {
439
+ status: state.finalStatus,
440
+ phase: finalPhase,
441
+ current_command: null,
442
+ last_heartbeat_at: finishedAt,
443
+ finished_at: finishedAt,
444
+ error: state.finalError,
445
+ changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
446
+ artifact_hygiene_counts: changes.artifact_hygiene.counts,
447
+ artifact_status: artifactStatus,
448
+ artifact_collection_error: artifactCollectionError,
449
+ artifact_collection_started_at: artifactCollectionStartedAt,
450
+ artifact_collection_finished_at: artifactCollectionFinishedAt,
451
+ cleanup: cleanupReport,
452
+ out_of_scope_changes: outOfScopeChanges,
453
+ new_out_of_scope_changes: newOutOfScopeChanges,
454
+ preexisting_external_dirty_files: preexistingExternalDirty,
455
+ verify_status: verifyJson.status,
456
+ verify_commands: ctx.verifyCommands,
457
+ diff_available: changes.diff_available,
458
+ diff_truncated: changes.diff_truncated,
459
+ workspace_dirty_before: changes.workspace_dirty_before,
460
+ workspace_dirty_after: changes.workspace_dirty_after,
461
+ workspace_dirty: changes.workspace_dirty_after,
462
+ acceptance_status: state.finalStatus === "done_by_agent" ? "pending" : null,
463
+ });
464
+ writeTaskRuntime(ctx.taskDir, {
465
+ phase: finalPhase,
466
+ current_command: null,
467
+ last_heartbeat_at: finishedAt,
468
+ runner_pid: process.pid,
469
+ child_pid: undefined,
470
+ });
471
+ writeTaskProgress(ctx.taskDir, finalPhase, {
472
+ heartbeatAt: finishedAt,
473
+ note: state.finalError || `Task finished with status ${state.finalStatus}.`,
474
+ });
475
+ return { task_id: ctx.taskId, status: state.finalStatus, error: state.finalError };
476
+ }
477
+ function buildResultJson(input) {
478
+ const { ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning } = input;
479
+ const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, cleanupReport, artifactManifest, changedFileGroups } = evidence;
480
+ return {
481
+ task_id: ctx.taskId,
482
+ status: state.finalStatus,
483
+ agent: ctx.agentName,
484
+ workspace_root: ctx.wsRoot,
485
+ repo_path: ctx.initialStatus.repo_path,
486
+ resolved_repo_path: ctx.repoPath,
487
+ plan_source: ctx.initialStatus.plan_source || "saved",
488
+ template: ctx.initialStatus.template || null,
489
+ change_policy: ctx.changePolicy,
490
+ summary: state.finalError || "Agent execution and configured verification completed successfully.",
420
491
  changed_files: changes.changed_files,
421
492
  changed_file_groups: {
422
493
  source_changes: changedFileGroups.source_changes.length,
@@ -454,68 +525,21 @@ export async function runTask(taskId) {
454
525
  ...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
455
526
  ],
456
527
  warnings: [
457
- ...beforeSnapshot.warnings,
528
+ ...ctx.beforeSnapshot.warnings,
458
529
  ...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
459
- ...preexistingWarnings,
530
+ ...evidence.preexistingWarnings,
460
531
  ...(androidWarning ? [androidWarning] : []),
461
532
  ],
462
- errors: finalError ? [finalError] : [],
463
- known_issues: finalError ? [finalError] : [],
533
+ errors: state.finalError ? [state.finalError] : [],
534
+ known_issues: state.finalError ? [state.finalError] : [],
464
535
  failure_reason: followup.failure_reason,
465
536
  failed_command: followup.failed_command,
466
537
  suggested_next_action: followup.suggested_next_action,
467
538
  safe_followup_prompt: followup.safe_followup_prompt,
468
- next_steps: finalStatus === "done_by_agent"
539
+ next_steps: state.finalStatus === "done_by_agent"
469
540
  ? ["Review get_task_summary and audit_task before accepting the work."]
470
541
  : ["Resolve the reported failure before accepting the work."],
471
- }, null, 2), "utf-8");
472
- if (finalError) {
473
- // Phase 9: Preserve PatchWardenError structured info in error.log
474
- const structuredError = errorPayload(lastCaughtError);
475
- writeFileSync(join(taskDir, "error.log"), JSON.stringify({
476
- summary: finalError,
477
- ...structuredError,
478
- }, null, 2), "utf-8");
479
- }
480
- const finishedAt = new Date().toISOString();
481
- updateStatus(taskDir, {
482
- status: finalStatus,
483
- phase: finalPhase,
484
- current_command: null,
485
- last_heartbeat_at: finishedAt,
486
- finished_at: finishedAt,
487
- error: finalError,
488
- changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
489
- artifact_hygiene_counts: changes.artifact_hygiene.counts,
490
- artifact_status: artifactStatus,
491
- artifact_collection_error: artifactCollectionError,
492
- artifact_collection_started_at: artifactCollectionStartedAt,
493
- artifact_collection_finished_at: artifactCollectionFinishedAt,
494
- cleanup: cleanupReport,
495
- out_of_scope_changes: outOfScopeChanges,
496
- new_out_of_scope_changes: newOutOfScopeChanges,
497
- preexisting_external_dirty_files: preexistingExternalDirty,
498
- verify_status: verifyJson.status,
499
- verify_commands: verifyCommands,
500
- diff_available: changes.diff_available,
501
- diff_truncated: changes.diff_truncated,
502
- workspace_dirty_before: changes.workspace_dirty_before,
503
- workspace_dirty_after: changes.workspace_dirty_after,
504
- workspace_dirty: changes.workspace_dirty_after,
505
- acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
506
- });
507
- writeTaskRuntime(taskDir, {
508
- phase: finalPhase,
509
- current_command: null,
510
- last_heartbeat_at: finishedAt,
511
- runner_pid: process.pid,
512
- child_pid: undefined,
513
- });
514
- writeTaskProgress(taskDir, finalPhase, {
515
- heartbeatAt: finishedAt,
516
- note: finalError || `Task finished with status ${finalStatus}.`,
517
- });
518
- return { task_id: taskId, status: finalStatus, error: finalError };
542
+ };
519
543
  }
520
544
  function buildFailureFollowup(status, error, commands) {
521
545
  const failed = commands.find((command) => command.status !== "passed");
@@ -643,11 +667,11 @@ async function runManagedProcess(options) {
643
667
  try {
644
668
  forceKill(child);
645
669
  }
646
- catch { }
670
+ catch { } // cleanup failure is safe to ignore
647
671
  try {
648
672
  child.kill("SIGKILL");
649
673
  }
650
- catch { }
674
+ catch { } // cleanup failure is safe to ignore
651
675
  // Resolve the exit promise via the shared finish handle
652
676
  if (childExitFinish)
653
677
  childExitFinish(null);
@@ -1027,7 +1051,7 @@ function gracefulKill(child) {
1027
1051
  else
1028
1052
  child.kill("SIGTERM");
1029
1053
  }
1030
- catch { }
1054
+ catch { } // cleanup failure is safe to ignore
1031
1055
  }
1032
1056
  function forceKill(child) {
1033
1057
  if (!child.pid)
@@ -1050,7 +1074,7 @@ function forceKill(child) {
1050
1074
  try {
1051
1075
  child.kill("SIGKILL");
1052
1076
  }
1053
- catch { }
1077
+ catch { } // cleanup failure is safe to ignore
1054
1078
  }
1055
1079
  }
1056
1080
  function openStream(path) {