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,22 +1,36 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
- import { createWriteStream, existsSync, readFileSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, readFileSync, } from "node:fs";
3
3
  import { isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { getTasksDir, getPlansDir, getConfig, resolveWorkspaceRoot } from "../config.js";
5
5
  import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
6
6
  import { guardTestCommand, } from "../security/commandGuard.js";
7
- import { writeTaskProgress } from "../taskProgress.js";
8
- import { writeTaskRuntime } from "../taskRuntime.js";
7
+ import { writeTaskProgress } from "./taskProgress.js";
8
+ import { writeTaskRuntime } from "./taskRuntime.js";
9
+ import { claimPendingTask, readTaskStatusFile, updateTaskStatusFile, } from "./taskStatusStore.js";
9
10
  import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
10
11
  import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js";
11
12
  import { buildChangeArtifacts, captureRepoSnapshot, compareSnapshots, emptyArtifactHygiene, writeSnapshot, extractExternalDirtyFiles, findNewExternalDirtyFiles, buildArtifactManifest, groupChangedFiles, } from "./changeCapture.js";
12
13
  import { errorPayload } from "../errors.js";
13
- import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
14
+ import { ARTIFACT_SCHEMA_VERSION } from "../version.js";
15
+ import { diagnoseAndroidBuild } from "../tools/workspace/androidDoctor.js";
14
16
  import { runPostTaskCleanup } from "./postTaskCleanup.js";
17
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
18
+ import { allowedEnvironmentValues, buildChildEnvironment, redactProcessOutput, resolvePackageManagerInvocation, resolveTrustedExecutable, SecureProcessLogCapture, } from "./processSecurity.js";
15
19
  const HEARTBEAT_INTERVAL_MS = 2000;
16
20
  const GRACEFUL_KILL_MS = 2000;
17
21
  const MAX_CAPTURE_CHARS = 100_000;
18
22
  const ARTIFACT_COLLECTION_TIMEOUT_MS = 60_000;
19
23
  export async function runTask(taskId) {
24
+ const prepared = await prepareTask(taskId);
25
+ if (!("taskDir" in prepared))
26
+ return prepared;
27
+ const ctx = prepared;
28
+ const state = await executeAgent(ctx);
29
+ await runVerification(ctx, state);
30
+ const evidence = await collectArtifacts(ctx, state);
31
+ return finalizeTask(ctx, state, evidence);
32
+ }
33
+ async function prepareTask(taskId) {
20
34
  const config = getConfig();
21
35
  const tasksDir = getTasksDir(config);
22
36
  const plansDir = getPlansDir(config);
@@ -26,7 +40,24 @@ export async function runTask(taskId) {
26
40
  const statusFile = join(taskDir, "status.json");
27
41
  if (!existsSync(statusFile))
28
42
  throw new Error(`Task not found: "${taskId}"`);
29
- const initialStatus = readStatus(statusFile);
43
+ const startedAtMs = Date.now();
44
+ const claim = claimPendingTask(statusFile, {
45
+ status: "running",
46
+ phase: "preparing",
47
+ started_at: new Date(startedAtMs).toISOString(),
48
+ last_heartbeat_at: new Date(startedAtMs).toISOString(),
49
+ current_command: null,
50
+ error: null,
51
+ });
52
+ if (!claim.claimed) {
53
+ const currentStatus = String(claim.status.status || "failed");
54
+ return {
55
+ task_id: taskId,
56
+ status: currentStatus,
57
+ error: `Task is ${currentStatus}; only pending tasks can be claimed for execution.`,
58
+ };
59
+ }
60
+ const initialStatus = claim.status;
30
61
  const planId = String(initialStatus.plan_id || "");
31
62
  const agentName = String(initialStatus.agent || "");
32
63
  const rawRepoPath = String(initialStatus.resolved_repo_path || initialStatus.repo_path || wsRoot);
@@ -40,7 +71,6 @@ export async function runTask(taskId) {
40
71
  catch (error) {
41
72
  return failBeforeExecution(taskId, taskDir, errorMessage(error), error);
42
73
  }
43
- const startedAtMs = Date.now();
44
74
  const deadlineMs = startedAtMs + timeoutSeconds * 1000;
45
75
  let repoPath;
46
76
  try {
@@ -57,11 +87,10 @@ export async function runTask(taskId) {
57
87
  const message = `verification metadata validation failed: ${errorMessage(error)}`;
58
88
  return failBeforeExecution(taskId, taskDir, message, error);
59
89
  }
60
- // ── Assessment freshness revalidation before execution ──
61
90
  const assessmentId = String(initialStatus.assessment_id || "");
62
91
  if (assessmentId) {
63
92
  try {
64
- const preExecSnapshot = captureRepoSnapshot(repoPath);
93
+ const preExecSnapshot = await captureRepoSnapshot(repoPath);
65
94
  const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
66
95
  if (!validation.valid) {
67
96
  const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
@@ -73,19 +102,7 @@ export async function runTask(taskId) {
73
102
  return failBeforeExecution(taskId, taskDir, message, error);
74
103
  }
75
104
  }
76
- updateStatus(taskDir, {
77
- status: "running",
78
- phase: "preparing",
79
- started_at: new Date(startedAtMs).toISOString(),
80
- last_heartbeat_at: new Date().toISOString(),
81
- timeout_seconds: timeoutSeconds,
82
- current_command: null,
83
- error: null,
84
- });
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.
105
+ updateStatus(taskDir, { timeout_seconds: timeoutSeconds });
89
106
  writeTaskRuntime(taskDir, {
90
107
  task_started_at: new Date(startedAtMs).toISOString(),
91
108
  watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
@@ -95,156 +112,160 @@ export async function runTask(taskId) {
95
112
  let beforeWorkspaceSnapshot;
96
113
  let externalDirtyBaseline = [];
97
114
  try {
98
- beforeSnapshot = captureRepoSnapshot(repoPath);
99
- beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
115
+ beforeSnapshot = await captureRepoSnapshot(repoPath);
116
+ beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : await captureRepoSnapshot(wsRoot);
100
117
  writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
101
118
  writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
102
- // Phase 4: Record external dirty files as baseline before task execution
103
119
  externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
104
120
  }
105
121
  catch (error) {
106
122
  const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
107
123
  return failBeforeExecution(taskId, taskDir, message, error);
108
124
  }
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;
125
+ return {
126
+ taskId, taskDir, statusFile, repoPath, wsRoot, config, plansDir, initialStatus,
127
+ planId, agentName, testCommand, changePolicy, verifyCommands, timeoutSeconds,
128
+ startedAtMs, deadlineMs, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
129
+ };
130
+ }
131
+ async function executeAgent(ctx) {
132
+ const state = {
133
+ agentResult: null,
134
+ testResult: skippedTest(ctx.testCommand, ctx.repoPath, "Agent did not complete successfully."),
135
+ verifyResults: [],
136
+ finalStatus: "failed",
137
+ finalError: null,
138
+ lastCaughtError: null,
139
+ };
115
140
  try {
116
- const planFile = resolve(plansDir, planId, "plan.md");
141
+ const planFile = resolve(ctx.plansDir, ctx.planId, "plan.md");
117
142
  if (!existsSync(planFile))
118
- throw new Error(`Plan not found: "${planId}". Save the plan first.`);
143
+ throw new Error(`Plan not found: "${ctx.planId}". Save the plan first.`);
119
144
  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({
145
+ const prompt = buildExecutionPrompt(planContent, ctx.repoPath, ctx.testCommand);
146
+ const invocation = buildAgentInvocation(ctx.agentName, ctx.repoPath, prompt, ctx.config);
147
+ setTaskPhase(ctx.taskDir, "executing_agent", invocation.commandLabel);
148
+ state.agentResult = await runManagedProcess({
125
149
  command: invocation.command,
126
150
  args: invocation.args,
127
- cwd: repoPath,
128
- taskDir,
129
- statusFile,
151
+ cwd: ctx.repoPath,
152
+ taskDir: ctx.taskDir,
153
+ statusFile: ctx.statusFile,
130
154
  phase: "executing_agent",
131
- currentCommand: agentCommandLabel,
132
- deadlineMs,
133
- stdoutPath: join(taskDir, "stdout.log"),
134
- stderrPath: join(taskDir, "stderr.log"),
155
+ currentCommand: invocation.commandLabel,
156
+ deadlineMs: ctx.deadlineMs,
157
+ stdoutPath: join(ctx.taskDir, "stdout.log"),
158
+ stderrPath: join(ctx.taskDir, "stderr.log"),
159
+ environmentVariableNames: invocation.environmentVariableNames,
160
+ blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
135
161
  });
162
+ const agentResult = state.agentResult;
136
163
  if (agentResult.terminationReason === "canceled" || agentResult.terminationReason === "killed") {
137
- finalStatus = "canceled";
138
- finalError = agentResult.terminationReason === "killed"
164
+ state.finalStatus = "canceled";
165
+ state.finalError = agentResult.terminationReason === "killed"
139
166
  ? "Task was terminated by kill_task."
140
167
  : "Task was canceled by user request.";
141
168
  }
142
169
  else if (agentResult.terminationReason === "timeout") {
143
- finalError = `Task timed out after ${timeoutSeconds} seconds during agent execution.`;
170
+ state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during agent execution.`;
144
171
  }
145
172
  else if (agentResult.spawnError) {
146
- finalError = `Agent spawn failed: ${agentResult.spawnError}`;
173
+ state.finalError = `Agent spawn failed: ${agentResult.spawnError}`;
147
174
  }
148
175
  else if (agentResult.exitCode !== 0) {
149
- finalError = `Agent exited with code ${agentResult.exitCode}.`;
176
+ state.finalError = `Agent exited with code ${agentResult.exitCode}.`;
150
177
  }
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)
178
+ }
179
+ catch (error) {
180
+ state.lastCaughtError = error;
181
+ state.finalError = errorMessage(error);
182
+ }
183
+ return state;
184
+ }
185
+ async function runVerification(ctx, state) {
186
+ if (state.finalError !== null)
187
+ return state;
188
+ try {
189
+ if (ctx.verifyCommands.length > 0) {
190
+ for (const command of ctx.verifyCommands) {
191
+ setTaskPhase(ctx.taskDir, "running_tests", command);
192
+ const verification = await runTrustedTestCommand(command, ctx.repoPath, ctx.taskDir, ctx.statusFile, ctx.deadlineMs);
193
+ state.verifyResults.push(verification);
194
+ if (verification.terminationReason || Date.now() >= ctx.deadlineMs)
157
195
  break;
158
196
  }
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);
197
+ state.testResult = state.verifyResults[0] || skippedTest(ctx.testCommand, ctx.repoPath, "No verification command ran.");
198
+ const interrupted = state.verifyResults.find((result) => result.terminationReason);
199
+ const failedVerification = state.verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
162
200
  if (interrupted?.terminationReason === "canceled" || interrupted?.terminationReason === "killed") {
163
- finalStatus = "canceled";
164
- finalError = interrupted.terminationReason === "killed"
201
+ state.finalStatus = "canceled";
202
+ state.finalError = interrupted.terminationReason === "killed"
165
203
  ? "Task was terminated by kill_task during verification."
166
204
  : "Task was canceled during verification.";
167
205
  }
168
- else if (interrupted?.terminationReason === "timeout" || Date.now() >= deadlineMs) {
169
- finalError = `Task timed out after ${timeoutSeconds} seconds during verification.`;
206
+ else if (interrupted?.terminationReason === "timeout" || Date.now() >= ctx.deadlineMs) {
207
+ state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during verification.`;
170
208
  }
171
209
  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}.`;
210
+ state.finalStatus = "failed_verification";
211
+ state.finalError = failedVerification.spawnError
212
+ ? `Verification command "${failedVerification.command}" could not start: ${failedVerification.spawnError}`
213
+ : `Verification command "${failedVerification.command}" exited with code ${failedVerification.exitCode}.`;
176
214
  }
177
- else if (verifyResults.length === verifyCommands.length) {
178
- finalStatus = "done_by_agent";
215
+ else if (state.verifyResults.length === ctx.verifyCommands.length) {
216
+ state.finalStatus = "done_by_agent";
179
217
  }
180
218
  else {
181
- finalError = "Verification did not complete all configured commands.";
219
+ state.finalError = "Verification did not complete all configured commands.";
182
220
  }
183
221
  }
184
222
  else {
185
- testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
186
- finalStatus = "done_by_agent";
223
+ state.testResult = skippedTest(ctx.testCommand, ctx.repoPath, "No verification command configured.");
224
+ state.finalStatus = "done_by_agent";
187
225
  }
188
226
  }
189
227
  catch (error) {
190
- lastCaughtError = error;
191
- finalError = errorMessage(error);
228
+ state.lastCaughtError = error;
229
+ state.finalError = errorMessage(error);
192
230
  }
193
- let cleanupReport = {
194
- enabled: true,
195
- removed: [],
196
- skipped: [],
197
- source_files_touched: 0,
198
- };
199
- if (finalStatus !== "canceled") {
231
+ return state;
232
+ }
233
+ async function collectArtifacts(ctx, state) {
234
+ let cleanupReport = { enabled: true, removed: [], skipped: [], source_files_touched: 0 };
235
+ if (state.finalStatus !== "canceled") {
200
236
  try {
201
- cleanupReport = runPostTaskCleanup(repoPath, taskDir);
202
- updateStatus(taskDir, { cleanup: cleanupReport });
237
+ cleanupReport = runPostTaskCleanup(ctx.repoPath, ctx.taskDir);
238
+ updateStatus(ctx.taskDir, { cleanup: cleanupReport });
203
239
  }
204
240
  catch (error) {
205
241
  cleanupReport = {
206
- enabled: true,
207
- removed: [],
242
+ enabled: true, removed: [],
208
243
  skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
209
244
  source_files_touched: 0,
210
245
  };
211
- writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
212
- updateStatus(taskDir, { cleanup: cleanupReport });
246
+ atomicWriteJsonFileSync(join(ctx.taskDir, "post-task-cleanup.json"), cleanupReport);
247
+ updateStatus(ctx.taskDir, { cleanup: cleanupReport });
213
248
  }
214
249
  }
215
- setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
250
+ setTaskPhase(ctx.taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
216
251
  const artifactCollectionStartedAt = new Date().toISOString();
217
252
  let changes;
218
253
  let artifactStatus = "collected";
219
254
  let artifactCollectionError = null;
220
255
  let timeoutHandle;
256
+ const artifactController = new AbortController();
221
257
  try {
222
- // Phase 5: Wrap artifact collection with a timeout
223
- 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);
228
- }),
229
- new Promise((_, reject) => {
230
- timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
231
- }),
232
- ]);
233
- if (timeoutHandle)
234
- clearTimeout(timeoutHandle);
235
- changes = collectionResult;
258
+ timeoutHandle = setTimeout(() => {
259
+ artifactController.abort(new Error("Artifact collection timed out"));
260
+ }, ARTIFACT_COLLECTION_TIMEOUT_MS);
261
+ const afterSnapshot = await captureRepoSnapshot(ctx.repoPath, artifactController.signal);
262
+ writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
263
+ changes = await buildChangeArtifacts(ctx.repoPath, ctx.beforeSnapshot, afterSnapshot, artifactController.signal);
236
264
  }
237
265
  catch (error) {
238
- if (timeoutHandle)
239
- clearTimeout(timeoutHandle);
240
- lastCaughtError = error;
266
+ state.lastCaughtError = error;
241
267
  artifactCollectionError = errorMessage(error);
242
- if (artifactCollectionError.includes("timed out")) {
243
- artifactStatus = "timeout";
244
- }
245
- else {
246
- artifactStatus = "failed";
247
- }
268
+ artifactStatus = artifactCollectionError.includes("timed out") ? "timeout" : "failed";
248
269
  changes = {
249
270
  changed_files: [],
250
271
  diff: `(change capture failed: ${artifactCollectionError})\n`,
@@ -254,17 +275,16 @@ export async function runTask(taskId) {
254
275
  additions: 0,
255
276
  deletions: 0,
256
277
  file_stats: [],
257
- workspace_dirty_before: beforeSnapshot.workspace_dirty,
258
- workspace_dirty_after: beforeSnapshot.workspace_dirty,
278
+ workspace_dirty_before: ctx.beforeSnapshot.workspace_dirty,
279
+ workspace_dirty_after: ctx.beforeSnapshot.workspace_dirty,
259
280
  patch_mode: "hash_only",
260
281
  unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
261
282
  artifact_hygiene: emptyArtifactHygiene(),
262
283
  };
263
- // Phase 5: Write partial_result.md when artifact collection fails
264
- writeFileSync(join(taskDir, "partial_result.md"), [
284
+ atomicWriteFileSync(join(ctx.taskDir, "partial_result.md"), [
265
285
  "# PatchWarden Partial Result",
266
286
  "",
267
- `Task: ${taskId}`,
287
+ `Task: ${ctx.taskId}`,
268
288
  "",
269
289
  "## Artifact Collection Status",
270
290
  artifactStatus,
@@ -273,108 +293,121 @@ export async function runTask(taskId) {
273
293
  artifactCollectionError,
274
294
  "",
275
295
  "## Agent Status",
276
- agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
296
+ state.agentResult ? `Exit code: ${state.agentResult.exitCode}` : "Agent did not run",
277
297
  "",
278
298
  "## Verification Status",
279
- verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
299
+ state.verifyResults.length > 0 ? state.verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
280
300
  "",
281
301
  "## Note",
282
302
  "Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
283
303
  "",
284
- ].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";
304
+ ].join("\n"));
305
+ if (state.finalStatus === "done_by_agent") {
306
+ state.finalError = `Change capture failed: ${artifactCollectionError}`;
307
+ state.finalStatus = "failed";
289
308
  }
290
309
  else {
291
- finalError ||= `Change capture failed: ${artifactCollectionError}`;
310
+ state.finalError ||= `Change capture failed: ${artifactCollectionError}`;
292
311
  }
293
312
  }
313
+ finally {
314
+ if (timeoutHandle)
315
+ clearTimeout(timeoutHandle);
316
+ }
294
317
  const artifactCollectionFinishedAt = new Date().toISOString();
295
318
  let outOfScopeChanges = [];
296
- let preexistingExternalDirty = externalDirtyBaseline;
319
+ const preexistingExternalDirty = ctx.externalDirtyBaseline;
297
320
  let newOutOfScopeChanges = [];
298
321
  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)));
322
+ const afterWorkspaceSnapshot = ctx.repoPath === ctx.wsRoot ? await captureRepoSnapshot(ctx.repoPath) : await captureRepoSnapshot(ctx.wsRoot);
323
+ writeSnapshot(ctx.taskDir, "workspace-after.json", afterWorkspaceSnapshot);
324
+ const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, ctx.repoPath, ctx.wsRoot);
325
+ newOutOfScopeChanges = findNewExternalDirtyFiles(ctx.externalDirtyBaseline, allExternalDirty);
326
+ outOfScopeChanges = compareSnapshots(ctx.beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
327
+ .filter((file) => !isPathInside(resolve(ctx.wsRoot, file.path), ctx.repoPath) ||
328
+ Boolean(file.old_path && !isPathInside(resolve(ctx.wsRoot, file.old_path), ctx.repoPath)));
307
329
  }
308
330
  catch (error) {
309
- finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
310
- if (finalStatus === "done_by_agent")
311
- finalStatus = "failed";
331
+ state.finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
332
+ if (state.finalStatus === "done_by_agent")
333
+ state.finalStatus = "failed";
312
334
  }
313
- // Phase 4: Pre-existing external dirty files are warnings, not failures
314
335
  const preexistingWarnings = preexistingExternalDirty.length > 0
315
336
  ? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
316
337
  : [];
338
+ applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty);
339
+ atomicWriteFileSync(join(ctx.taskDir, "git.diff"), changes.diff);
340
+ atomicWriteFileSync(join(ctx.taskDir, "diff.patch"), changes.diff);
341
+ atomicWriteJsonFileSync(join(ctx.taskDir, "changed-files.json"), { schema_version: ARTIFACT_SCHEMA_VERSION, ...changes });
342
+ const artifactManifest = await buildArtifactManifest(changes.changed_files, ctx.repoPath, ctx.taskId);
343
+ atomicWriteJsonFileSync(join(ctx.taskDir, "artifact_manifest.json"), artifactManifest);
344
+ const changedFileGroups = groupChangedFiles(changes.changed_files);
345
+ atomicWriteJsonFileSync(join(ctx.taskDir, "file-stats.json"), {
346
+ task_id: ctx.taskId,
347
+ additions: changes.additions,
348
+ deletions: changes.deletions,
349
+ files: changes.file_stats,
350
+ });
351
+ return {
352
+ changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
353
+ outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
354
+ artifactManifest, changedFileGroups,
355
+ };
356
+ }
357
+ function applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty) {
317
358
  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,
359
+ state.finalStatus = "failed_scope_violation";
360
+ state.finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
361
+ atomicWriteJsonFileSync(join(ctx.taskDir, "rollback-plan.json"), {
362
+ task_id: ctx.taskId,
322
363
  status: "review_required",
323
364
  automatic_rollback_performed: false,
324
365
  warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
325
366
  out_of_scope_changes: newOutOfScopeChanges,
326
367
  preexisting_external_dirty_files: preexistingExternalDirty,
327
- }, null, 2), "utf-8");
328
- writeFileSync(join(taskDir, "rollback_scope_violation_plan.md"), [
368
+ });
369
+ atomicWriteFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
329
370
  "# Scope Violation Rollback Plan",
330
371
  "",
331
- `Task: ${taskId}`,
372
+ `Task: ${ctx.taskId}`,
332
373
  "",
333
374
  "PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
334
375
  "",
335
376
  "## New out-of-scope files (caused by this task)",
336
377
  ...newOutOfScopeChanges.map((file) => `- ${file.change}: ${file.path}`),
337
378
  "",
338
- ].join("\n"), "utf-8");
379
+ ].join("\n"));
339
380
  }
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).`;
381
+ else if (changes.diff_redacted) {
382
+ state.finalStatus = "failed_policy_violation";
383
+ state.finalError = "Credential-like content was detected in the task diff. PatchWarden redacted the evidence; remove the sensitive content before retrying.";
343
384
  }
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.
385
+ else if (ctx.changePolicy === "no_changes" && changes.changed_files.length > 0) {
386
+ state.finalStatus = "failed_policy_violation";
387
+ state.finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
388
+ }
389
+ }
390
+ function finalizeTask(ctx, state, evidence) {
391
+ const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport, artifactManifest, changedFileGroups } = evidence;
392
+ atomicWriteFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult));
393
+ const verifyJson = buildVerifyJson(ctx.verifyCommands, state.verifyResults, ctx.repoPath);
362
394
  if (newOutOfScopeChanges.length > 0) {
363
395
  verifyJson.status = "failed";
364
396
  verifyJson.failure_reason = "scope_violation";
365
397
  }
366
- else if (finalStatus === "failed_policy_violation") {
398
+ else if (state.finalStatus === "failed_policy_violation") {
367
399
  verifyJson.status = "failed";
368
- verifyJson.failure_reason = "change_policy_violation";
400
+ verifyJson.failure_reason = changes.diff_redacted
401
+ ? "sensitive_content_violation"
402
+ : "change_policy_violation";
369
403
  }
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);
404
+ atomicWriteJsonFileSync(join(ctx.taskDir, "verify.json"), verifyJson);
405
+ atomicWriteFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands));
406
+ if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus))
407
+ state.finalStatus = "failed";
408
+ const finalPhase = state.finalStatus === "done_by_agent" ? "done_by_agent" : state.finalStatus;
409
+ const followup = buildFailureFollowup(state.finalStatus, state.finalError, verifyJson.commands);
410
+ const androidDiagnostic = diagnoseAndroidBuild(ctx.repoPath);
378
411
  let androidWarning = null;
379
412
  if (androidDiagnostic.status !== "skip") {
380
413
  if (androidDiagnostic.status === "fail") {
@@ -388,13 +421,13 @@ export async function runTask(taskId) {
388
421
  }
389
422
  }
390
423
  const resultMd = buildResultMarkdown({
391
- taskId,
392
- planId,
393
- agent: agentName,
394
- status: finalStatus,
395
- error: finalError,
396
- agentResult,
397
- testResult,
424
+ taskId: ctx.taskId,
425
+ planId: ctx.planId,
426
+ agent: ctx.agentName,
427
+ status: state.finalStatus,
428
+ error: state.finalError,
429
+ agentResult: state.agentResult,
430
+ testResult: state.testResult,
398
431
  verify: verifyJson,
399
432
  changes,
400
433
  outOfScopeChanges,
@@ -405,18 +438,75 @@ export async function runTask(taskId) {
405
438
  androidWarning,
406
439
  preexistingWarnings,
407
440
  });
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.",
441
+ atomicWriteFileSync(join(ctx.taskDir, "result.md"), resultMd);
442
+ const resultJson = buildResultJson({
443
+ ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning,
444
+ });
445
+ atomicWriteJsonFileSync(join(ctx.taskDir, "result.json"), resultJson);
446
+ if (state.finalError) {
447
+ const structuredError = errorPayload(state.lastCaughtError);
448
+ atomicWriteJsonFileSync(join(ctx.taskDir, "error.log"), {
449
+ summary: state.finalError,
450
+ ...structuredError,
451
+ });
452
+ }
453
+ const finishedAt = new Date().toISOString();
454
+ updateStatus(ctx.taskDir, {
455
+ status: state.finalStatus,
456
+ phase: finalPhase,
457
+ current_command: null,
458
+ last_heartbeat_at: finishedAt,
459
+ finished_at: finishedAt,
460
+ error: state.finalError,
461
+ changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
462
+ artifact_hygiene_counts: changes.artifact_hygiene.counts,
463
+ artifact_status: artifactStatus,
464
+ artifact_collection_error: artifactCollectionError,
465
+ artifact_collection_started_at: artifactCollectionStartedAt,
466
+ artifact_collection_finished_at: artifactCollectionFinishedAt,
467
+ cleanup: cleanupReport,
468
+ out_of_scope_changes: outOfScopeChanges,
469
+ new_out_of_scope_changes: newOutOfScopeChanges,
470
+ preexisting_external_dirty_files: preexistingExternalDirty,
471
+ verify_status: verifyJson.status,
472
+ verify_commands: ctx.verifyCommands,
473
+ diff_available: changes.diff_available,
474
+ diff_truncated: changes.diff_truncated,
475
+ diff_redacted: changes.diff_redacted === true,
476
+ diff_redaction_categories: changes.diff_redaction_categories ?? [],
477
+ workspace_dirty_before: changes.workspace_dirty_before,
478
+ workspace_dirty_after: changes.workspace_dirty_after,
479
+ workspace_dirty: changes.workspace_dirty_after,
480
+ acceptance_status: state.finalStatus === "done_by_agent" ? "pending" : null,
481
+ });
482
+ writeTaskRuntime(ctx.taskDir, {
483
+ phase: finalPhase,
484
+ current_command: null,
485
+ last_heartbeat_at: finishedAt,
486
+ runner_pid: process.pid,
487
+ child_pid: undefined,
488
+ });
489
+ writeTaskProgress(ctx.taskDir, finalPhase, {
490
+ heartbeatAt: finishedAt,
491
+ note: state.finalError || `Task finished with status ${state.finalStatus}.`,
492
+ });
493
+ return { task_id: ctx.taskId, status: state.finalStatus, error: state.finalError };
494
+ }
495
+ function buildResultJson(input) {
496
+ const { ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning } = input;
497
+ const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, cleanupReport, artifactManifest, changedFileGroups } = evidence;
498
+ return {
499
+ schema_version: ARTIFACT_SCHEMA_VERSION,
500
+ task_id: ctx.taskId,
501
+ status: state.finalStatus,
502
+ agent: ctx.agentName,
503
+ workspace_root: ctx.wsRoot,
504
+ repo_path: ctx.initialStatus.repo_path,
505
+ resolved_repo_path: ctx.repoPath,
506
+ plan_source: ctx.initialStatus.plan_source || "saved",
507
+ template: ctx.initialStatus.template || null,
508
+ change_policy: ctx.changePolicy,
509
+ summary: state.finalError || "Agent execution and configured verification completed successfully.",
420
510
  changed_files: changes.changed_files,
421
511
  changed_file_groups: {
422
512
  source_changes: changedFileGroups.source_changes.length,
@@ -454,68 +544,24 @@ export async function runTask(taskId) {
454
544
  ...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
455
545
  ],
456
546
  warnings: [
457
- ...beforeSnapshot.warnings,
547
+ ...ctx.beforeSnapshot.warnings,
458
548
  ...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
459
- ...preexistingWarnings,
549
+ ...(changes.diff_redacted ? [
550
+ `Credential-like diff content was redacted (${(changes.diff_redaction_categories ?? []).join(", ") || "sensitive_content"}).`,
551
+ ] : []),
552
+ ...evidence.preexistingWarnings,
460
553
  ...(androidWarning ? [androidWarning] : []),
461
554
  ],
462
- errors: finalError ? [finalError] : [],
463
- known_issues: finalError ? [finalError] : [],
555
+ errors: state.finalError ? [state.finalError] : [],
556
+ known_issues: state.finalError ? [state.finalError] : [],
464
557
  failure_reason: followup.failure_reason,
465
558
  failed_command: followup.failed_command,
466
559
  suggested_next_action: followup.suggested_next_action,
467
560
  safe_followup_prompt: followup.safe_followup_prompt,
468
- next_steps: finalStatus === "done_by_agent"
561
+ next_steps: state.finalStatus === "done_by_agent"
469
562
  ? ["Review get_task_summary and audit_task before accepting the work."]
470
563
  : ["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 };
564
+ };
519
565
  }
520
566
  function buildFailureFollowup(status, error, commands) {
521
567
  const failed = commands.find((command) => command.status !== "passed");
@@ -570,13 +616,22 @@ async function runManagedProcess(options) {
570
616
  if (Date.now() >= options.deadlineMs) {
571
617
  return { exitCode: null, stdout: "", stderr: "", spawnError: null, terminationReason: "timeout" };
572
618
  }
619
+ const exactRedactionValues = allowedEnvironmentValues(options.environmentVariableNames);
620
+ const logCapture = new SecureProcessLogCapture([options.stdoutPath, options.stderrPath], options.maxLogBytes);
573
621
  let child;
574
622
  try {
575
- child = spawn(options.command, options.args, {
623
+ const env = buildChildEnvironment({
624
+ cwd: options.cwd,
625
+ allowedNames: options.environmentVariableNames,
626
+ blockedNames: options.blockedEnvironmentVariableNames,
627
+ });
628
+ const command = resolveTrustedExecutable(options.command, options.cwd, { pathValue: env.PATH });
629
+ child = spawn(command, options.args, {
576
630
  cwd: options.cwd,
577
631
  stdio: ["ignore", "pipe", "pipe"],
578
632
  detached: process.platform !== "win32",
579
633
  windowsHide: true,
634
+ env,
580
635
  });
581
636
  }
582
637
  catch (error) {
@@ -588,8 +643,6 @@ async function runManagedProcess(options) {
588
643
  child_pid: child.pid,
589
644
  child_started_at: new Date().toISOString(),
590
645
  });
591
- const stdoutStream = openStream(options.stdoutPath);
592
- const stderrStream = openStream(options.stderrPath);
593
646
  let stdout = "";
594
647
  let stderr = "";
595
648
  let spawnError = null;
@@ -643,11 +696,11 @@ async function runManagedProcess(options) {
643
696
  try {
644
697
  forceKill(child);
645
698
  }
646
- catch { }
699
+ catch { } // cleanup failure is safe to ignore
647
700
  try {
648
701
  child.kill("SIGKILL");
649
702
  }
650
- catch { }
703
+ catch { } // cleanup failure is safe to ignore
651
704
  // Resolve the exit promise via the shared finish handle
652
705
  if (childExitFinish)
653
706
  childExitFinish(null);
@@ -656,12 +709,12 @@ async function runManagedProcess(options) {
656
709
  child.stdout?.on("data", (chunk) => {
657
710
  const text = chunk.toString("utf-8");
658
711
  stdout = appendBounded(stdout, text);
659
- stdoutStream?.write(text);
712
+ logCapture.append(options.stdoutPath, chunk);
660
713
  });
661
714
  child.stderr?.on("data", (chunk) => {
662
715
  const text = chunk.toString("utf-8");
663
716
  stderr = appendBounded(stderr, text);
664
- stderrStream?.write(text);
717
+ logCapture.append(options.stderrPath, chunk);
665
718
  });
666
719
  heartbeat();
667
720
  const heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL_MS);
@@ -678,12 +731,6 @@ async function runManagedProcess(options) {
678
731
  };
679
732
  childExitFinish = finish;
680
733
  child.once("close", (code) => finish(code));
681
- child.once("exit", (code) => {
682
- // On Windows, "close" can lag behind "exit" when stdio pipes drain
683
- // after taskkill. Resolve on exit to avoid hanging the runner, but
684
- // still let "close" fire if it arrives first.
685
- finish(code);
686
- });
687
734
  child.once("error", (error) => {
688
735
  spawnError = error.message;
689
736
  finish(null);
@@ -692,8 +739,7 @@ async function runManagedProcess(options) {
692
739
  clearInterval(heartbeatTimer);
693
740
  if (forceTimer)
694
741
  clearTimeout(forceTimer);
695
- stdoutStream?.destroy();
696
- stderrStream?.destroy();
742
+ logCapture.flush(exactRedactionValues);
697
743
  writeTaskRuntime(options.taskDir, {
698
744
  phase: options.phase,
699
745
  last_heartbeat_at: new Date().toISOString(),
@@ -701,7 +747,15 @@ async function runManagedProcess(options) {
701
747
  runner_pid: process.pid,
702
748
  child_pid: undefined,
703
749
  });
704
- return { exitCode, stdout, stderr, spawnError, terminationReason };
750
+ const safeStdout = redactProcessOutput(stdout, exactRedactionValues);
751
+ const safeStderr = redactProcessOutput(stderr, exactRedactionValues);
752
+ return {
753
+ exitCode,
754
+ stdout: safeStdout.length > MAX_CAPTURE_CHARS ? safeStdout.slice(-MAX_CAPTURE_CHARS) : safeStdout,
755
+ stderr: safeStderr.length > MAX_CAPTURE_CHARS ? safeStderr.slice(-MAX_CAPTURE_CHARS) : safeStderr,
756
+ spawnError,
757
+ terminationReason,
758
+ };
705
759
  }
706
760
  async function runTrustedTestCommand(testCommand, repoPath, taskDir, statusFile, deadlineMs) {
707
761
  const config = getConfig();
@@ -712,9 +766,9 @@ async function runTrustedTestCommand(testCommand, repoPath, taskDir, statusFile,
712
766
  let command = parts[0];
713
767
  let args = parts.slice(1);
714
768
  if (process.platform === "win32" && /^(npm|npm\.cmd|pnpm|pnpm\.cmd)$/i.test(command)) {
715
- const shim = /^pnpm/i.test(command) ? "pnpm.cmd" : "npm.cmd";
716
- command = process.env.ComSpec || "cmd.exe";
717
- args = ["/d", "/s", "/c", shim, ...args];
769
+ const packageManager = resolvePackageManagerInvocation(command, repoPath);
770
+ command = packageManager.command;
771
+ args = [...packageManager.argsPrefix, ...args];
718
772
  }
719
773
  const result = await runManagedProcess({
720
774
  command,
@@ -939,10 +993,10 @@ function normalizeTimeout(value, config) {
939
993
  function failBeforeExecution(taskId, taskDir, message, caughtError) {
940
994
  // Phase 9: Preserve PatchWardenError structured info in error.log
941
995
  const structuredError = caughtError ? errorPayload(caughtError) : { error: message };
942
- writeFileSync(join(taskDir, "error.log"), JSON.stringify({
996
+ atomicWriteJsonFileSync(join(taskDir, "error.log"), {
943
997
  summary: message,
944
998
  ...structuredError,
945
- }, null, 2), "utf-8");
999
+ });
946
1000
  const current = readStatus(join(taskDir, "status.json"));
947
1001
  const now = new Date().toISOString();
948
1002
  const verify = {
@@ -950,17 +1004,17 @@ function failBeforeExecution(taskId, taskDir, message, caughtError) {
950
1004
  requested_commands: Array.isArray(current.verify_commands) ? current.verify_commands : [],
951
1005
  commands: [],
952
1006
  };
953
- writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verify, null, 2), "utf-8");
954
- writeFileSync(join(taskDir, "verify.log"), `Verification did not run: ${message}\n`, "utf-8");
955
- writeFileSync(join(taskDir, "test.log"), `(not run)\nExit code: not run\n${message}\n`, "utf-8");
956
- writeFileSync(join(taskDir, "git.diff"), "(task failed before change capture)\n", "utf-8");
957
- writeFileSync(join(taskDir, "diff.patch"), "(task failed before change capture)\n", "utf-8");
958
- writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
1007
+ atomicWriteJsonFileSync(join(taskDir, "verify.json"), verify);
1008
+ atomicWriteFileSync(join(taskDir, "verify.log"), `Verification did not run: ${message}\n`);
1009
+ atomicWriteFileSync(join(taskDir, "test.log"), `(not run)\nExit code: not run\n${message}\n`);
1010
+ atomicWriteFileSync(join(taskDir, "git.diff"), "(task failed before change capture)\n");
1011
+ atomicWriteFileSync(join(taskDir, "diff.patch"), "(task failed before change capture)\n");
1012
+ atomicWriteJsonFileSync(join(taskDir, "file-stats.json"), {
959
1013
  task_id: taskId,
960
1014
  additions: 0,
961
1015
  deletions: 0,
962
1016
  files: [],
963
- }, null, 2), "utf-8");
1017
+ });
964
1018
  const result = {
965
1019
  task_id: taskId,
966
1020
  status: "failed",
@@ -986,8 +1040,8 @@ function failBeforeExecution(taskId, taskDir, message, caughtError) {
986
1040
  safe_followup_prompt: "Inspect result.json and error.log, correct the task metadata or configuration, and retry without widening repository scope.",
987
1041
  next_steps: ["Fix task metadata or configuration and retry the task."],
988
1042
  };
989
- writeFileSync(join(taskDir, "result.json"), JSON.stringify(result, null, 2), "utf-8");
990
- writeFileSync(join(taskDir, "result.md"), `# PatchWarden Task Result\n\n## Status\nfailed\n\n## Summary\n${message}\n`, "utf-8");
1043
+ atomicWriteJsonFileSync(join(taskDir, "result.json"), result);
1044
+ atomicWriteFileSync(join(taskDir, "result.md"), `# PatchWarden Task Result\n\n## Status\nfailed\n\n## Summary\n${message}\n`);
991
1045
  updateStatus(taskDir, {
992
1046
  status: "failed",
993
1047
  phase: "failed",
@@ -1013,12 +1067,10 @@ function setTaskPhase(taskDir, phase, currentCommand, note) {
1013
1067
  }
1014
1068
  function updateStatus(taskDir, patch) {
1015
1069
  const statusFile = join(taskDir, "status.json");
1016
- const current = readStatus(statusFile);
1017
- const next = { ...current, ...patch, updated_at: new Date().toISOString() };
1018
- writeFileSync(statusFile, JSON.stringify(next, null, 2), "utf-8");
1070
+ updateTaskStatusFile(statusFile, patch);
1019
1071
  }
1020
1072
  function readStatus(statusFile) {
1021
- return JSON.parse(readFileSync(statusFile, "utf-8"));
1073
+ return readTaskStatusFile(statusFile);
1022
1074
  }
1023
1075
  function gracefulKill(child) {
1024
1076
  try {
@@ -1027,17 +1079,20 @@ function gracefulKill(child) {
1027
1079
  else
1028
1080
  child.kill("SIGTERM");
1029
1081
  }
1030
- catch { }
1082
+ catch { } // cleanup failure is safe to ignore
1031
1083
  }
1032
1084
  function forceKill(child) {
1033
1085
  if (!child.pid)
1034
1086
  return;
1035
1087
  try {
1036
1088
  if (process.platform === "win32") {
1037
- const result = spawnSync("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
1089
+ const systemRoot = process.env.SystemRoot || process.env.WINDIR || "C:\\Windows";
1090
+ const taskkill = resolveTrustedExecutable(`${systemRoot}\\System32\\taskkill.exe`, process.cwd());
1091
+ const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
1038
1092
  stdio: "ignore",
1039
1093
  timeout: 5000,
1040
1094
  windowsHide: true,
1095
+ env: buildChildEnvironment({ cwd: process.cwd() }),
1041
1096
  });
1042
1097
  if (result.status !== 0)
1043
1098
  child.kill("SIGKILL");
@@ -1050,12 +1105,9 @@ function forceKill(child) {
1050
1105
  try {
1051
1106
  child.kill("SIGKILL");
1052
1107
  }
1053
- catch { }
1108
+ catch { } // cleanup failure is safe to ignore
1054
1109
  }
1055
1110
  }
1056
- function openStream(path) {
1057
- return path ? createWriteStream(path, { flags: "a" }) : null;
1058
- }
1059
1111
  function appendBounded(current, next) {
1060
1112
  const combined = current + next;
1061
1113
  return combined.length <= MAX_CAPTURE_CHARS