patchwarden 1.5.0 → 1.6.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 (597) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +194 -26
  3. package/README.md +160 -22
  4. package/dist/assessments/agentAssessor.d.ts +1 -1
  5. package/dist/assessments/agentAssessor.js +24 -15
  6. package/dist/assessments/assessmentStore.d.ts +1 -1
  7. package/dist/assessments/assessmentStore.js +44 -36
  8. package/dist/assessments/confirmCli.js +5 -4
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +98 -11
  11. package/dist/control/fileManager.d.ts +2 -0
  12. package/dist/control/fileManager.js +39 -0
  13. package/dist/control/middleware/auth.d.ts +16 -0
  14. package/dist/control/middleware/auth.js +17 -0
  15. package/dist/control/middleware/static.d.ts +3 -0
  16. package/dist/control/middleware/static.js +78 -0
  17. package/dist/control/routeTable.d.ts +24 -0
  18. package/dist/control/routeTable.js +263 -0
  19. package/dist/control/routes/audit.d.ts +15 -0
  20. package/dist/control/routes/audit.js +287 -0
  21. package/dist/control/routes/evidence.d.ts +4 -0
  22. package/dist/control/routes/evidence.js +96 -0
  23. package/dist/control/routes/lineage.d.ts +3 -0
  24. package/dist/control/routes/lineage.js +71 -0
  25. package/dist/control/routes/policy.d.ts +3 -0
  26. package/dist/control/routes/policy.js +81 -0
  27. package/dist/control/routes/process.d.ts +12 -0
  28. package/dist/control/routes/process.js +487 -0
  29. package/dist/control/routes/sessions.d.ts +30 -0
  30. package/dist/control/routes/sessions.js +287 -0
  31. package/dist/control/routes/status.d.ts +51 -0
  32. package/dist/control/routes/status.js +307 -0
  33. package/dist/control/routes/taskActions.d.ts +21 -0
  34. package/dist/control/routes/taskActions.js +197 -0
  35. package/dist/control/routes/tasks.d.ts +24 -0
  36. package/dist/control/routes/tasks.js +309 -0
  37. package/dist/control/routes/workspace.d.ts +15 -0
  38. package/dist/control/routes/workspace.js +220 -0
  39. package/dist/control/runtime.d.ts +92 -0
  40. package/dist/control/runtime.js +425 -0
  41. package/dist/control/server.d.ts +13 -0
  42. package/dist/control/server.js +150 -0
  43. package/dist/control/shared.d.ts +59 -0
  44. package/dist/control/shared.js +341 -0
  45. package/dist/controlCenter.d.ts +6 -0
  46. package/dist/controlCenter.js +7 -2197
  47. package/dist/direct/directAudit.js +234 -225
  48. package/dist/direct/directGuards.d.ts +2 -1
  49. package/dist/direct/directGuards.js +72 -37
  50. package/dist/direct/directPatch.d.ts +6 -1
  51. package/dist/direct/directPatch.js +55 -8
  52. package/dist/direct/directSessionStore.d.ts +16 -6
  53. package/dist/direct/directSessionStore.js +127 -35
  54. package/dist/direct/directVerification.js +8 -16
  55. package/dist/doctor.d.ts +18 -1
  56. package/dist/doctor.js +614 -373
  57. package/dist/goal/acceptanceEngine.d.ts +1 -1
  58. package/dist/goal/goalProgress.js +75 -69
  59. package/dist/goal/goalReport.d.ts +54 -0
  60. package/dist/goal/goalReport.js +197 -0
  61. package/dist/goal/goalStatus.d.ts +8 -0
  62. package/dist/goal/goalStatus.js +10 -3
  63. package/dist/goal/goalStore.d.ts +9 -2
  64. package/dist/goal/goalStore.js +152 -45
  65. package/dist/goal/handoffExport.js +6 -6
  66. package/dist/goal/specKitImport.d.ts +63 -0
  67. package/dist/goal/specKitImport.js +221 -0
  68. package/dist/goal/subgoalSync.js +13 -10
  69. package/dist/goal/worktreeManager.d.ts +2 -0
  70. package/dist/goal/worktreeManager.js +209 -136
  71. package/dist/httpServer.js +126 -44
  72. package/dist/index.js +7 -4
  73. package/dist/logging.d.ts +7 -1
  74. package/dist/logging.js +13 -3
  75. package/dist/policy/projectPolicy.js +5 -2
  76. package/dist/release/releaseGate.d.ts +1 -1
  77. package/dist/release/releaseGate.js +78 -25
  78. package/dist/runner/agentInvocation.d.ts +14 -0
  79. package/dist/runner/agentInvocation.js +96 -4
  80. package/dist/runner/changeCapture.d.ts +14 -6
  81. package/dist/runner/changeCapture.js +287 -81
  82. package/dist/runner/cli.js +7 -6
  83. package/dist/runner/postTaskCleanup.js +60 -7
  84. package/dist/runner/processSecurity.d.ts +56 -0
  85. package/dist/runner/processSecurity.js +373 -0
  86. package/dist/runner/runTask.d.ts +1 -1
  87. package/dist/runner/runTask.js +339 -287
  88. package/dist/runner/simpleProcess.d.ts +3 -0
  89. package/dist/runner/simpleProcess.js +52 -38
  90. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  91. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  92. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  93. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  94. package/dist/runner/taskStatusStore.d.ts +11 -0
  95. package/dist/runner/taskStatusStore.js +24 -0
  96. package/dist/runner/watch.d.ts +14 -1
  97. package/dist/runner/watch.js +265 -54
  98. package/dist/security/contentRedaction.d.ts +6 -0
  99. package/dist/security/contentRedaction.js +24 -2
  100. package/dist/security/discoveryTokenStore.d.ts +3 -2
  101. package/dist/security/discoveryTokenStore.js +17 -2
  102. package/dist/security/loopbackHost.d.ts +2 -0
  103. package/dist/security/loopbackHost.js +16 -0
  104. package/dist/security/pathGuard.js +32 -17
  105. package/dist/security/planGuard.js +2 -1
  106. package/dist/security/runtimeGuard.js +9 -6
  107. package/dist/security/secretComparison.d.ts +3 -0
  108. package/dist/security/secretComparison.js +11 -0
  109. package/dist/security/sensitiveGuard.d.ts +5 -0
  110. package/dist/security/sensitiveGuard.js +27 -8
  111. package/dist/security/toolInvocationGuard.d.ts +2 -2
  112. package/dist/security/toolInvocationGuard.js +1 -1
  113. package/dist/security/workspaceRootGuard.d.ts +8 -0
  114. package/dist/security/workspaceRootGuard.js +47 -0
  115. package/dist/smoke-test.js +280 -273
  116. package/dist/tools/catalog/index.d.ts +4 -0
  117. package/dist/tools/catalog/index.js +4 -0
  118. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  119. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  120. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
  121. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  122. package/dist/tools/definitions/toolDefs.js +1177 -0
  123. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  124. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  125. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
  126. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
  127. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  128. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
  129. package/dist/tools/diagnostics/index.d.ts +6 -0
  130. package/dist/tools/diagnostics/index.js +6 -0
  131. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  132. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  133. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
  134. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
  135. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  136. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  137. package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
  138. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
  139. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
  140. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  141. package/dist/tools/direct/index.d.ts +3 -0
  142. package/dist/tools/direct/index.js +3 -0
  143. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  144. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  145. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  146. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  147. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  148. package/dist/tools/discovery/index.d.ts +3 -0
  149. package/dist/tools/discovery/index.js +3 -0
  150. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  151. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  152. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  153. package/dist/tools/dispatch/coreDispatch.js +283 -0
  154. package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
  155. package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
  156. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  157. package/dist/tools/dispatch/directDispatch.js +116 -0
  158. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  159. package/dist/tools/dispatch/goalDispatch.js +92 -0
  160. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  161. package/dist/tools/dispatch/releaseDispatch.js +46 -0
  162. package/dist/tools/dispatch/types.d.ts +23 -0
  163. package/dist/tools/dispatch/types.js +15 -0
  164. package/dist/tools/dispatch/validation.d.ts +7 -0
  165. package/dist/tools/dispatch/validation.js +71 -0
  166. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  167. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
  168. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  169. package/dist/tools/goals/index.d.ts +3 -0
  170. package/dist/tools/goals/index.js +3 -0
  171. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  172. package/dist/tools/registry.d.ts +10 -14
  173. package/dist/tools/registry.js +47 -1651
  174. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  175. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  176. package/dist/tools/release/index.d.ts +2 -0
  177. package/dist/tools/release/index.js +2 -0
  178. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  179. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  180. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  181. package/dist/tools/tasks/cancelTask.js +85 -0
  182. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  183. package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
  184. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  185. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  186. package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
  187. package/dist/tools/tasks/evidencePack.js +387 -0
  188. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  189. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  190. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  191. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  192. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  193. package/dist/tools/tasks/index.d.ts +18 -0
  194. package/dist/tools/tasks/index.js +18 -0
  195. package/dist/tools/tasks/killTask.d.ts +1 -0
  196. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  197. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  198. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  199. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  200. package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
  201. package/dist/tools/tasks/retryTask.js +60 -0
  202. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  203. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
  204. package/dist/tools/tasks/runVerification.js +38 -0
  205. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  206. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  207. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  208. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  209. package/dist/tools/workspace/applyPatch.js +53 -0
  210. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  211. package/dist/tools/workspace/index.d.ts +10 -0
  212. package/dist/tools/workspace/index.js +10 -0
  213. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  214. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  215. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  216. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  217. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  218. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  219. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  220. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  221. package/dist/tools/workspace/syncFile.js +134 -0
  222. package/dist/utils/atomicFile.d.ts +7 -0
  223. package/dist/utils/atomicFile.js +48 -0
  224. package/dist/utils/boundedFile.d.ts +8 -0
  225. package/dist/utils/boundedFile.js +93 -0
  226. package/dist/utils/lockedJsonFile.d.ts +15 -0
  227. package/dist/utils/lockedJsonFile.js +224 -0
  228. package/dist/utils/platform.d.ts +11 -0
  229. package/dist/utils/platform.js +26 -0
  230. package/dist/utils/stableJson.d.ts +2 -0
  231. package/dist/utils/stableJson.js +12 -0
  232. package/dist/version.d.ts +3 -2
  233. package/dist/version.js +3 -2
  234. package/dist/watcherStatus.d.ts +2 -2
  235. package/dist/watcherStatus.js +18 -0
  236. package/docs/CODE_WIKI.md +1648 -0
  237. package/docs/agentseal-integration.md +150 -0
  238. package/docs/architecture.md +23 -0
  239. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  240. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  241. package/docs/control-center/README.md +4 -4
  242. package/docs/dashboard-overview.md +86 -0
  243. package/docs/demo.md +8 -0
  244. package/docs/desktop-app.md +175 -0
  245. package/docs/direct-session-workflow.md +98 -0
  246. package/docs/evidence-pack-schema.md +215 -0
  247. package/docs/lineage-evidence-pack-workflow.md +127 -0
  248. package/docs/mcp-inspector-testing.md +200 -0
  249. package/docs/open-source-application.md +168 -0
  250. package/docs/opencode-worker.md +151 -0
  251. package/docs/openhands-worker.md +181 -0
  252. package/docs/release-checklist.md +1 -1
  253. package/docs/release-evidence.md +82 -0
  254. package/docs/spec-kit-integration.md +131 -0
  255. package/docs/task-safe-review-workflow.md +98 -0
  256. package/docs/threat-model.md +97 -0
  257. package/docs/user-feedback.md +40 -0
  258. package/docs/why-patchwarden.md +110 -0
  259. package/examples/config.example.json +4 -2
  260. package/package.json +28 -11
  261. package/scripts/README.md +15 -6
  262. package/scripts/build.js +11 -0
  263. package/scripts/checks/brand-check.js +3 -0
  264. package/scripts/checks/build-output-check.js +37 -0
  265. package/scripts/checks/control-center-smoke.js +447 -6
  266. package/scripts/checks/control-smoke.js +19 -8
  267. package/scripts/checks/http-mcp-smoke.js +82 -4
  268. package/scripts/checks/lifecycle-smoke.js +39 -33
  269. package/scripts/checks/mcp-manifest-check.js +1 -1
  270. package/scripts/checks/mcp-smoke.js +30 -4
  271. package/scripts/checks/package-manifest-check.js +20 -7
  272. package/scripts/checks/unit-tests.js +3 -5
  273. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  274. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  275. package/scripts/control/manage-patchwarden.ps1 +69 -4
  276. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  277. package/scripts/control/run-background-supervisor.ps1 +41 -0
  278. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  279. package/scripts/generate-demo-gif.py +320 -0
  280. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  281. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  282. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  283. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  284. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  285. package/scripts/lib/clean-generated-output.js +11 -0
  286. package/scripts/lib/file-discovery.js +12 -0
  287. package/scripts/release/desktop-preflight.js +211 -0
  288. package/scripts/release/pack-clean.js +59 -14
  289. package/src/assessments/agentAssessor.ts +29 -15
  290. package/src/assessments/assessmentStore.ts +55 -43
  291. package/src/assessments/confirmCli.ts +5 -4
  292. package/src/config.ts +105 -11
  293. package/src/control/fileManager.ts +40 -0
  294. package/src/control/middleware/auth.ts +35 -0
  295. package/src/control/middleware/static.ts +84 -0
  296. package/src/control/routeTable.ts +301 -0
  297. package/src/control/routes/audit.ts +329 -0
  298. package/src/control/routes/evidence.ts +107 -0
  299. package/src/control/routes/lineage.ts +92 -0
  300. package/src/control/routes/policy.ts +81 -0
  301. package/src/control/routes/process.ts +505 -0
  302. package/src/control/routes/sessions.ts +314 -0
  303. package/src/control/routes/status.ts +405 -0
  304. package/src/control/routes/taskActions.ts +214 -0
  305. package/src/control/routes/tasks.ts +331 -0
  306. package/src/control/routes/workspace.ts +234 -0
  307. package/src/control/runtime.ts +508 -0
  308. package/src/control/server.ts +165 -0
  309. package/src/control/shared.ts +360 -0
  310. package/src/controlCenter.ts +7 -2347
  311. package/src/direct/directAudit.ts +17 -9
  312. package/src/direct/directGuards.ts +100 -51
  313. package/src/direct/directPatch.ts +95 -8
  314. package/src/direct/directSessionStore.ts +211 -55
  315. package/src/direct/directVerification.ts +9 -23
  316. package/src/doctor.ts +766 -492
  317. package/src/goal/acceptanceEngine.ts +1 -1
  318. package/src/goal/goalProgress.ts +17 -13
  319. package/src/goal/goalReport.ts +263 -0
  320. package/src/goal/goalStatus.ts +25 -3
  321. package/src/goal/goalStore.ts +241 -54
  322. package/src/goal/handoffExport.ts +6 -6
  323. package/src/goal/specKitImport.ts +355 -0
  324. package/src/goal/subgoalSync.ts +15 -10
  325. package/src/goal/worktreeManager.ts +139 -22
  326. package/src/httpServer.ts +130 -44
  327. package/src/index.ts +7 -4
  328. package/src/logging.ts +20 -4
  329. package/src/policy/projectPolicy.ts +5 -2
  330. package/src/release/releaseGate.ts +74 -27
  331. package/src/runner/agentInvocation.ts +126 -4
  332. package/src/runner/changeCapture.ts +342 -80
  333. package/src/runner/cli.ts +7 -6
  334. package/src/runner/postTaskCleanup.ts +55 -7
  335. package/src/runner/processSecurity.ts +433 -0
  336. package/src/runner/runTask.ts +441 -292
  337. package/src/runner/simpleProcess.ts +68 -33
  338. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  339. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  340. package/src/runner/taskStatusStore.ts +54 -0
  341. package/src/runner/watch.ts +290 -57
  342. package/src/security/contentRedaction.ts +31 -2
  343. package/src/security/discoveryTokenStore.ts +18 -4
  344. package/src/security/loopbackHost.ts +19 -0
  345. package/src/security/pathGuard.ts +35 -16
  346. package/src/security/planGuard.ts +2 -1
  347. package/src/security/runtimeGuard.ts +9 -6
  348. package/src/security/secretComparison.ts +13 -0
  349. package/src/security/sensitiveGuard.ts +27 -8
  350. package/src/security/toolInvocationGuard.ts +2 -2
  351. package/src/security/workspaceRootGuard.ts +53 -0
  352. package/src/smoke-test.ts +276 -273
  353. package/src/tools/catalog/index.ts +4 -0
  354. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  355. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
  356. package/src/tools/definitions/toolDefs.ts +1264 -0
  357. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  358. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
  359. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
  360. package/src/tools/diagnostics/index.ts +6 -0
  361. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  362. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
  363. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  364. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
  365. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
  366. package/src/tools/direct/index.ts +3 -0
  367. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  368. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  369. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  370. package/src/tools/discovery/index.ts +3 -0
  371. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  372. package/src/tools/dispatch/coreDispatch.ts +379 -0
  373. package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
  374. package/src/tools/dispatch/directDispatch.ts +168 -0
  375. package/src/tools/dispatch/goalDispatch.ts +128 -0
  376. package/src/tools/dispatch/releaseDispatch.ts +66 -0
  377. package/src/tools/dispatch/types.ts +24 -0
  378. package/src/tools/dispatch/validation.ts +78 -0
  379. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  380. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
  381. package/src/tools/goals/index.ts +3 -0
  382. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  383. package/src/tools/registry.ts +54 -1858
  384. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  385. package/src/tools/release/index.ts +2 -0
  386. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  387. package/src/tools/tasks/cancelTask.ts +109 -0
  388. package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
  389. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  390. package/src/tools/tasks/evidencePack.ts +504 -0
  391. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  392. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  393. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  394. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  395. package/src/tools/tasks/index.ts +18 -0
  396. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  397. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  398. package/src/tools/tasks/retryTask.ts +69 -0
  399. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
  400. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  401. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  402. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  403. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  404. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  405. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  406. package/src/tools/workspace/index.ts +10 -0
  407. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  408. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  409. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  410. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  411. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  412. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  413. package/src/tools/workspace/syncFile.ts +292 -0
  414. package/src/utils/atomicFile.ts +58 -0
  415. package/src/utils/boundedFile.ts +116 -0
  416. package/src/utils/lockedJsonFile.ts +251 -0
  417. package/src/utils/platform.ts +31 -0
  418. package/src/utils/stableJson.ts +12 -0
  419. package/src/version.ts +3 -2
  420. package/src/watcherStatus.ts +19 -1
  421. package/ui/desktop-bootstrap.js +11 -0
  422. package/ui/desktop-bridge.js +36 -0
  423. package/ui/desktop.css +160 -0
  424. package/ui/getting-started.js +93 -0
  425. package/ui/i18n.js +484 -0
  426. package/ui/log-parser.js +30 -0
  427. package/ui/pages/audit.html +204 -3
  428. package/ui/pages/dashboard.html +937 -79
  429. package/ui/pages/direct-sessions.html +609 -60
  430. package/ui/pages/getting-started.html +43 -0
  431. package/ui/pages/logs.html +105 -22
  432. package/ui/pages/settings.html +101 -0
  433. package/ui/pages/task-detail.html +468 -438
  434. package/ui/pages/tasks.html +648 -65
  435. package/ui/pages/workspace.html +38 -1
  436. package/ui/settings.js +245 -0
  437. package/PatchWarden-Control.cmd +0 -6
  438. package/Restart-PatchWarden-Control.cmd +0 -6
  439. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  440. package/dist/test/unit/acceptance-engine.test.js +0 -228
  441. package/dist/test/unit/agent-router.test.d.ts +0 -1
  442. package/dist/test/unit/agent-router.test.js +0 -287
  443. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  444. package/dist/test/unit/android-doctor.test.js +0 -118
  445. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  446. package/dist/test/unit/audit-checks.test.js +0 -350
  447. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  448. package/dist/test/unit/chinese-path.test.js +0 -91
  449. package/dist/test/unit/command-guard.test.d.ts +0 -1
  450. package/dist/test/unit/command-guard.test.js +0 -160
  451. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  452. package/dist/test/unit/diagnose-task.test.js +0 -457
  453. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  454. package/dist/test/unit/direct-guards.test.js +0 -213
  455. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  456. package/dist/test/unit/discovery-token-store.test.js +0 -139
  457. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  458. package/dist/test/unit/evidence-pack.test.js +0 -130
  459. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  460. package/dist/test/unit/goal-graph.test.js +0 -298
  461. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  462. package/dist/test/unit/goal-progress.test.js +0 -381
  463. package/dist/test/unit/goal-status.test.d.ts +0 -1
  464. package/dist/test/unit/goal-status.test.js +0 -215
  465. package/dist/test/unit/goal-store.test.d.ts +0 -1
  466. package/dist/test/unit/goal-store.test.js +0 -253
  467. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  468. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  469. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  470. package/dist/test/unit/goal-tools-registry.test.js +0 -190
  471. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  472. package/dist/test/unit/handoff-export.test.js +0 -263
  473. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  474. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  475. package/dist/test/unit/logging.test.d.ts +0 -1
  476. package/dist/test/unit/logging.test.js +0 -397
  477. package/dist/test/unit/path-guard.test.d.ts +0 -1
  478. package/dist/test/unit/path-guard.test.js +0 -109
  479. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  480. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  481. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  482. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  483. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  484. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  485. package/dist/test/unit/release-gate.test.d.ts +0 -1
  486. package/dist/test/unit/release-gate.test.js +0 -242
  487. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  488. package/dist/test/unit/run-task-loop.test.js +0 -380
  489. package/dist/test/unit/safe-status.test.d.ts +0 -1
  490. package/dist/test/unit/safe-status.test.js +0 -165
  491. package/dist/test/unit/safe-views.test.d.ts +0 -1
  492. package/dist/test/unit/safe-views.test.js +0 -171
  493. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  494. package/dist/test/unit/schema-drift-check.test.js +0 -176
  495. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  496. package/dist/test/unit/sensitive-guard.test.js +0 -104
  497. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  498. package/dist/test/unit/subgoal-sync.test.js +0 -183
  499. package/dist/test/unit/sync-file.test.d.ts +0 -1
  500. package/dist/test/unit/sync-file.test.js +0 -154
  501. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  502. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  503. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  504. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  505. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  506. package/dist/test/unit/toolSearch.test.js +0 -571
  507. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  508. package/dist/test/unit/watcher-status.test.js +0 -169
  509. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  510. package/dist/test/unit/worktree-manager.test.js +0 -176
  511. package/dist/tools/applyPatch.js +0 -41
  512. package/dist/tools/cancelTask.d.ts +0 -30
  513. package/dist/tools/cancelTask.js +0 -64
  514. package/dist/tools/evidencePack.js +0 -168
  515. package/dist/tools/finalizeDirectSession.js +0 -84
  516. package/dist/tools/goalSubgoalTask.js +0 -110
  517. package/dist/tools/killTask.d.ts +0 -15
  518. package/dist/tools/retryTask.js +0 -45
  519. package/dist/tools/runVerification.js +0 -36
  520. package/dist/tools/syncFile.js +0 -65
  521. package/docs/control-center/control-center-daily-driver.md +0 -211
  522. package/docs/control-center/control-center-mvp.md +0 -205
  523. package/docs/control-center/control-center-phase2.md +0 -159
  524. package/docs/release-v0.6.0.md +0 -71
  525. package/docs/release-v0.6.1.md +0 -75
  526. package/docs/release-v0.6.4.md +0 -45
  527. package/src/test/unit/acceptance-engine.test.ts +0 -261
  528. package/src/test/unit/agent-router.test.ts +0 -342
  529. package/src/test/unit/android-doctor.test.ts +0 -158
  530. package/src/test/unit/audit-checks.test.ts +0 -567
  531. package/src/test/unit/chinese-path.test.ts +0 -106
  532. package/src/test/unit/command-guard.test.ts +0 -221
  533. package/src/test/unit/diagnose-task.test.ts +0 -544
  534. package/src/test/unit/direct-guards.test.ts +0 -297
  535. package/src/test/unit/discovery-token-store.test.ts +0 -181
  536. package/src/test/unit/evidence-pack.test.ts +0 -142
  537. package/src/test/unit/goal-graph.test.ts +0 -347
  538. package/src/test/unit/goal-progress.test.ts +0 -538
  539. package/src/test/unit/goal-status.test.ts +0 -270
  540. package/src/test/unit/goal-store.test.ts +0 -318
  541. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  542. package/src/test/unit/goal-tools-registry.test.ts +0 -243
  543. package/src/test/unit/handoff-export.test.ts +0 -295
  544. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  545. package/src/test/unit/logging.test.ts +0 -497
  546. package/src/test/unit/path-guard.test.ts +0 -150
  547. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  548. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  549. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  550. package/src/test/unit/release-gate.test.ts +0 -314
  551. package/src/test/unit/run-task-loop.test.ts +0 -425
  552. package/src/test/unit/safe-status.test.ts +0 -187
  553. package/src/test/unit/safe-views.test.ts +0 -184
  554. package/src/test/unit/schema-drift-check.test.ts +0 -260
  555. package/src/test/unit/sensitive-guard.test.ts +0 -124
  556. package/src/test/unit/subgoal-sync.test.ts +0 -236
  557. package/src/test/unit/sync-file.test.ts +0 -231
  558. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  559. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  560. package/src/test/unit/toolSearch.test.ts +0 -756
  561. package/src/test/unit/watcher-status.test.ts +0 -190
  562. package/src/test/unit/worktree-manager.test.ts +0 -247
  563. package/src/tools/cancelTask.ts +0 -70
  564. package/src/tools/evidencePack.ts +0 -205
  565. package/src/tools/retryTask.ts +0 -53
  566. package/src/tools/syncFile.ts +0 -122
  567. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  568. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  569. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  570. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  571. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  572. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  573. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  574. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  575. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  576. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  577. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  578. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  579. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  580. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  581. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  582. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  583. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  584. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  585. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  586. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  587. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  588. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  589. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  590. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  591. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  592. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  593. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  594. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  595. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  596. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  597. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -1,10 +1,7 @@
1
1
  import { spawn, spawnSync, type ChildProcess } from "node:child_process";
2
2
  import {
3
- createWriteStream,
4
3
  existsSync,
5
4
  readFileSync,
6
- writeFileSync,
7
- type WriteStream,
8
5
  } from "node:fs";
9
6
  import { isAbsolute, join, relative, resolve } from "node:path";
10
7
  import { getTasksDir, getPlansDir, getConfig, resolveWorkspaceRoot } from "../config.js";
@@ -12,11 +9,17 @@ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
12
9
  import {
13
10
  guardTestCommand,
14
11
  } from "../security/commandGuard.js";
15
- import { writeTaskProgress } from "../taskProgress.js";
16
- import { writeTaskRuntime } from "../taskRuntime.js";
12
+ import { writeTaskProgress } from "./taskProgress.js";
13
+ import { writeTaskRuntime } from "./taskRuntime.js";
14
+ import {
15
+ claimPendingTask,
16
+ readTaskStatusFile,
17
+ updateTaskStatusFile,
18
+ type TaskStatusRecord,
19
+ } from "./taskStatusStore.js";
17
20
  import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
18
21
  import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js";
19
- import type { TaskPhase, TaskStatus } from "../tools/createTask.js";
22
+ import type { TaskPhase, TaskStatus } from "../tools/tasks/createTask.js";
20
23
  import {
21
24
  buildChangeArtifacts,
22
25
  captureRepoSnapshot,
@@ -31,10 +34,22 @@ import {
31
34
  type ChangeArtifacts,
32
35
  type ExternalDirtyFile,
33
36
  type ArtifactManifest,
37
+ type RepoSnapshot,
38
+ type ChangedFileGroups,
34
39
  } from "./changeCapture.js";
35
40
  import { PatchWardenError, errorPayload } from "../errors.js";
36
- import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
41
+ import { ARTIFACT_SCHEMA_VERSION } from "../version.js";
42
+ import { diagnoseAndroidBuild } from "../tools/workspace/androidDoctor.js";
37
43
  import { runPostTaskCleanup, type PostTaskCleanupReport } from "./postTaskCleanup.js";
44
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
45
+ import {
46
+ allowedEnvironmentValues,
47
+ buildChildEnvironment,
48
+ redactProcessOutput,
49
+ resolvePackageManagerInvocation,
50
+ resolveTrustedExecutable,
51
+ SecureProcessLogCapture,
52
+ } from "./processSecurity.js";
38
53
 
39
54
  const HEARTBEAT_INTERVAL_MS = 2000;
40
55
  const GRACEFUL_KILL_MS = 2000;
@@ -85,7 +100,63 @@ interface VerifyReport {
85
100
  failure_reason?: string;
86
101
  }
87
102
 
103
+ interface TaskContext {
104
+ taskId: string;
105
+ taskDir: string;
106
+ statusFile: string;
107
+ repoPath: string;
108
+ wsRoot: string;
109
+ config: ReturnType<typeof getConfig>;
110
+ plansDir: string;
111
+ initialStatus: TaskStatusRecord;
112
+ planId: string;
113
+ agentName: string;
114
+ testCommand: string;
115
+ changePolicy: string;
116
+ verifyCommands: string[];
117
+ timeoutSeconds: number;
118
+ startedAtMs: number;
119
+ deadlineMs: number;
120
+ beforeSnapshot: RepoSnapshot;
121
+ beforeWorkspaceSnapshot: RepoSnapshot;
122
+ externalDirtyBaseline: ExternalDirtyFile[];
123
+ }
124
+
125
+ interface ExecutionState {
126
+ agentResult: ManagedProcessResult | null;
127
+ testResult: TestExecutionResult;
128
+ verifyResults: TestExecutionResult[];
129
+ finalStatus: TaskStatus;
130
+ finalError: string | null;
131
+ lastCaughtError: unknown;
132
+ }
133
+
134
+ interface ArtifactEvidence {
135
+ changes: ChangeArtifacts;
136
+ artifactStatus: "collected" | "partial" | "failed" | "timeout";
137
+ artifactCollectionError: string | null;
138
+ artifactCollectionStartedAt: string;
139
+ artifactCollectionFinishedAt: string;
140
+ outOfScopeChanges: ChangedFile[];
141
+ newOutOfScopeChanges: ExternalDirtyFile[];
142
+ preexistingExternalDirty: ExternalDirtyFile[];
143
+ preexistingWarnings: string[];
144
+ cleanupReport: PostTaskCleanupReport;
145
+ artifactManifest: ArtifactManifest;
146
+ changedFileGroups: ChangedFileGroups;
147
+ }
148
+
88
149
  export async function runTask(taskId: string): Promise<TaskRunResult> {
150
+ const prepared = await prepareTask(taskId);
151
+ if (!("taskDir" in prepared)) return prepared;
152
+ const ctx: TaskContext = prepared;
153
+ const state = await executeAgent(ctx);
154
+ await runVerification(ctx, state);
155
+ const evidence = await collectArtifacts(ctx, state);
156
+ return finalizeTask(ctx, state, evidence);
157
+ }
158
+
159
+ async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult> {
89
160
  const config = getConfig();
90
161
  const tasksDir = getTasksDir(config);
91
162
  const plansDir = getPlansDir(config);
@@ -96,7 +167,25 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
96
167
  const statusFile = join(taskDir, "status.json");
97
168
  if (!existsSync(statusFile)) throw new Error(`Task not found: "${taskId}"`);
98
169
 
99
- const initialStatus = readStatus(statusFile);
170
+ const startedAtMs = Date.now();
171
+ const claim = claimPendingTask(statusFile, {
172
+ status: "running",
173
+ phase: "preparing",
174
+ started_at: new Date(startedAtMs).toISOString(),
175
+ last_heartbeat_at: new Date(startedAtMs).toISOString(),
176
+ current_command: null,
177
+ error: null,
178
+ });
179
+ if (!claim.claimed) {
180
+ const currentStatus = String(claim.status.status || "failed") as TaskStatus;
181
+ return {
182
+ task_id: taskId,
183
+ status: currentStatus,
184
+ error: `Task is ${currentStatus}; only pending tasks can be claimed for execution.`,
185
+ };
186
+ }
187
+
188
+ const initialStatus = claim.status;
100
189
  const planId = String(initialStatus.plan_id || "");
101
190
  const agentName = String(initialStatus.agent || "");
102
191
  const rawRepoPath = String(initialStatus.resolved_repo_path || initialStatus.repo_path || wsRoot);
@@ -109,7 +198,6 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
109
198
  } catch (error) {
110
199
  return failBeforeExecution(taskId, taskDir, errorMessage(error), error);
111
200
  }
112
- const startedAtMs = Date.now();
113
201
  const deadlineMs = startedAtMs + timeoutSeconds * 1000;
114
202
 
115
203
  let repoPath: string;
@@ -126,11 +214,10 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
126
214
  return failBeforeExecution(taskId, taskDir, message, error);
127
215
  }
128
216
 
129
- // ── Assessment freshness revalidation before execution ──
130
217
  const assessmentId = String(initialStatus.assessment_id || "");
131
218
  if (assessmentId) {
132
219
  try {
133
- const preExecSnapshot = captureRepoSnapshot(repoPath);
220
+ const preExecSnapshot = await captureRepoSnapshot(repoPath);
134
221
  const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
135
222
  if (!validation.valid) {
136
223
  const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
@@ -142,166 +229,171 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
142
229
  }
143
230
  }
144
231
 
145
- updateStatus(taskDir, {
146
- status: "running",
147
- phase: "preparing",
148
- started_at: new Date(startedAtMs).toISOString(),
149
- last_heartbeat_at: new Date().toISOString(),
150
- timeout_seconds: timeoutSeconds,
151
- current_command: null,
152
- error: null,
153
- });
154
- // v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
155
- // can detect PID reuse and orphaned tasks. watcher_instance_id comes from
156
- // the watcher process env; when run_task MCP tool is used directly, this
157
- // field is intentionally left undefined so ownership cannot be falsely claimed.
232
+ updateStatus(taskDir, { timeout_seconds: timeoutSeconds });
158
233
  writeTaskRuntime(taskDir, {
159
234
  task_started_at: new Date(startedAtMs).toISOString(),
160
235
  watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
161
236
  });
162
237
  setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
163
238
 
164
- let beforeSnapshot;
165
- let beforeWorkspaceSnapshot;
239
+ let beforeSnapshot: RepoSnapshot;
240
+ let beforeWorkspaceSnapshot: RepoSnapshot;
166
241
  let externalDirtyBaseline: ExternalDirtyFile[] = [];
167
242
  try {
168
- beforeSnapshot = captureRepoSnapshot(repoPath);
169
- beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
243
+ beforeSnapshot = await captureRepoSnapshot(repoPath);
244
+ beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : await captureRepoSnapshot(wsRoot);
170
245
  writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
171
246
  writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
172
- // Phase 4: Record external dirty files as baseline before task execution
173
247
  externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
174
248
  } catch (error) {
175
249
  const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
176
250
  return failBeforeExecution(taskId, taskDir, message, error);
177
251
  }
178
252
 
179
- let agentResult: ManagedProcessResult | null = null;
180
- let testResult: TestExecutionResult = skippedTest(testCommand, repoPath, "Agent did not complete successfully.");
181
- const verifyResults: TestExecutionResult[] = [];
182
- let finalStatus: TaskStatus = "failed";
183
- let finalError: string | null = null;
184
- let lastCaughtError: unknown = null;
253
+ return {
254
+ taskId, taskDir, statusFile, repoPath, wsRoot, config, plansDir, initialStatus,
255
+ planId, agentName, testCommand, changePolicy, verifyCommands, timeoutSeconds,
256
+ startedAtMs, deadlineMs, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
257
+ };
258
+ }
259
+
260
+ async function executeAgent(ctx: TaskContext): Promise<ExecutionState> {
261
+ const state: ExecutionState = {
262
+ agentResult: null,
263
+ testResult: skippedTest(ctx.testCommand, ctx.repoPath, "Agent did not complete successfully."),
264
+ verifyResults: [],
265
+ finalStatus: "failed",
266
+ finalError: null,
267
+ lastCaughtError: null,
268
+ };
185
269
 
186
270
  try {
187
- const planFile = resolve(plansDir, planId, "plan.md");
188
- if (!existsSync(planFile)) throw new Error(`Plan not found: "${planId}". Save the plan first.`);
271
+ const planFile = resolve(ctx.plansDir, ctx.planId, "plan.md");
272
+ if (!existsSync(planFile)) throw new Error(`Plan not found: "${ctx.planId}". Save the plan first.`);
189
273
  const planContent = readFileSync(planFile, "utf-8");
190
- const prompt = buildExecutionPrompt(planContent, repoPath, testCommand);
191
- const invocation = buildAgentInvocation(agentName, repoPath, prompt, config);
192
- const agentCommandLabel = invocation.commandLabel;
274
+ const prompt = buildExecutionPrompt(planContent, ctx.repoPath, ctx.testCommand);
275
+ const invocation = buildAgentInvocation(ctx.agentName, ctx.repoPath, prompt, ctx.config);
193
276
 
194
- setTaskPhase(taskDir, "executing_agent", agentCommandLabel);
195
- agentResult = await runManagedProcess({
277
+ setTaskPhase(ctx.taskDir, "executing_agent", invocation.commandLabel);
278
+ state.agentResult = await runManagedProcess({
196
279
  command: invocation.command,
197
280
  args: invocation.args,
198
- cwd: repoPath,
199
- taskDir,
200
- statusFile,
281
+ cwd: ctx.repoPath,
282
+ taskDir: ctx.taskDir,
283
+ statusFile: ctx.statusFile,
201
284
  phase: "executing_agent",
202
- currentCommand: agentCommandLabel,
203
- deadlineMs,
204
- stdoutPath: join(taskDir, "stdout.log"),
205
- stderrPath: join(taskDir, "stderr.log"),
285
+ currentCommand: invocation.commandLabel,
286
+ deadlineMs: ctx.deadlineMs,
287
+ stdoutPath: join(ctx.taskDir, "stdout.log"),
288
+ stderrPath: join(ctx.taskDir, "stderr.log"),
289
+ environmentVariableNames: invocation.environmentVariableNames,
290
+ blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
206
291
  });
207
292
 
293
+ const agentResult = state.agentResult;
208
294
  if (agentResult.terminationReason === "canceled" || agentResult.terminationReason === "killed") {
209
- finalStatus = "canceled";
210
- finalError = agentResult.terminationReason === "killed"
295
+ state.finalStatus = "canceled";
296
+ state.finalError = agentResult.terminationReason === "killed"
211
297
  ? "Task was terminated by kill_task."
212
298
  : "Task was canceled by user request.";
213
299
  } else if (agentResult.terminationReason === "timeout") {
214
- finalError = `Task timed out after ${timeoutSeconds} seconds during agent execution.`;
300
+ state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during agent execution.`;
215
301
  } else if (agentResult.spawnError) {
216
- finalError = `Agent spawn failed: ${agentResult.spawnError}`;
302
+ state.finalError = `Agent spawn failed: ${agentResult.spawnError}`;
217
303
  } else if (agentResult.exitCode !== 0) {
218
- finalError = `Agent exited with code ${agentResult.exitCode}.`;
219
- } else if (verifyCommands.length > 0) {
220
- for (const command of verifyCommands) {
221
- setTaskPhase(taskDir, "running_tests", command);
222
- const verification = await runTrustedTestCommand(command, repoPath, taskDir, statusFile, deadlineMs);
223
- verifyResults.push(verification);
224
- if (verification.terminationReason || Date.now() >= deadlineMs) break;
304
+ state.finalError = `Agent exited with code ${agentResult.exitCode}.`;
305
+ }
306
+ } catch (error) {
307
+ state.lastCaughtError = error;
308
+ state.finalError = errorMessage(error);
309
+ }
310
+
311
+ return state;
312
+ }
313
+
314
+ async function runVerification(ctx: TaskContext, state: ExecutionState): Promise<ExecutionState> {
315
+ if (state.finalError !== null) return state;
316
+
317
+ try {
318
+ if (ctx.verifyCommands.length > 0) {
319
+ for (const command of ctx.verifyCommands) {
320
+ setTaskPhase(ctx.taskDir, "running_tests", command);
321
+ const verification = await runTrustedTestCommand(command, ctx.repoPath, ctx.taskDir, ctx.statusFile, ctx.deadlineMs);
322
+ state.verifyResults.push(verification);
323
+ if (verification.terminationReason || Date.now() >= ctx.deadlineMs) break;
225
324
  }
226
- testResult = verifyResults[0] || skippedTest(testCommand, repoPath, "No verification command ran.");
227
- const interrupted = verifyResults.find((result) => result.terminationReason);
228
- const failedVerification = verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
325
+ state.testResult = state.verifyResults[0] || skippedTest(ctx.testCommand, ctx.repoPath, "No verification command ran.");
326
+ const interrupted = state.verifyResults.find((result) => result.terminationReason);
327
+ const failedVerification = state.verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
229
328
  if (interrupted?.terminationReason === "canceled" || interrupted?.terminationReason === "killed") {
230
- finalStatus = "canceled";
231
- finalError = interrupted.terminationReason === "killed"
329
+ state.finalStatus = "canceled";
330
+ state.finalError = interrupted.terminationReason === "killed"
232
331
  ? "Task was terminated by kill_task during verification."
233
332
  : "Task was canceled during verification.";
234
- } else if (interrupted?.terminationReason === "timeout" || Date.now() >= deadlineMs) {
235
- finalError = `Task timed out after ${timeoutSeconds} seconds during verification.`;
333
+ } else if (interrupted?.terminationReason === "timeout" || Date.now() >= ctx.deadlineMs) {
334
+ state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during verification.`;
236
335
  } else if (failedVerification) {
237
- finalStatus = "failed_verification";
238
- finalError = failedVerification.spawnError
239
- ? `Verification command \"${failedVerification.command}\" could not start: ${failedVerification.spawnError}`
240
- : `Verification command \"${failedVerification.command}\" exited with code ${failedVerification.exitCode}.`;
241
- } else if (verifyResults.length === verifyCommands.length) {
242
- finalStatus = "done_by_agent";
336
+ state.finalStatus = "failed_verification";
337
+ state.finalError = failedVerification.spawnError
338
+ ? `Verification command "${failedVerification.command}" could not start: ${failedVerification.spawnError}`
339
+ : `Verification command "${failedVerification.command}" exited with code ${failedVerification.exitCode}.`;
340
+ } else if (state.verifyResults.length === ctx.verifyCommands.length) {
341
+ state.finalStatus = "done_by_agent";
243
342
  } else {
244
- finalError = "Verification did not complete all configured commands.";
343
+ state.finalError = "Verification did not complete all configured commands.";
245
344
  }
246
345
  } else {
247
- testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
248
- finalStatus = "done_by_agent";
346
+ state.testResult = skippedTest(ctx.testCommand, ctx.repoPath, "No verification command configured.");
347
+ state.finalStatus = "done_by_agent";
249
348
  }
250
349
  } catch (error) {
251
- lastCaughtError = error;
252
- finalError = errorMessage(error);
350
+ state.lastCaughtError = error;
351
+ state.finalError = errorMessage(error);
253
352
  }
254
353
 
255
- let cleanupReport: PostTaskCleanupReport = {
256
- enabled: true,
257
- removed: [],
258
- skipped: [],
259
- source_files_touched: 0,
260
- };
261
- if (finalStatus !== "canceled") {
354
+ return state;
355
+ }
356
+
357
+ async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promise<ArtifactEvidence> {
358
+ let cleanupReport: PostTaskCleanupReport = { enabled: true, removed: [], skipped: [], source_files_touched: 0 };
359
+ if (state.finalStatus !== "canceled") {
262
360
  try {
263
- cleanupReport = runPostTaskCleanup(repoPath, taskDir);
264
- updateStatus(taskDir, { cleanup: cleanupReport });
361
+ cleanupReport = runPostTaskCleanup(ctx.repoPath, ctx.taskDir);
362
+ updateStatus(ctx.taskDir, { cleanup: cleanupReport });
265
363
  } catch (error) {
266
364
  cleanupReport = {
267
- enabled: true,
268
- removed: [],
365
+ enabled: true, removed: [],
269
366
  skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
270
367
  source_files_touched: 0,
271
368
  };
272
- writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
273
- updateStatus(taskDir, { cleanup: cleanupReport });
369
+ atomicWriteJsonFileSync(join(ctx.taskDir, "post-task-cleanup.json"), cleanupReport);
370
+ updateStatus(ctx.taskDir, { cleanup: cleanupReport });
274
371
  }
275
372
  }
276
- setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
373
+
374
+ setTaskPhase(ctx.taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
277
375
  const artifactCollectionStartedAt = new Date().toISOString();
278
376
  let changes: ChangeArtifacts;
279
377
  let artifactStatus: "collected" | "partial" | "failed" | "timeout" = "collected";
280
378
  let artifactCollectionError: string | null = null;
281
379
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
380
+ const artifactController = new AbortController();
282
381
  try {
283
- // Phase 5: Wrap artifact collection with a timeout
284
- const collectionResult = await Promise.race([
285
- Promise.resolve().then(() => {
286
- const afterSnapshot = captureRepoSnapshot(repoPath);
287
- writeSnapshot(taskDir, "git-after.json", afterSnapshot);
288
- return buildChangeArtifacts(repoPath, beforeSnapshot, afterSnapshot);
289
- }),
290
- new Promise<never>((_, reject) => {
291
- timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
292
- }),
293
- ]);
294
- if (timeoutHandle) clearTimeout(timeoutHandle);
295
- changes = collectionResult;
382
+ timeoutHandle = setTimeout(() => {
383
+ artifactController.abort(new Error("Artifact collection timed out"));
384
+ }, ARTIFACT_COLLECTION_TIMEOUT_MS);
385
+ const afterSnapshot = await captureRepoSnapshot(ctx.repoPath, artifactController.signal);
386
+ writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
387
+ changes = await buildChangeArtifacts(
388
+ ctx.repoPath,
389
+ ctx.beforeSnapshot,
390
+ afterSnapshot,
391
+ artifactController.signal,
392
+ );
296
393
  } catch (error) {
297
- if (timeoutHandle) clearTimeout(timeoutHandle);
298
- lastCaughtError = error;
394
+ state.lastCaughtError = error;
299
395
  artifactCollectionError = errorMessage(error);
300
- if (artifactCollectionError.includes("timed out")) {
301
- artifactStatus = "timeout";
302
- } else {
303
- artifactStatus = "failed";
304
- }
396
+ artifactStatus = artifactCollectionError.includes("timed out") ? "timeout" : "failed";
305
397
  changes = {
306
398
  changed_files: [],
307
399
  diff: `(change capture failed: ${artifactCollectionError})\n`,
@@ -311,17 +403,16 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
311
403
  additions: 0,
312
404
  deletions: 0,
313
405
  file_stats: [],
314
- workspace_dirty_before: beforeSnapshot.workspace_dirty,
315
- workspace_dirty_after: beforeSnapshot.workspace_dirty,
406
+ workspace_dirty_before: ctx.beforeSnapshot.workspace_dirty,
407
+ workspace_dirty_after: ctx.beforeSnapshot.workspace_dirty,
316
408
  patch_mode: "hash_only",
317
409
  unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
318
410
  artifact_hygiene: emptyArtifactHygiene(),
319
411
  };
320
- // Phase 5: Write partial_result.md when artifact collection fails
321
- writeFileSync(join(taskDir, "partial_result.md"), [
412
+ atomicWriteFileSync(join(ctx.taskDir, "partial_result.md"), [
322
413
  "# PatchWarden Partial Result",
323
414
  "",
324
- `Task: ${taskId}`,
415
+ `Task: ${ctx.taskId}`,
325
416
  "",
326
417
  "## Artifact Collection Status",
327
418
  artifactStatus,
@@ -330,112 +421,132 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
330
421
  artifactCollectionError,
331
422
  "",
332
423
  "## Agent Status",
333
- agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
424
+ state.agentResult ? `Exit code: ${state.agentResult.exitCode}` : "Agent did not run",
334
425
  "",
335
426
  "## Verification Status",
336
- verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
427
+ state.verifyResults.length > 0 ? state.verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
337
428
  "",
338
429
  "## Note",
339
430
  "Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
340
431
  "",
341
- ].join("\n"), "utf-8");
342
- // Don't override test failure with artifact failure
343
- if (finalStatus === "done_by_agent") {
344
- finalError = `Change capture failed: ${artifactCollectionError}`;
345
- finalStatus = "failed";
432
+ ].join("\n"));
433
+ if (state.finalStatus === "done_by_agent") {
434
+ state.finalError = `Change capture failed: ${artifactCollectionError}`;
435
+ state.finalStatus = "failed";
346
436
  } else {
347
- finalError ||= `Change capture failed: ${artifactCollectionError}`;
437
+ state.finalError ||= `Change capture failed: ${artifactCollectionError}`;
348
438
  }
439
+ } finally {
440
+ if (timeoutHandle) clearTimeout(timeoutHandle);
349
441
  }
350
442
  const artifactCollectionFinishedAt = new Date().toISOString();
351
443
 
352
444
  let outOfScopeChanges: ChangedFile[] = [];
353
- let preexistingExternalDirty: ExternalDirtyFile[] = externalDirtyBaseline;
445
+ const preexistingExternalDirty: ExternalDirtyFile[] = ctx.externalDirtyBaseline;
354
446
  let newOutOfScopeChanges: ExternalDirtyFile[] = [];
355
447
  try {
356
- const afterWorkspaceSnapshot = repoPath === wsRoot ? captureRepoSnapshot(repoPath) : captureRepoSnapshot(wsRoot);
357
- writeSnapshot(taskDir, "workspace-after.json", afterWorkspaceSnapshot);
358
- const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, repoPath, wsRoot);
359
- // Phase 4: Only NEW external dirty files (not in baseline) are scope violations
360
- newOutOfScopeChanges = findNewExternalDirtyFiles(externalDirtyBaseline, allExternalDirty);
361
- outOfScopeChanges = compareSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
448
+ const afterWorkspaceSnapshot = ctx.repoPath === ctx.wsRoot ? await captureRepoSnapshot(ctx.repoPath) : await captureRepoSnapshot(ctx.wsRoot);
449
+ writeSnapshot(ctx.taskDir, "workspace-after.json", afterWorkspaceSnapshot);
450
+ const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, ctx.repoPath, ctx.wsRoot);
451
+ newOutOfScopeChanges = findNewExternalDirtyFiles(ctx.externalDirtyBaseline, allExternalDirty);
452
+ outOfScopeChanges = compareSnapshots(ctx.beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
362
453
  .filter((file) =>
363
- !isPathInside(resolve(wsRoot, file.path), repoPath) ||
364
- Boolean(file.old_path && !isPathInside(resolve(wsRoot, file.old_path), repoPath))
454
+ !isPathInside(resolve(ctx.wsRoot, file.path), ctx.repoPath) ||
455
+ Boolean(file.old_path && !isPathInside(resolve(ctx.wsRoot, file.old_path), ctx.repoPath))
365
456
  );
366
457
  } catch (error) {
367
- finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
368
- if (finalStatus === "done_by_agent") finalStatus = "failed";
458
+ state.finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
459
+ if (state.finalStatus === "done_by_agent") state.finalStatus = "failed";
369
460
  }
370
461
 
371
- // Phase 4: Pre-existing external dirty files are warnings, not failures
372
462
  const preexistingWarnings: string[] = preexistingExternalDirty.length > 0
373
463
  ? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
374
464
  : [];
375
465
 
466
+ applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty);
467
+
468
+ atomicWriteFileSync(join(ctx.taskDir, "git.diff"), changes.diff);
469
+ atomicWriteFileSync(join(ctx.taskDir, "diff.patch"), changes.diff);
470
+ atomicWriteJsonFileSync(join(ctx.taskDir, "changed-files.json"), { schema_version: ARTIFACT_SCHEMA_VERSION, ...changes });
471
+ const artifactManifest = await buildArtifactManifest(changes.changed_files, ctx.repoPath, ctx.taskId);
472
+ atomicWriteJsonFileSync(join(ctx.taskDir, "artifact_manifest.json"), artifactManifest);
473
+ const changedFileGroups = groupChangedFiles(changes.changed_files);
474
+ atomicWriteJsonFileSync(join(ctx.taskDir, "file-stats.json"), {
475
+ task_id: ctx.taskId,
476
+ additions: changes.additions,
477
+ deletions: changes.deletions,
478
+ files: changes.file_stats,
479
+ });
480
+
481
+ return {
482
+ changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
483
+ outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
484
+ artifactManifest, changedFileGroups,
485
+ };
486
+ }
487
+
488
+ function applyScopeViolationVerdict(
489
+ ctx: TaskContext,
490
+ state: ExecutionState,
491
+ changes: ChangeArtifacts,
492
+ newOutOfScopeChanges: ExternalDirtyFile[],
493
+ preexistingExternalDirty: ExternalDirtyFile[],
494
+ ): void {
376
495
  if (newOutOfScopeChanges.length > 0) {
377
- finalStatus = "failed_scope_violation";
378
- finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
379
- writeFileSync(join(taskDir, "rollback-plan.json"), JSON.stringify({
380
- task_id: taskId,
496
+ state.finalStatus = "failed_scope_violation";
497
+ state.finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
498
+ atomicWriteJsonFileSync(join(ctx.taskDir, "rollback-plan.json"), {
499
+ task_id: ctx.taskId,
381
500
  status: "review_required",
382
501
  automatic_rollback_performed: false,
383
502
  warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
384
503
  out_of_scope_changes: newOutOfScopeChanges,
385
504
  preexisting_external_dirty_files: preexistingExternalDirty,
386
- }, null, 2), "utf-8");
387
- writeFileSync(join(taskDir, "rollback_scope_violation_plan.md"), [
505
+ });
506
+ atomicWriteFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
388
507
  "# Scope Violation Rollback Plan",
389
508
  "",
390
- `Task: ${taskId}`,
509
+ `Task: ${ctx.taskId}`,
391
510
  "",
392
511
  "PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
393
512
  "",
394
513
  "## New out-of-scope files (caused by this task)",
395
514
  ...newOutOfScopeChanges.map((file) => `- ${file.change}: ${file.path}`),
396
515
  "",
397
- ].join("\n"), "utf-8");
398
- } else if (changePolicy === "no_changes" && changes.changed_files.length > 0) {
399
- finalStatus = "failed_policy_violation";
400
- finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
516
+ ].join("\n"));
517
+ } else if (changes.diff_redacted) {
518
+ state.finalStatus = "failed_policy_violation";
519
+ state.finalError = "Credential-like content was detected in the task diff. PatchWarden redacted the evidence; remove the sensitive content before retrying.";
520
+ } else if (ctx.changePolicy === "no_changes" && changes.changed_files.length > 0) {
521
+ state.finalStatus = "failed_policy_violation";
522
+ state.finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
401
523
  }
524
+ }
402
525
 
403
- writeFileSync(join(taskDir, "git.diff"), changes.diff, "utf-8");
404
- writeFileSync(join(taskDir, "diff.patch"), changes.diff, "utf-8");
405
- writeFileSync(join(taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
406
-
407
- // Phase 6: Generate artifact_manifest.json and group changed files
408
- const artifactManifest: ArtifactManifest = buildArtifactManifest(changes.changed_files, repoPath, taskId);
409
- writeFileSync(join(taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
410
- const changedFileGroups = groupChangedFiles(changes.changed_files);
526
+ function finalizeTask(ctx: TaskContext, state: ExecutionState, evidence: ArtifactEvidence): TaskRunResult {
527
+ const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
528
+ outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
529
+ artifactManifest, changedFileGroups } = evidence;
411
530
 
412
- writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
413
- task_id: taskId,
414
- additions: changes.additions,
415
- deletions: changes.deletions,
416
- files: changes.file_stats,
417
- }, null, 2), "utf-8");
418
- writeFileSync(join(taskDir, "test.log"), buildTestLog(testResult), "utf-8");
419
- const verifyJson = buildVerifyJson(verifyCommands, verifyResults, repoPath);
420
- // Phase 4: Use newOutOfScopeChanges (not outOfScopeChanges) for verify_status
421
- // to stay consistent with finalStatus. Pre-existing external dirty files
422
- // that didn't change during the task should NOT cause verify_status to fail.
531
+ atomicWriteFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult));
532
+ const verifyJson = buildVerifyJson(ctx.verifyCommands, state.verifyResults, ctx.repoPath);
423
533
  if (newOutOfScopeChanges.length > 0) {
424
534
  verifyJson.status = "failed";
425
535
  verifyJson.failure_reason = "scope_violation";
426
- } else if (finalStatus === "failed_policy_violation") {
536
+ } else if (state.finalStatus === "failed_policy_violation") {
427
537
  verifyJson.status = "failed";
428
- verifyJson.failure_reason = "change_policy_violation";
538
+ verifyJson.failure_reason = changes.diff_redacted
539
+ ? "sensitive_content_violation"
540
+ : "change_policy_violation";
429
541
  }
430
- writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
431
- writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
542
+ atomicWriteJsonFileSync(join(ctx.taskDir, "verify.json"), verifyJson);
543
+ atomicWriteFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands));
432
544
 
433
- if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus)) finalStatus = "failed";
434
- const finalPhase: TaskPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
435
- const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
545
+ if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus)) state.finalStatus = "failed";
546
+ const finalPhase: TaskPhase = state.finalStatus === "done_by_agent" ? "done_by_agent" : (state.finalStatus as TaskPhase);
547
+ const followup = buildFailureFollowup(state.finalStatus, state.finalError, verifyJson.commands);
436
548
 
437
- // Phase 7: Run Android build environment diagnostics if android_app exists
438
- const androidDiagnostic = diagnoseAndroidBuild(repoPath);
549
+ const androidDiagnostic = diagnoseAndroidBuild(ctx.repoPath);
439
550
  let androidWarning: string | null = null;
440
551
  if (androidDiagnostic.status !== "skip") {
441
552
  if (androidDiagnostic.status === "fail") {
@@ -449,13 +560,13 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
449
560
  }
450
561
 
451
562
  const resultMd = buildResultMarkdown({
452
- taskId,
453
- planId,
454
- agent: agentName,
455
- status: finalStatus,
456
- error: finalError,
457
- agentResult,
458
- testResult,
563
+ taskId: ctx.taskId,
564
+ planId: ctx.planId,
565
+ agent: ctx.agentName,
566
+ status: state.finalStatus,
567
+ error: state.finalError,
568
+ agentResult: state.agentResult,
569
+ testResult: state.testResult,
459
570
  verify: verifyJson,
460
571
  changes,
461
572
  outOfScopeChanges,
@@ -466,18 +577,91 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
466
577
  androidWarning,
467
578
  preexistingWarnings,
468
579
  });
469
- writeFileSync(join(taskDir, "result.md"), resultMd, "utf-8");
470
- writeFileSync(join(taskDir, "result.json"), JSON.stringify({
471
- task_id: taskId,
472
- status: finalStatus,
473
- agent: agentName,
474
- workspace_root: wsRoot,
475
- repo_path: initialStatus.repo_path,
476
- resolved_repo_path: repoPath,
477
- plan_source: initialStatus.plan_source || "saved",
478
- template: initialStatus.template || null,
479
- change_policy: changePolicy,
480
- summary: finalError || "Agent execution and configured verification completed successfully.",
580
+ atomicWriteFileSync(join(ctx.taskDir, "result.md"), resultMd);
581
+
582
+ const resultJson = buildResultJson({
583
+ ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning,
584
+ });
585
+ atomicWriteJsonFileSync(join(ctx.taskDir, "result.json"), resultJson);
586
+
587
+ if (state.finalError) {
588
+ const structuredError = errorPayload(state.lastCaughtError);
589
+ atomicWriteJsonFileSync(join(ctx.taskDir, "error.log"), {
590
+ summary: state.finalError,
591
+ ...structuredError,
592
+ });
593
+ }
594
+
595
+ const finishedAt = new Date().toISOString();
596
+ updateStatus(ctx.taskDir, {
597
+ status: state.finalStatus,
598
+ phase: finalPhase,
599
+ current_command: null,
600
+ last_heartbeat_at: finishedAt,
601
+ finished_at: finishedAt,
602
+ error: state.finalError,
603
+ changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
604
+ artifact_hygiene_counts: changes.artifact_hygiene.counts,
605
+ artifact_status: artifactStatus,
606
+ artifact_collection_error: artifactCollectionError,
607
+ artifact_collection_started_at: artifactCollectionStartedAt,
608
+ artifact_collection_finished_at: artifactCollectionFinishedAt,
609
+ cleanup: cleanupReport,
610
+ out_of_scope_changes: outOfScopeChanges,
611
+ new_out_of_scope_changes: newOutOfScopeChanges,
612
+ preexisting_external_dirty_files: preexistingExternalDirty,
613
+ verify_status: verifyJson.status,
614
+ verify_commands: ctx.verifyCommands,
615
+ diff_available: changes.diff_available,
616
+ diff_truncated: changes.diff_truncated,
617
+ diff_redacted: changes.diff_redacted === true,
618
+ diff_redaction_categories: changes.diff_redaction_categories ?? [],
619
+ workspace_dirty_before: changes.workspace_dirty_before,
620
+ workspace_dirty_after: changes.workspace_dirty_after,
621
+ workspace_dirty: changes.workspace_dirty_after,
622
+ acceptance_status: state.finalStatus === "done_by_agent" ? "pending" : null,
623
+ });
624
+ writeTaskRuntime(ctx.taskDir, {
625
+ phase: finalPhase,
626
+ current_command: null,
627
+ last_heartbeat_at: finishedAt,
628
+ runner_pid: process.pid,
629
+ child_pid: undefined,
630
+ });
631
+ writeTaskProgress(ctx.taskDir, finalPhase, {
632
+ heartbeatAt: finishedAt,
633
+ note: state.finalError || `Task finished with status ${state.finalStatus}.`,
634
+ });
635
+
636
+ return { task_id: ctx.taskId, status: state.finalStatus, error: state.finalError };
637
+ }
638
+
639
+ function buildResultJson(input: {
640
+ ctx: TaskContext;
641
+ state: ExecutionState;
642
+ evidence: ArtifactEvidence;
643
+ verifyJson: VerifyReport;
644
+ followup: ReturnType<typeof buildFailureFollowup>;
645
+ androidDiagnostic: ReturnType<typeof diagnoseAndroidBuild>;
646
+ androidWarning: string | null;
647
+ }): Record<string, unknown> {
648
+ const { ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning } = input;
649
+ const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt,
650
+ artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges,
651
+ preexistingExternalDirty, cleanupReport, artifactManifest, changedFileGroups } = evidence;
652
+
653
+ return {
654
+ schema_version: ARTIFACT_SCHEMA_VERSION,
655
+ task_id: ctx.taskId,
656
+ status: state.finalStatus,
657
+ agent: ctx.agentName,
658
+ workspace_root: ctx.wsRoot,
659
+ repo_path: ctx.initialStatus.repo_path,
660
+ resolved_repo_path: ctx.repoPath,
661
+ plan_source: ctx.initialStatus.plan_source || "saved",
662
+ template: ctx.initialStatus.template || null,
663
+ change_policy: ctx.changePolicy,
664
+ summary: state.finalError || "Agent execution and configured verification completed successfully.",
481
665
  changed_files: changes.changed_files,
482
666
  changed_file_groups: {
483
667
  source_changes: changedFileGroups.source_changes.length,
@@ -515,70 +699,24 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
515
699
  ...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
516
700
  ],
517
701
  warnings: [
518
- ...beforeSnapshot.warnings,
702
+ ...ctx.beforeSnapshot.warnings,
519
703
  ...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
520
- ...preexistingWarnings,
704
+ ...(changes.diff_redacted ? [
705
+ `Credential-like diff content was redacted (${(changes.diff_redaction_categories ?? []).join(", ") || "sensitive_content"}).`,
706
+ ] : []),
707
+ ...evidence.preexistingWarnings,
521
708
  ...(androidWarning ? [androidWarning] : []),
522
709
  ],
523
- errors: finalError ? [finalError] : [],
524
- known_issues: finalError ? [finalError] : [],
710
+ errors: state.finalError ? [state.finalError] : [],
711
+ known_issues: state.finalError ? [state.finalError] : [],
525
712
  failure_reason: followup.failure_reason,
526
713
  failed_command: followup.failed_command,
527
714
  suggested_next_action: followup.suggested_next_action,
528
715
  safe_followup_prompt: followup.safe_followup_prompt,
529
- next_steps: finalStatus === "done_by_agent"
716
+ next_steps: state.finalStatus === "done_by_agent"
530
717
  ? ["Review get_task_summary and audit_task before accepting the work."]
531
718
  : ["Resolve the reported failure before accepting the work."],
532
- }, null, 2), "utf-8");
533
- if (finalError) {
534
- // Phase 9: Preserve PatchWardenError structured info in error.log
535
- const structuredError = errorPayload(lastCaughtError);
536
- writeFileSync(join(taskDir, "error.log"), JSON.stringify({
537
- summary: finalError,
538
- ...structuredError,
539
- }, null, 2), "utf-8");
540
- }
541
-
542
- const finishedAt = new Date().toISOString();
543
- updateStatus(taskDir, {
544
- status: finalStatus,
545
- phase: finalPhase,
546
- current_command: null,
547
- last_heartbeat_at: finishedAt,
548
- finished_at: finishedAt,
549
- error: finalError,
550
- changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
551
- artifact_hygiene_counts: changes.artifact_hygiene.counts,
552
- artifact_status: artifactStatus,
553
- artifact_collection_error: artifactCollectionError,
554
- artifact_collection_started_at: artifactCollectionStartedAt,
555
- artifact_collection_finished_at: artifactCollectionFinishedAt,
556
- cleanup: cleanupReport,
557
- out_of_scope_changes: outOfScopeChanges,
558
- new_out_of_scope_changes: newOutOfScopeChanges,
559
- preexisting_external_dirty_files: preexistingExternalDirty,
560
- verify_status: verifyJson.status,
561
- verify_commands: verifyCommands,
562
- diff_available: changes.diff_available,
563
- diff_truncated: changes.diff_truncated,
564
- workspace_dirty_before: changes.workspace_dirty_before,
565
- workspace_dirty_after: changes.workspace_dirty_after,
566
- workspace_dirty: changes.workspace_dirty_after,
567
- acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
568
- });
569
- writeTaskRuntime(taskDir, {
570
- phase: finalPhase,
571
- current_command: null,
572
- last_heartbeat_at: finishedAt,
573
- runner_pid: process.pid,
574
- child_pid: undefined,
575
- });
576
- writeTaskProgress(taskDir, finalPhase, {
577
- heartbeatAt: finishedAt,
578
- note: finalError || `Task finished with status ${finalStatus}.`,
579
- });
580
-
581
- return { task_id: taskId, status: finalStatus, error: finalError };
719
+ };
582
720
  }
583
721
 
584
722
  function buildFailureFollowup(
@@ -651,18 +789,33 @@ async function runManagedProcess(options: {
651
789
  deadlineMs: number;
652
790
  stdoutPath?: string;
653
791
  stderrPath?: string;
792
+ environmentVariableNames?: string[];
793
+ blockedEnvironmentVariableNames?: string[];
794
+ maxLogBytes?: number;
654
795
  }): Promise<ManagedProcessResult> {
655
796
  if (Date.now() >= options.deadlineMs) {
656
797
  return { exitCode: null, stdout: "", stderr: "", spawnError: null, terminationReason: "timeout" };
657
798
  }
658
799
 
800
+ const exactRedactionValues = allowedEnvironmentValues(options.environmentVariableNames);
801
+ const logCapture = new SecureProcessLogCapture(
802
+ [options.stdoutPath, options.stderrPath],
803
+ options.maxLogBytes,
804
+ );
659
805
  let child: ChildProcess;
660
806
  try {
661
- child = spawn(options.command, options.args, {
807
+ const env = buildChildEnvironment({
808
+ cwd: options.cwd,
809
+ allowedNames: options.environmentVariableNames,
810
+ blockedNames: options.blockedEnvironmentVariableNames,
811
+ });
812
+ const command = resolveTrustedExecutable(options.command, options.cwd, { pathValue: env.PATH });
813
+ child = spawn(command, options.args, {
662
814
  cwd: options.cwd,
663
815
  stdio: ["ignore", "pipe", "pipe"],
664
816
  detached: process.platform !== "win32",
665
817
  windowsHide: true,
818
+ env,
666
819
  });
667
820
  } catch (error) {
668
821
  return { exitCode: null, stdout: "", stderr: "", spawnError: errorMessage(error), terminationReason: null };
@@ -674,8 +827,6 @@ async function runManagedProcess(options: {
674
827
  child_started_at: new Date().toISOString(),
675
828
  });
676
829
 
677
- const stdoutStream = openStream(options.stdoutPath);
678
- const stderrStream = openStream(options.stderrPath);
679
830
  let stdout = "";
680
831
  let stderr = "";
681
832
  let spawnError: string | null = null;
@@ -724,8 +875,8 @@ async function runManagedProcess(options: {
724
875
  // force-resolve to prevent the runner from hanging indefinitely.
725
876
  // Only starts AFTER termination is requested — normal long tasks are unaffected.
726
877
  fallbackTimer = setTimeout(() => {
727
- try { forceKill(child); } catch {}
728
- try { child.kill("SIGKILL"); } catch {}
878
+ try { forceKill(child); } catch {} // cleanup failure is safe to ignore
879
+ try { child.kill("SIGKILL"); } catch {} // cleanup failure is safe to ignore
729
880
  // Resolve the exit promise via the shared finish handle
730
881
  if (childExitFinish) childExitFinish(null);
731
882
  }, 10000);
@@ -734,12 +885,12 @@ async function runManagedProcess(options: {
734
885
  child.stdout?.on("data", (chunk: Buffer) => {
735
886
  const text = chunk.toString("utf-8");
736
887
  stdout = appendBounded(stdout, text);
737
- stdoutStream?.write(text);
888
+ logCapture.append(options.stdoutPath, chunk);
738
889
  });
739
890
  child.stderr?.on("data", (chunk: Buffer) => {
740
891
  const text = chunk.toString("utf-8");
741
892
  stderr = appendBounded(stderr, text);
742
- stderrStream?.write(text);
893
+ logCapture.append(options.stderrPath, chunk);
743
894
  });
744
895
 
745
896
  heartbeat();
@@ -755,12 +906,6 @@ async function runManagedProcess(options: {
755
906
  };
756
907
  childExitFinish = finish;
757
908
  child.once("close", (code) => finish(code));
758
- child.once("exit", (code) => {
759
- // On Windows, "close" can lag behind "exit" when stdio pipes drain
760
- // after taskkill. Resolve on exit to avoid hanging the runner, but
761
- // still let "close" fire if it arrives first.
762
- finish(code);
763
- });
764
909
  child.once("error", (error) => {
765
910
  spawnError = error.message;
766
911
  finish(null);
@@ -769,8 +914,7 @@ async function runManagedProcess(options: {
769
914
 
770
915
  clearInterval(heartbeatTimer);
771
916
  if (forceTimer) clearTimeout(forceTimer);
772
- stdoutStream?.destroy();
773
- stderrStream?.destroy();
917
+ logCapture.flush(exactRedactionValues);
774
918
  writeTaskRuntime(options.taskDir, {
775
919
  phase: options.phase,
776
920
  last_heartbeat_at: new Date().toISOString(),
@@ -778,8 +922,16 @@ async function runManagedProcess(options: {
778
922
  runner_pid: process.pid,
779
923
  child_pid: undefined,
780
924
  });
925
+ const safeStdout = redactProcessOutput(stdout, exactRedactionValues);
926
+ const safeStderr = redactProcessOutput(stderr, exactRedactionValues);
781
927
 
782
- return { exitCode, stdout, stderr, spawnError, terminationReason };
928
+ return {
929
+ exitCode,
930
+ stdout: safeStdout.length > MAX_CAPTURE_CHARS ? safeStdout.slice(-MAX_CAPTURE_CHARS) : safeStdout,
931
+ stderr: safeStderr.length > MAX_CAPTURE_CHARS ? safeStderr.slice(-MAX_CAPTURE_CHARS) : safeStderr,
932
+ spawnError,
933
+ terminationReason,
934
+ };
783
935
  }
784
936
 
785
937
  async function runTrustedTestCommand(
@@ -797,9 +949,9 @@ async function runTrustedTestCommand(
797
949
  let command = parts[0];
798
950
  let args = parts.slice(1);
799
951
  if (process.platform === "win32" && /^(npm|npm\.cmd|pnpm|pnpm\.cmd)$/i.test(command)) {
800
- const shim = /^pnpm/i.test(command) ? "pnpm.cmd" : "npm.cmd";
801
- command = process.env.ComSpec || "cmd.exe";
802
- args = ["/d", "/s", "/c", shim, ...args];
952
+ const packageManager = resolvePackageManagerInvocation(command, repoPath);
953
+ command = packageManager.command;
954
+ args = [...packageManager.argsPrefix, ...args];
803
955
  }
804
956
  const result = await runManagedProcess({
805
957
  command,
@@ -1059,10 +1211,10 @@ function normalizeTimeout(value: unknown, config: ReturnType<typeof getConfig>):
1059
1211
  function failBeforeExecution(taskId: string, taskDir: string, message: string, caughtError?: unknown): TaskRunResult {
1060
1212
  // Phase 9: Preserve PatchWardenError structured info in error.log
1061
1213
  const structuredError = caughtError ? errorPayload(caughtError) : { error: message };
1062
- writeFileSync(join(taskDir, "error.log"), JSON.stringify({
1214
+ atomicWriteJsonFileSync(join(taskDir, "error.log"), {
1063
1215
  summary: message,
1064
1216
  ...structuredError,
1065
- }, null, 2), "utf-8");
1217
+ });
1066
1218
  const current = readStatus(join(taskDir, "status.json"));
1067
1219
  const now = new Date().toISOString();
1068
1220
  const verify: VerifyReport = {
@@ -1070,17 +1222,17 @@ function failBeforeExecution(taskId: string, taskDir: string, message: string, c
1070
1222
  requested_commands: Array.isArray(current.verify_commands) ? current.verify_commands : [],
1071
1223
  commands: [],
1072
1224
  };
1073
- writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verify, null, 2), "utf-8");
1074
- writeFileSync(join(taskDir, "verify.log"), `Verification did not run: ${message}\n`, "utf-8");
1075
- writeFileSync(join(taskDir, "test.log"), `(not run)\nExit code: not run\n${message}\n`, "utf-8");
1076
- writeFileSync(join(taskDir, "git.diff"), "(task failed before change capture)\n", "utf-8");
1077
- writeFileSync(join(taskDir, "diff.patch"), "(task failed before change capture)\n", "utf-8");
1078
- writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
1225
+ atomicWriteJsonFileSync(join(taskDir, "verify.json"), verify);
1226
+ atomicWriteFileSync(join(taskDir, "verify.log"), `Verification did not run: ${message}\n`);
1227
+ atomicWriteFileSync(join(taskDir, "test.log"), `(not run)\nExit code: not run\n${message}\n`);
1228
+ atomicWriteFileSync(join(taskDir, "git.diff"), "(task failed before change capture)\n");
1229
+ atomicWriteFileSync(join(taskDir, "diff.patch"), "(task failed before change capture)\n");
1230
+ atomicWriteJsonFileSync(join(taskDir, "file-stats.json"), {
1079
1231
  task_id: taskId,
1080
1232
  additions: 0,
1081
1233
  deletions: 0,
1082
1234
  files: [],
1083
- }, null, 2), "utf-8");
1235
+ });
1084
1236
  const result = {
1085
1237
  task_id: taskId,
1086
1238
  status: "failed",
@@ -1106,8 +1258,8 @@ function failBeforeExecution(taskId: string, taskDir: string, message: string, c
1106
1258
  safe_followup_prompt: "Inspect result.json and error.log, correct the task metadata or configuration, and retry without widening repository scope.",
1107
1259
  next_steps: ["Fix task metadata or configuration and retry the task."],
1108
1260
  };
1109
- writeFileSync(join(taskDir, "result.json"), JSON.stringify(result, null, 2), "utf-8");
1110
- writeFileSync(join(taskDir, "result.md"), `# PatchWarden Task Result\n\n## Status\nfailed\n\n## Summary\n${message}\n`, "utf-8");
1261
+ atomicWriteJsonFileSync(join(taskDir, "result.json"), result);
1262
+ atomicWriteFileSync(join(taskDir, "result.md"), `# PatchWarden Task Result\n\n## Status\nfailed\n\n## Summary\n${message}\n`);
1111
1263
  updateStatus(taskDir, {
1112
1264
  status: "failed",
1113
1265
  phase: "failed",
@@ -1140,44 +1292,41 @@ function setTaskPhase(
1140
1292
 
1141
1293
  function updateStatus(taskDir: string, patch: Record<string, unknown>): void {
1142
1294
  const statusFile = join(taskDir, "status.json");
1143
- const current = readStatus(statusFile);
1144
- const next = { ...current, ...patch, updated_at: new Date().toISOString() };
1145
- writeFileSync(statusFile, JSON.stringify(next, null, 2), "utf-8");
1295
+ updateTaskStatusFile(statusFile, patch);
1146
1296
  }
1147
1297
 
1148
- function readStatus(statusFile: string): Record<string, any> {
1149
- return JSON.parse(readFileSync(statusFile, "utf-8"));
1298
+ function readStatus(statusFile: string): TaskStatusRecord {
1299
+ return readTaskStatusFile(statusFile);
1150
1300
  }
1151
1301
 
1152
1302
  function gracefulKill(child: ChildProcess): void {
1153
1303
  try {
1154
1304
  if (process.platform !== "win32" && child.pid) process.kill(-child.pid, "SIGTERM");
1155
1305
  else child.kill("SIGTERM");
1156
- } catch {}
1306
+ } catch {} // cleanup failure is safe to ignore
1157
1307
  }
1158
1308
 
1159
1309
  function forceKill(child: ChildProcess): void {
1160
1310
  if (!child.pid) return;
1161
1311
  try {
1162
1312
  if (process.platform === "win32") {
1163
- const result = spawnSync("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
1313
+ const systemRoot = process.env.SystemRoot || process.env.WINDIR || "C:\\Windows";
1314
+ const taskkill = resolveTrustedExecutable(`${systemRoot}\\System32\\taskkill.exe`, process.cwd());
1315
+ const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
1164
1316
  stdio: "ignore",
1165
1317
  timeout: 5000,
1166
1318
  windowsHide: true,
1319
+ env: buildChildEnvironment({ cwd: process.cwd() }),
1167
1320
  });
1168
1321
  if (result.status !== 0) child.kill("SIGKILL");
1169
1322
  } else {
1170
1323
  process.kill(-child.pid, "SIGKILL");
1171
1324
  }
1172
1325
  } catch {
1173
- try { child.kill("SIGKILL"); } catch {}
1326
+ try { child.kill("SIGKILL"); } catch {} // cleanup failure is safe to ignore
1174
1327
  }
1175
1328
  }
1176
1329
 
1177
- function openStream(path?: string): WriteStream | null {
1178
- return path ? createWriteStream(path, { flags: "a" }) : null;
1179
- }
1180
-
1181
1330
  function appendBounded(current: string, next: string): string {
1182
1331
  const combined = current + next;
1183
1332
  return combined.length <= MAX_CAPTURE_CHARS