patchwarden 1.5.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (591) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +158 -26
  3. package/README.md +124 -24
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
  6. package/dist/assessments/assessmentDiagnostics.js +132 -0
  7. package/dist/assessments/assessmentStore.d.ts +39 -0
  8. package/dist/assessments/assessmentStore.js +207 -45
  9. package/dist/assessments/securitySnapshot.d.ts +50 -0
  10. package/dist/assessments/securitySnapshot.js +158 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +66 -13
  13. package/dist/control/fileManager.d.ts +2 -0
  14. package/dist/control/fileManager.js +39 -0
  15. package/dist/control/middleware/auth.d.ts +6 -0
  16. package/dist/control/middleware/auth.js +14 -5
  17. package/dist/control/middleware/static.js +16 -3
  18. package/dist/control/routeTable.d.ts +24 -0
  19. package/dist/control/routeTable.js +263 -0
  20. package/dist/control/routes/audit.js +17 -7
  21. package/dist/control/routes/evidence.js +1 -1
  22. package/dist/control/routes/lineage.js +1 -1
  23. package/dist/control/routes/process.d.ts +7 -0
  24. package/dist/control/routes/process.js +329 -42
  25. package/dist/control/routes/sessions.d.ts +9 -1
  26. package/dist/control/routes/sessions.js +84 -21
  27. package/dist/control/routes/status.d.ts +60 -1
  28. package/dist/control/routes/status.js +94 -15
  29. package/dist/control/routes/taskActions.js +24 -60
  30. package/dist/control/routes/tasks.d.ts +4 -0
  31. package/dist/control/routes/tasks.js +61 -62
  32. package/dist/control/routes/workspace.js +50 -23
  33. package/dist/control/runtime.d.ts +3 -2
  34. package/dist/control/runtime.js +65 -40
  35. package/dist/control/server.js +37 -365
  36. package/dist/control/shared.d.ts +25 -1
  37. package/dist/control/shared.js +64 -11
  38. package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
  39. package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
  40. package/dist/direct/directAudit.js +234 -225
  41. package/dist/direct/directGuards.d.ts +2 -1
  42. package/dist/direct/directGuards.js +42 -29
  43. package/dist/direct/directPatch.d.ts +6 -1
  44. package/dist/direct/directPatch.js +55 -8
  45. package/dist/direct/directSessionStore.d.ts +16 -6
  46. package/dist/direct/directSessionStore.js +128 -35
  47. package/dist/direct/directVerification.js +8 -16
  48. package/dist/doctor.js +65 -57
  49. package/dist/goal/acceptanceEngine.d.ts +1 -1
  50. package/dist/goal/goalProgress.js +75 -69
  51. package/dist/goal/goalReport.js +5 -12
  52. package/dist/goal/goalStatus.d.ts +2 -0
  53. package/dist/goal/goalStatus.js +10 -3
  54. package/dist/goal/goalStore.d.ts +9 -2
  55. package/dist/goal/goalStore.js +152 -45
  56. package/dist/goal/handoffExport.js +6 -6
  57. package/dist/goal/specKitImport.js +97 -96
  58. package/dist/goal/subgoalSync.js +11 -9
  59. package/dist/goal/worktreeManager.d.ts +2 -0
  60. package/dist/goal/worktreeManager.js +209 -136
  61. package/dist/httpServer.js +111 -32
  62. package/dist/logging.js +5 -3
  63. package/dist/policy/projectPolicy.js +5 -2
  64. package/dist/release/releaseGate.d.ts +1 -1
  65. package/dist/release/releaseGate.js +78 -25
  66. package/dist/runner/agentInvocation.d.ts +14 -0
  67. package/dist/runner/agentInvocation.js +96 -4
  68. package/dist/runner/changeCapture.d.ts +13 -5
  69. package/dist/runner/changeCapture.js +243 -61
  70. package/dist/runner/postTaskCleanup.js +48 -19
  71. package/dist/runner/processSecurity.d.ts +56 -0
  72. package/dist/runner/processSecurity.js +373 -0
  73. package/dist/runner/runTask.d.ts +2 -1
  74. package/dist/runner/runTask.js +162 -93
  75. package/dist/runner/simpleProcess.d.ts +3 -0
  76. package/dist/runner/simpleProcess.js +50 -36
  77. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  78. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  79. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +3 -1
  80. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  81. package/dist/runner/taskStatusStore.d.ts +11 -0
  82. package/dist/runner/taskStatusStore.js +35 -0
  83. package/dist/runner/watch.d.ts +14 -1
  84. package/dist/runner/watch.js +320 -49
  85. package/dist/security/contentRedaction.js +2 -2
  86. package/dist/security/discoveryTokenStore.d.ts +3 -2
  87. package/dist/security/discoveryTokenStore.js +17 -2
  88. package/dist/security/loopbackHost.d.ts +2 -0
  89. package/dist/security/loopbackHost.js +16 -0
  90. package/dist/security/pathGuard.js +32 -17
  91. package/dist/security/planGuard.js +2 -1
  92. package/dist/security/runtimeGuard.js +9 -6
  93. package/dist/security/secretComparison.d.ts +3 -0
  94. package/dist/security/secretComparison.js +11 -0
  95. package/dist/security/sensitiveGuard.d.ts +5 -0
  96. package/dist/security/sensitiveGuard.js +27 -8
  97. package/dist/security/toolInvocationGuard.d.ts +2 -2
  98. package/dist/security/toolInvocationGuard.js +1 -1
  99. package/dist/security/workspaceRootGuard.d.ts +8 -0
  100. package/dist/security/workspaceRootGuard.js +47 -0
  101. package/dist/smoke-test.js +27 -26
  102. package/dist/tools/catalog/index.d.ts +4 -0
  103. package/dist/tools/catalog/index.js +4 -0
  104. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  105. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  106. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  107. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  108. package/dist/tools/definitions/toolDefs.js +1177 -0
  109. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  110. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  111. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +2 -1
  112. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
  113. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
  114. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
  115. package/dist/tools/diagnostics/index.d.ts +6 -0
  116. package/dist/tools/diagnostics/index.js +6 -0
  117. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  118. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  119. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  120. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  121. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  122. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  123. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  124. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  125. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  126. package/dist/tools/direct/index.d.ts +3 -0
  127. package/dist/tools/direct/index.js +3 -0
  128. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  129. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  130. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  131. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  132. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  133. package/dist/tools/discovery/index.d.ts +3 -0
  134. package/dist/tools/discovery/index.js +3 -0
  135. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  136. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  137. package/dist/tools/dispatch/coreDispatch.js +32 -31
  138. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  139. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  140. package/dist/tools/dispatch/directDispatch.js +11 -10
  141. package/dist/tools/dispatch/goalDispatch.js +5 -4
  142. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  143. package/dist/tools/dispatch/validation.d.ts +7 -0
  144. package/dist/tools/dispatch/validation.js +71 -0
  145. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  146. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  147. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  148. package/dist/tools/goals/index.d.ts +3 -0
  149. package/dist/tools/goals/index.js +3 -0
  150. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  151. package/dist/tools/registry.d.ts +7 -11
  152. package/dist/tools/registry.js +17 -1178
  153. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  154. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  155. package/dist/tools/release/index.d.ts +2 -0
  156. package/dist/tools/release/index.js +2 -0
  157. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  158. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  159. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  160. package/dist/tools/tasks/cancelTask.js +86 -0
  161. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  162. package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
  163. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  164. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
  165. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  166. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  167. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  168. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +11 -6
  169. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  170. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
  171. package/dist/tools/tasks/index.d.ts +18 -0
  172. package/dist/tools/tasks/index.js +18 -0
  173. package/dist/tools/tasks/killTask.d.ts +1 -0
  174. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  175. package/dist/tools/{listTasks.js → tasks/listTasks.js} +5 -4
  176. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
  177. package/dist/tools/tasks/reconcileTasks.js +496 -0
  178. package/dist/tools/tasks/retryTask.js +60 -0
  179. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  180. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +63 -41
  181. package/dist/tools/tasks/runVerification.js +38 -0
  182. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  183. package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
  184. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  185. package/dist/tools/tasks/taskStates.d.ts +6 -0
  186. package/dist/tools/tasks/taskStates.js +52 -0
  187. package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
  188. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  189. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  190. package/dist/tools/workspace/applyPatch.js +53 -0
  191. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  192. package/dist/tools/workspace/index.d.ts +10 -0
  193. package/dist/tools/workspace/index.js +10 -0
  194. package/dist/tools/workspace/listAgents.d.ts +23 -0
  195. package/dist/tools/workspace/listAgents.js +56 -0
  196. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  197. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  198. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  199. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  200. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  201. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  202. package/dist/tools/workspace/syncFile.js +134 -0
  203. package/dist/utils/atomicFile.d.ts +7 -0
  204. package/dist/utils/atomicFile.js +67 -0
  205. package/dist/utils/boundedFile.d.ts +8 -0
  206. package/dist/utils/boundedFile.js +93 -0
  207. package/dist/utils/lockedJsonFile.d.ts +15 -0
  208. package/dist/utils/lockedJsonFile.js +224 -0
  209. package/dist/utils/platform.d.ts +11 -0
  210. package/dist/utils/platform.js +26 -0
  211. package/dist/utils/stableJson.d.ts +2 -0
  212. package/dist/utils/stableJson.js +12 -0
  213. package/dist/version.d.ts +3 -2
  214. package/dist/version.js +3 -2
  215. package/dist/watcherStatus.d.ts +2 -2
  216. package/dist/watcherStatus.js +18 -0
  217. package/docs/CODE_WIKI.md +808 -143
  218. package/docs/architecture.md +15 -55
  219. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  220. package/docs/control-center/README.md +4 -4
  221. package/docs/dashboard-overview.md +4 -4
  222. package/docs/desktop-app.md +175 -0
  223. package/docs/evidence-pack-schema.md +3 -3
  224. package/docs/open-source-application.md +16 -11
  225. package/docs/release-checklist.md +1 -1
  226. package/docs/release-evidence.md +35 -39
  227. package/docs/task-safe-review-workflow.md +5 -5
  228. package/docs/threat-model.md +19 -1
  229. package/examples/config.example.json +4 -2
  230. package/package.json +27 -10
  231. package/scripts/README.md +15 -6
  232. package/scripts/build.js +11 -0
  233. package/scripts/checks/brand-check.js +3 -0
  234. package/scripts/checks/build-output-check.js +37 -0
  235. package/scripts/checks/control-center-smoke.js +102 -10
  236. package/scripts/checks/control-smoke.js +19 -8
  237. package/scripts/checks/http-mcp-smoke.js +82 -4
  238. package/scripts/checks/lifecycle-smoke.js +15 -12
  239. package/scripts/checks/mcp-manifest-check.js +1 -1
  240. package/scripts/checks/mcp-smoke.js +58 -4
  241. package/scripts/checks/package-manifest-check.js +20 -7
  242. package/scripts/checks/unit-tests.js +3 -5
  243. package/scripts/checks/watcher-supervisor-smoke.js +38 -1
  244. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  245. package/scripts/control/manage-patchwarden.ps1 +69 -4
  246. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  247. package/scripts/control/run-background-supervisor.ps1 +41 -0
  248. package/scripts/control/start-patchwarden-tunnel.ps1 +191 -33
  249. package/scripts/e2e/demo-runtime.mjs +153 -0
  250. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  251. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  252. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  253. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  254. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  255. package/scripts/lib/clean-generated-output.js +11 -0
  256. package/scripts/lib/file-discovery.js +12 -0
  257. package/scripts/release/desktop-preflight.js +211 -0
  258. package/scripts/release/pack-clean.js +59 -14
  259. package/src/assessments/agentAssessor.ts +26 -12
  260. package/src/assessments/assessmentDiagnostics.ts +172 -0
  261. package/src/assessments/assessmentStore.ts +288 -51
  262. package/src/assessments/securitySnapshot.ts +231 -0
  263. package/src/config.ts +70 -13
  264. package/src/control/fileManager.ts +40 -0
  265. package/src/control/middleware/auth.ts +22 -4
  266. package/src/control/middleware/static.ts +16 -3
  267. package/src/control/routeTable.ts +301 -0
  268. package/src/control/routes/audit.ts +14 -6
  269. package/src/control/routes/evidence.ts +1 -1
  270. package/src/control/routes/lineage.ts +1 -1
  271. package/src/control/routes/process.ts +341 -40
  272. package/src/control/routes/sessions.ts +88 -25
  273. package/src/control/routes/status.ts +134 -17
  274. package/src/control/routes/taskActions.ts +24 -58
  275. package/src/control/routes/tasks.ts +66 -58
  276. package/src/control/routes/workspace.ts +56 -25
  277. package/src/control/runtime.ts +71 -43
  278. package/src/control/server.ts +37 -342
  279. package/src/control/shared.ts +77 -11
  280. package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
  281. package/src/direct/directAudit.ts +17 -9
  282. package/src/direct/directGuards.ts +72 -44
  283. package/src/direct/directPatch.ts +95 -8
  284. package/src/direct/directSessionStore.ts +213 -55
  285. package/src/direct/directVerification.ts +9 -23
  286. package/src/doctor.ts +76 -64
  287. package/src/goal/acceptanceEngine.ts +1 -1
  288. package/src/goal/goalProgress.ts +17 -13
  289. package/src/goal/goalReport.ts +5 -13
  290. package/src/goal/goalStatus.ts +19 -3
  291. package/src/goal/goalStore.ts +241 -54
  292. package/src/goal/handoffExport.ts +6 -6
  293. package/src/goal/specKitImport.ts +12 -12
  294. package/src/goal/subgoalSync.ts +11 -8
  295. package/src/goal/worktreeManager.ts +139 -22
  296. package/src/httpServer.ts +113 -30
  297. package/src/logging.ts +10 -3
  298. package/src/policy/projectPolicy.ts +5 -2
  299. package/src/release/releaseGate.ts +74 -27
  300. package/src/runner/agentInvocation.ts +126 -4
  301. package/src/runner/changeCapture.ts +291 -57
  302. package/src/runner/postTaskCleanup.ts +41 -17
  303. package/src/runner/processSecurity.ts +433 -0
  304. package/src/runner/runTask.ts +197 -97
  305. package/src/runner/simpleProcess.ts +66 -31
  306. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  307. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
  308. package/src/runner/taskStatusStore.ts +69 -0
  309. package/src/runner/watch.ts +348 -52
  310. package/src/security/contentRedaction.ts +2 -2
  311. package/src/security/discoveryTokenStore.ts +18 -4
  312. package/src/security/loopbackHost.ts +19 -0
  313. package/src/security/pathGuard.ts +35 -16
  314. package/src/security/planGuard.ts +2 -1
  315. package/src/security/runtimeGuard.ts +9 -6
  316. package/src/security/secretComparison.ts +13 -0
  317. package/src/security/sensitiveGuard.ts +27 -8
  318. package/src/security/toolInvocationGuard.ts +2 -2
  319. package/src/security/workspaceRootGuard.ts +53 -0
  320. package/src/smoke-test.ts +27 -26
  321. package/src/tools/catalog/index.ts +4 -0
  322. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  323. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  324. package/src/tools/definitions/toolDefs.ts +1264 -0
  325. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  326. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +136 -39
  327. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
  328. package/src/tools/diagnostics/index.ts +6 -0
  329. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  330. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  331. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  332. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  333. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  334. package/src/tools/direct/index.ts +3 -0
  335. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  336. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  337. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  338. package/src/tools/discovery/index.ts +3 -0
  339. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  340. package/src/tools/dispatch/coreDispatch.ts +36 -31
  341. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  342. package/src/tools/dispatch/directDispatch.ts +11 -10
  343. package/src/tools/dispatch/goalDispatch.ts +5 -4
  344. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  345. package/src/tools/dispatch/validation.ts +78 -0
  346. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  347. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  348. package/src/tools/goals/index.ts +3 -0
  349. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  350. package/src/tools/registry.ts +17 -1261
  351. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  352. package/src/tools/release/index.ts +2 -0
  353. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  354. package/src/tools/tasks/cancelTask.ts +110 -0
  355. package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
  356. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
  357. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  358. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  359. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +13 -6
  360. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  361. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
  362. package/src/tools/tasks/index.ts +18 -0
  363. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
  364. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
  365. package/src/tools/tasks/retryTask.ts +69 -0
  366. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
  367. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  368. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  369. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  370. package/src/tools/tasks/taskStates.ts +54 -0
  371. package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
  372. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  373. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  374. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  375. package/src/tools/workspace/index.ts +10 -0
  376. package/src/tools/workspace/listAgents.ts +75 -0
  377. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  378. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  379. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  380. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  381. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  382. package/src/tools/workspace/syncFile.ts +292 -0
  383. package/src/utils/atomicFile.ts +74 -0
  384. package/src/utils/boundedFile.ts +116 -0
  385. package/src/utils/lockedJsonFile.ts +251 -0
  386. package/src/utils/platform.ts +31 -0
  387. package/src/utils/stableJson.ts +12 -0
  388. package/src/version.ts +3 -2
  389. package/src/watcherStatus.ts +19 -1
  390. package/ui/desktop-bootstrap.js +11 -0
  391. package/ui/desktop-bridge.js +36 -0
  392. package/ui/desktop.css +160 -0
  393. package/ui/getting-started.js +93 -0
  394. package/ui/i18n.js +484 -0
  395. package/ui/log-parser.js +30 -0
  396. package/ui/pages/audit.html +12 -0
  397. package/ui/pages/dashboard.html +94 -147
  398. package/ui/pages/direct-sessions.html +123 -26
  399. package/ui/pages/getting-started.html +43 -0
  400. package/ui/pages/logs.html +134 -22
  401. package/ui/pages/settings.html +101 -0
  402. package/ui/pages/task-detail.html +12 -0
  403. package/ui/pages/tasks.html +54 -8
  404. package/ui/pages/workspace.html +38 -1
  405. package/ui/settings.js +254 -0
  406. package/PatchWarden-Control.cmd +0 -6
  407. package/Restart-PatchWarden-Control.cmd +0 -6
  408. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  409. package/dist/test/unit/acceptance-engine.test.js +0 -228
  410. package/dist/test/unit/agent-router.test.d.ts +0 -1
  411. package/dist/test/unit/agent-router.test.js +0 -287
  412. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  413. package/dist/test/unit/android-doctor.test.js +0 -118
  414. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  415. package/dist/test/unit/apply-patch.test.js +0 -225
  416. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  417. package/dist/test/unit/audit-checks.test.js +0 -350
  418. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  419. package/dist/test/unit/chinese-path.test.js +0 -91
  420. package/dist/test/unit/command-guard.test.d.ts +0 -1
  421. package/dist/test/unit/command-guard.test.js +0 -160
  422. package/dist/test/unit/create-task.test.d.ts +0 -1
  423. package/dist/test/unit/create-task.test.js +0 -197
  424. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  425. package/dist/test/unit/diagnose-task.test.js +0 -457
  426. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  427. package/dist/test/unit/direct-guards.test.js +0 -328
  428. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  429. package/dist/test/unit/discovery-token-store.test.js +0 -139
  430. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  431. package/dist/test/unit/evidence-pack.test.js +0 -224
  432. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  433. package/dist/test/unit/get-task-status.test.js +0 -174
  434. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  435. package/dist/test/unit/get-task-summary.test.js +0 -146
  436. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  437. package/dist/test/unit/goal-graph.test.js +0 -298
  438. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  439. package/dist/test/unit/goal-progress.test.js +0 -381
  440. package/dist/test/unit/goal-report.test.d.ts +0 -1
  441. package/dist/test/unit/goal-report.test.js +0 -159
  442. package/dist/test/unit/goal-status.test.d.ts +0 -1
  443. package/dist/test/unit/goal-status.test.js +0 -215
  444. package/dist/test/unit/goal-store.test.d.ts +0 -1
  445. package/dist/test/unit/goal-store.test.js +0 -253
  446. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  447. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  448. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  449. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  450. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  451. package/dist/test/unit/handoff-export.test.js +0 -263
  452. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  453. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  454. package/dist/test/unit/logging.test.d.ts +0 -1
  455. package/dist/test/unit/logging.test.js +0 -397
  456. package/dist/test/unit/path-guard.test.d.ts +0 -1
  457. package/dist/test/unit/path-guard.test.js +0 -133
  458. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  459. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  460. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  461. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  462. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  463. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  464. package/dist/test/unit/release-gate.test.d.ts +0 -1
  465. package/dist/test/unit/release-gate.test.js +0 -242
  466. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  467. package/dist/test/unit/run-task-loop.test.js +0 -380
  468. package/dist/test/unit/safe-status.test.d.ts +0 -1
  469. package/dist/test/unit/safe-status.test.js +0 -165
  470. package/dist/test/unit/safe-views.test.d.ts +0 -1
  471. package/dist/test/unit/safe-views.test.js +0 -171
  472. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  473. package/dist/test/unit/schema-drift-check.test.js +0 -176
  474. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  475. package/dist/test/unit/sensitive-guard.test.js +0 -104
  476. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  477. package/dist/test/unit/spec-kit-import.test.js +0 -341
  478. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  479. package/dist/test/unit/subgoal-sync.test.js +0 -183
  480. package/dist/test/unit/sync-file.test.d.ts +0 -1
  481. package/dist/test/unit/sync-file.test.js +0 -154
  482. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  483. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  484. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  485. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  486. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  487. package/dist/test/unit/toolSearch.test.js +0 -571
  488. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  489. package/dist/test/unit/wait-for-task.test.js +0 -144
  490. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  491. package/dist/test/unit/watcher-status.test.js +0 -169
  492. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  493. package/dist/test/unit/worktree-manager.test.js +0 -176
  494. package/dist/tools/applyPatch.js +0 -41
  495. package/dist/tools/cancelTask.d.ts +0 -30
  496. package/dist/tools/cancelTask.js +0 -64
  497. package/dist/tools/finalizeDirectSession.js +0 -84
  498. package/dist/tools/goalSubgoalTask.js +0 -110
  499. package/dist/tools/killTask.d.ts +0 -15
  500. package/dist/tools/listAgents.d.ts +0 -12
  501. package/dist/tools/listAgents.js +0 -42
  502. package/dist/tools/reconcileTasks.js +0 -352
  503. package/dist/tools/retryTask.js +0 -45
  504. package/dist/tools/runVerification.js +0 -36
  505. package/dist/tools/syncFile.js +0 -65
  506. package/docs/control-center/control-center-daily-driver.md +0 -211
  507. package/docs/control-center/control-center-mvp.md +0 -205
  508. package/docs/control-center/control-center-phase2.md +0 -159
  509. package/docs/execution-plan-2026-07-09.md +0 -315
  510. package/docs/release-v0.6.0.md +0 -71
  511. package/docs/release-v0.6.1.md +0 -75
  512. package/docs/release-v0.6.4.md +0 -45
  513. package/docs/roadmap-execution-and-acceptance.md +0 -365
  514. package/src/test/unit/acceptance-engine.test.ts +0 -261
  515. package/src/test/unit/agent-router.test.ts +0 -342
  516. package/src/test/unit/android-doctor.test.ts +0 -158
  517. package/src/test/unit/apply-patch.test.ts +0 -293
  518. package/src/test/unit/audit-checks.test.ts +0 -567
  519. package/src/test/unit/chinese-path.test.ts +0 -106
  520. package/src/test/unit/command-guard.test.ts +0 -221
  521. package/src/test/unit/create-task.test.ts +0 -255
  522. package/src/test/unit/diagnose-task.test.ts +0 -544
  523. package/src/test/unit/direct-guards.test.ts +0 -467
  524. package/src/test/unit/discovery-token-store.test.ts +0 -181
  525. package/src/test/unit/evidence-pack.test.ts +0 -251
  526. package/src/test/unit/get-task-status.test.ts +0 -203
  527. package/src/test/unit/get-task-summary.test.ts +0 -173
  528. package/src/test/unit/goal-graph.test.ts +0 -347
  529. package/src/test/unit/goal-progress.test.ts +0 -538
  530. package/src/test/unit/goal-report.test.ts +0 -189
  531. package/src/test/unit/goal-status.test.ts +0 -270
  532. package/src/test/unit/goal-store.test.ts +0 -318
  533. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  534. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  535. package/src/test/unit/handoff-export.test.ts +0 -295
  536. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  537. package/src/test/unit/logging.test.ts +0 -497
  538. package/src/test/unit/path-guard.test.ts +0 -180
  539. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  540. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  541. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  542. package/src/test/unit/release-gate.test.ts +0 -314
  543. package/src/test/unit/run-task-loop.test.ts +0 -425
  544. package/src/test/unit/safe-status.test.ts +0 -187
  545. package/src/test/unit/safe-views.test.ts +0 -184
  546. package/src/test/unit/schema-drift-check.test.ts +0 -260
  547. package/src/test/unit/sensitive-guard.test.ts +0 -124
  548. package/src/test/unit/spec-kit-import.test.ts +0 -429
  549. package/src/test/unit/subgoal-sync.test.ts +0 -236
  550. package/src/test/unit/sync-file.test.ts +0 -231
  551. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  552. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  553. package/src/test/unit/toolSearch.test.ts +0 -756
  554. package/src/test/unit/wait-for-task.test.ts +0 -176
  555. package/src/test/unit/watcher-status.test.ts +0 -190
  556. package/src/test/unit/worktree-manager.test.ts +0 -247
  557. package/src/tools/cancelTask.ts +0 -70
  558. package/src/tools/listAgents.ts +0 -54
  559. package/src/tools/retryTask.ts +0 -53
  560. package/src/tools/syncFile.ts +0 -122
  561. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  562. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  563. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  564. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  565. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  566. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  567. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  568. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  569. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  570. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  571. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  572. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  573. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  574. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  575. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  576. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  577. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  578. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  579. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  580. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  581. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  582. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  583. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  584. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  585. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  586. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  587. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  588. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  589. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  590. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  591. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
@@ -1,10 +1,8 @@
1
1
  import { spawn, spawnSync, type ChildProcess } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
2
3
  import {
3
- createWriteStream,
4
4
  existsSync,
5
5
  readFileSync,
6
- writeFileSync,
7
- type WriteStream,
8
6
  } from "node:fs";
9
7
  import { isAbsolute, join, relative, resolve } from "node:path";
10
8
  import { getTasksDir, getPlansDir, getConfig, resolveWorkspaceRoot } from "../config.js";
@@ -12,11 +10,18 @@ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
12
10
  import {
13
11
  guardTestCommand,
14
12
  } from "../security/commandGuard.js";
15
- import { writeTaskProgress } from "../taskProgress.js";
16
- import { writeTaskRuntime } from "../taskRuntime.js";
13
+ import { writeTaskProgress } from "./taskProgress.js";
14
+ import { writeTaskRuntime } from "./taskRuntime.js";
15
+ import {
16
+ claimPendingTask,
17
+ readTaskStatusFile,
18
+ updateTaskStatusFile,
19
+ type TaskStatusRecord,
20
+ } from "./taskStatusStore.js";
17
21
  import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
22
+ import { recordAssessmentValidationFailure } from "../assessments/assessmentDiagnostics.js";
18
23
  import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js";
19
- import type { TaskPhase, TaskStatus } from "../tools/createTask.js";
24
+ import type { TaskPhase, TaskStatus } from "../tools/tasks/createTask.js";
20
25
  import {
21
26
  buildChangeArtifacts,
22
27
  captureRepoSnapshot,
@@ -35,8 +40,18 @@ import {
35
40
  type ChangedFileGroups,
36
41
  } from "./changeCapture.js";
37
42
  import { PatchWardenError, errorPayload } from "../errors.js";
38
- import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
43
+ import { ARTIFACT_SCHEMA_VERSION } from "../version.js";
44
+ import { diagnoseAndroidBuild } from "../tools/workspace/androidDoctor.js";
39
45
  import { runPostTaskCleanup, type PostTaskCleanupReport } from "./postTaskCleanup.js";
46
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
47
+ import {
48
+ allowedEnvironmentValues,
49
+ buildChildEnvironment,
50
+ redactProcessOutput,
51
+ resolvePackageManagerInvocation,
52
+ resolveTrustedExecutable,
53
+ SecureProcessLogCapture,
54
+ } from "./processSecurity.js";
40
55
 
41
56
  const HEARTBEAT_INTERVAL_MS = 2000;
42
57
  const GRACEFUL_KILL_MS = 2000;
@@ -95,7 +110,7 @@ interface TaskContext {
95
110
  wsRoot: string;
96
111
  config: ReturnType<typeof getConfig>;
97
112
  plansDir: string;
98
- initialStatus: Record<string, any>;
113
+ initialStatus: TaskStatusRecord;
99
114
  planId: string;
100
115
  agentName: string;
101
116
  testCommand: string;
@@ -104,6 +119,7 @@ interface TaskContext {
104
119
  timeoutSeconds: number;
105
120
  startedAtMs: number;
106
121
  deadlineMs: number;
122
+ runnerInstanceId: string;
107
123
  beforeSnapshot: RepoSnapshot;
108
124
  beforeWorkspaceSnapshot: RepoSnapshot;
109
125
  externalDirtyBaseline: ExternalDirtyFile[];
@@ -111,6 +127,7 @@ interface TaskContext {
111
127
 
112
128
  interface ExecutionState {
113
129
  agentResult: ManagedProcessResult | null;
130
+ agentFailureCategory: string | null;
114
131
  testResult: TestExecutionResult;
115
132
  verifyResults: TestExecutionResult[];
116
133
  finalStatus: TaskStatus;
@@ -154,7 +171,26 @@ async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult>
154
171
  const statusFile = join(taskDir, "status.json");
155
172
  if (!existsSync(statusFile)) throw new Error(`Task not found: "${taskId}"`);
156
173
 
157
- const initialStatus = readStatus(statusFile);
174
+ const startedAtMs = Date.now();
175
+ const runnerInstanceId = randomUUID().replace(/-/g, "");
176
+ const claim = claimPendingTask(statusFile, {
177
+ status: "running",
178
+ phase: "preparing",
179
+ started_at: new Date(startedAtMs).toISOString(),
180
+ last_heartbeat_at: new Date(startedAtMs).toISOString(),
181
+ current_command: null,
182
+ error: null,
183
+ });
184
+ if (!claim.claimed) {
185
+ const currentStatus = String(claim.status.status || "failed") as TaskStatus;
186
+ return {
187
+ task_id: taskId,
188
+ status: currentStatus,
189
+ error: `Task is ${currentStatus}; only pending tasks can be claimed for execution.`,
190
+ };
191
+ }
192
+
193
+ const initialStatus = claim.status;
158
194
  const planId = String(initialStatus.plan_id || "");
159
195
  const agentName = String(initialStatus.agent || "");
160
196
  const rawRepoPath = String(initialStatus.resolved_repo_path || initialStatus.repo_path || wsRoot);
@@ -167,7 +203,6 @@ async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult>
167
203
  } catch (error) {
168
204
  return failBeforeExecution(taskId, taskDir, errorMessage(error), error);
169
205
  }
170
- const startedAtMs = Date.now();
171
206
  const deadlineMs = startedAtMs + timeoutSeconds * 1000;
172
207
 
173
208
  let repoPath: string;
@@ -190,7 +225,11 @@ async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult>
190
225
  const preExecSnapshot = await captureRepoSnapshot(repoPath);
191
226
  const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
192
227
  if (!validation.valid) {
193
- const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
228
+ recordAssessmentValidationFailure(validation);
229
+ const changedFields = validation.config_change_categories?.length
230
+ ? ` Changed fields: ${validation.config_change_categories.join(", ")}.`
231
+ : "";
232
+ const message = `assessment validation failed: ${validation.failure_reason}.${changedFields} Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
194
233
  return failBeforeExecution(taskId, taskDir, message);
195
234
  }
196
235
  } catch (error) {
@@ -199,18 +238,12 @@ async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult>
199
238
  }
200
239
  }
201
240
 
202
- updateStatus(taskDir, {
203
- status: "running",
204
- phase: "preparing",
205
- started_at: new Date(startedAtMs).toISOString(),
206
- last_heartbeat_at: new Date().toISOString(),
207
- timeout_seconds: timeoutSeconds,
208
- current_command: null,
209
- error: null,
210
- });
241
+ updateStatus(taskDir, { timeout_seconds: timeoutSeconds });
211
242
  writeTaskRuntime(taskDir, {
212
243
  task_started_at: new Date(startedAtMs).toISOString(),
213
244
  watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
245
+ runner_pid: process.pid,
246
+ runner_instance_id: runnerInstanceId,
214
247
  });
215
248
  setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
216
249
 
@@ -231,13 +264,14 @@ async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult>
231
264
  return {
232
265
  taskId, taskDir, statusFile, repoPath, wsRoot, config, plansDir, initialStatus,
233
266
  planId, agentName, testCommand, changePolicy, verifyCommands, timeoutSeconds,
234
- startedAtMs, deadlineMs, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
267
+ startedAtMs, deadlineMs, runnerInstanceId, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
235
268
  };
236
269
  }
237
270
 
238
271
  async function executeAgent(ctx: TaskContext): Promise<ExecutionState> {
239
272
  const state: ExecutionState = {
240
273
  agentResult: null,
274
+ agentFailureCategory: null,
241
275
  testResult: skippedTest(ctx.testCommand, ctx.repoPath, "Agent did not complete successfully."),
242
276
  verifyResults: [],
243
277
  finalStatus: "failed",
@@ -262,8 +296,11 @@ async function executeAgent(ctx: TaskContext): Promise<ExecutionState> {
262
296
  phase: "executing_agent",
263
297
  currentCommand: invocation.commandLabel,
264
298
  deadlineMs: ctx.deadlineMs,
299
+ runnerInstanceId: ctx.runnerInstanceId,
265
300
  stdoutPath: join(ctx.taskDir, "stdout.log"),
266
301
  stderrPath: join(ctx.taskDir, "stderr.log"),
302
+ environmentVariableNames: invocation.environmentVariableNames,
303
+ blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
267
304
  });
268
305
 
269
306
  const agentResult = state.agentResult;
@@ -273,11 +310,15 @@ async function executeAgent(ctx: TaskContext): Promise<ExecutionState> {
273
310
  ? "Task was terminated by kill_task."
274
311
  : "Task was canceled by user request.";
275
312
  } else if (agentResult.terminationReason === "timeout") {
313
+ state.finalStatus = "timeout";
276
314
  state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during agent execution.`;
277
315
  } else if (agentResult.spawnError) {
278
316
  state.finalError = `Agent spawn failed: ${agentResult.spawnError}`;
279
317
  } else if (agentResult.exitCode !== 0) {
280
- state.finalError = `Agent exited with code ${agentResult.exitCode}.`;
318
+ state.agentFailureCategory = classifyAgentFailure(agentResult.stderr);
319
+ state.finalError = state.agentFailureCategory
320
+ ? `Agent provider failure (${state.agentFailureCategory}); process exited with code ${agentResult.exitCode}.`
321
+ : `Agent exited with code ${agentResult.exitCode}.`;
281
322
  }
282
323
  } catch (error) {
283
324
  state.lastCaughtError = error;
@@ -294,7 +335,14 @@ async function runVerification(ctx: TaskContext, state: ExecutionState): Promise
294
335
  if (ctx.verifyCommands.length > 0) {
295
336
  for (const command of ctx.verifyCommands) {
296
337
  setTaskPhase(ctx.taskDir, "running_tests", command);
297
- const verification = await runTrustedTestCommand(command, ctx.repoPath, ctx.taskDir, ctx.statusFile, ctx.deadlineMs);
338
+ const verification = await runTrustedTestCommand(
339
+ command,
340
+ ctx.repoPath,
341
+ ctx.taskDir,
342
+ ctx.statusFile,
343
+ ctx.deadlineMs,
344
+ ctx.runnerInstanceId,
345
+ );
298
346
  state.verifyResults.push(verification);
299
347
  if (verification.terminationReason || Date.now() >= ctx.deadlineMs) break;
300
348
  }
@@ -307,6 +355,7 @@ async function runVerification(ctx: TaskContext, state: ExecutionState): Promise
307
355
  ? "Task was terminated by kill_task during verification."
308
356
  : "Task was canceled during verification.";
309
357
  } else if (interrupted?.terminationReason === "timeout" || Date.now() >= ctx.deadlineMs) {
358
+ state.finalStatus = "timeout";
310
359
  state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during verification.`;
311
360
  } else if (failedVerification) {
312
361
  state.finalStatus = "failed_verification";
@@ -342,7 +391,7 @@ async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promis
342
391
  skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
343
392
  source_files_touched: 0,
344
393
  };
345
- writeFileSync(join(ctx.taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
394
+ atomicWriteJsonFileSync(join(ctx.taskDir, "post-task-cleanup.json"), cleanupReport);
346
395
  updateStatus(ctx.taskDir, { cleanup: cleanupReport });
347
396
  }
348
397
  }
@@ -353,21 +402,20 @@ async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promis
353
402
  let artifactStatus: "collected" | "partial" | "failed" | "timeout" = "collected";
354
403
  let artifactCollectionError: string | null = null;
355
404
  let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
405
+ const artifactController = new AbortController();
356
406
  try {
357
- const collectionResult = await Promise.race([
358
- Promise.resolve().then(async () => {
359
- const afterSnapshot = await captureRepoSnapshot(ctx.repoPath);
360
- writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
361
- return await buildChangeArtifacts(ctx.repoPath, ctx.beforeSnapshot, afterSnapshot);
362
- }),
363
- new Promise<never>((_, reject) => {
364
- timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
365
- }),
366
- ]);
367
- if (timeoutHandle) clearTimeout(timeoutHandle);
368
- changes = collectionResult;
407
+ timeoutHandle = setTimeout(() => {
408
+ artifactController.abort(new Error("Artifact collection timed out"));
409
+ }, ARTIFACT_COLLECTION_TIMEOUT_MS);
410
+ const afterSnapshot = await captureRepoSnapshot(ctx.repoPath, artifactController.signal);
411
+ writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
412
+ changes = await buildChangeArtifacts(
413
+ ctx.repoPath,
414
+ ctx.beforeSnapshot,
415
+ afterSnapshot,
416
+ artifactController.signal,
417
+ );
369
418
  } catch (error) {
370
- if (timeoutHandle) clearTimeout(timeoutHandle);
371
419
  state.lastCaughtError = error;
372
420
  artifactCollectionError = errorMessage(error);
373
421
  artifactStatus = artifactCollectionError.includes("timed out") ? "timeout" : "failed";
@@ -386,7 +434,7 @@ async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promis
386
434
  unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
387
435
  artifact_hygiene: emptyArtifactHygiene(),
388
436
  };
389
- writeFileSync(join(ctx.taskDir, "partial_result.md"), [
437
+ atomicWriteFileSync(join(ctx.taskDir, "partial_result.md"), [
390
438
  "# PatchWarden Partial Result",
391
439
  "",
392
440
  `Task: ${ctx.taskId}`,
@@ -406,13 +454,15 @@ async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promis
406
454
  "## Note",
407
455
  "Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
408
456
  "",
409
- ].join("\n"), "utf-8");
457
+ ].join("\n"));
410
458
  if (state.finalStatus === "done_by_agent") {
411
459
  state.finalError = `Change capture failed: ${artifactCollectionError}`;
412
460
  state.finalStatus = "failed";
413
461
  } else {
414
462
  state.finalError ||= `Change capture failed: ${artifactCollectionError}`;
415
463
  }
464
+ } finally {
465
+ if (timeoutHandle) clearTimeout(timeoutHandle);
416
466
  }
417
467
  const artifactCollectionFinishedAt = new Date().toISOString();
418
468
 
@@ -440,18 +490,18 @@ async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promis
440
490
 
441
491
  applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty);
442
492
 
443
- writeFileSync(join(ctx.taskDir, "git.diff"), changes.diff, "utf-8");
444
- writeFileSync(join(ctx.taskDir, "diff.patch"), changes.diff, "utf-8");
445
- writeFileSync(join(ctx.taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
493
+ atomicWriteFileSync(join(ctx.taskDir, "git.diff"), changes.diff);
494
+ atomicWriteFileSync(join(ctx.taskDir, "diff.patch"), changes.diff);
495
+ atomicWriteJsonFileSync(join(ctx.taskDir, "changed-files.json"), { schema_version: ARTIFACT_SCHEMA_VERSION, ...changes });
446
496
  const artifactManifest = await buildArtifactManifest(changes.changed_files, ctx.repoPath, ctx.taskId);
447
- writeFileSync(join(ctx.taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
497
+ atomicWriteJsonFileSync(join(ctx.taskDir, "artifact_manifest.json"), artifactManifest);
448
498
  const changedFileGroups = groupChangedFiles(changes.changed_files);
449
- writeFileSync(join(ctx.taskDir, "file-stats.json"), JSON.stringify({
499
+ atomicWriteJsonFileSync(join(ctx.taskDir, "file-stats.json"), {
450
500
  task_id: ctx.taskId,
451
501
  additions: changes.additions,
452
502
  deletions: changes.deletions,
453
503
  files: changes.file_stats,
454
- }, null, 2), "utf-8");
504
+ });
455
505
 
456
506
  return {
457
507
  changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
@@ -470,15 +520,15 @@ function applyScopeViolationVerdict(
470
520
  if (newOutOfScopeChanges.length > 0) {
471
521
  state.finalStatus = "failed_scope_violation";
472
522
  state.finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
473
- writeFileSync(join(ctx.taskDir, "rollback-plan.json"), JSON.stringify({
523
+ atomicWriteJsonFileSync(join(ctx.taskDir, "rollback-plan.json"), {
474
524
  task_id: ctx.taskId,
475
525
  status: "review_required",
476
526
  automatic_rollback_performed: false,
477
527
  warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
478
528
  out_of_scope_changes: newOutOfScopeChanges,
479
529
  preexisting_external_dirty_files: preexistingExternalDirty,
480
- }, null, 2), "utf-8");
481
- writeFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
530
+ });
531
+ atomicWriteFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
482
532
  "# Scope Violation Rollback Plan",
483
533
  "",
484
534
  `Task: ${ctx.taskId}`,
@@ -488,7 +538,10 @@ function applyScopeViolationVerdict(
488
538
  "## New out-of-scope files (caused by this task)",
489
539
  ...newOutOfScopeChanges.map((file) => `- ${file.change}: ${file.path}`),
490
540
  "",
491
- ].join("\n"), "utf-8");
541
+ ].join("\n"));
542
+ } else if (changes.diff_redacted) {
543
+ state.finalStatus = "failed_policy_violation";
544
+ state.finalError = "Credential-like content was detected in the task diff. PatchWarden redacted the evidence; remove the sensitive content before retrying.";
492
545
  } else if (ctx.changePolicy === "no_changes" && changes.changed_files.length > 0) {
493
546
  state.finalStatus = "failed_policy_violation";
494
547
  state.finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
@@ -500,19 +553,21 @@ function finalizeTask(ctx: TaskContext, state: ExecutionState, evidence: Artifac
500
553
  outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
501
554
  artifactManifest, changedFileGroups } = evidence;
502
555
 
503
- writeFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult), "utf-8");
556
+ atomicWriteFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult));
504
557
  const verifyJson = buildVerifyJson(ctx.verifyCommands, state.verifyResults, ctx.repoPath);
505
558
  if (newOutOfScopeChanges.length > 0) {
506
559
  verifyJson.status = "failed";
507
560
  verifyJson.failure_reason = "scope_violation";
508
561
  } else if (state.finalStatus === "failed_policy_violation") {
509
562
  verifyJson.status = "failed";
510
- verifyJson.failure_reason = "change_policy_violation";
563
+ verifyJson.failure_reason = changes.diff_redacted
564
+ ? "sensitive_content_violation"
565
+ : "change_policy_violation";
511
566
  }
512
- writeFileSync(join(ctx.taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
513
- writeFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
567
+ atomicWriteJsonFileSync(join(ctx.taskDir, "verify.json"), verifyJson);
568
+ atomicWriteFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands));
514
569
 
515
- if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus)) state.finalStatus = "failed";
570
+ if (!["canceled", "timeout", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus)) state.finalStatus = "failed";
516
571
  const finalPhase: TaskPhase = state.finalStatus === "done_by_agent" ? "done_by_agent" : (state.finalStatus as TaskPhase);
517
572
  const followup = buildFailureFollowup(state.finalStatus, state.finalError, verifyJson.commands);
518
573
 
@@ -547,19 +602,19 @@ function finalizeTask(ctx: TaskContext, state: ExecutionState, evidence: Artifac
547
602
  androidWarning,
548
603
  preexistingWarnings,
549
604
  });
550
- writeFileSync(join(ctx.taskDir, "result.md"), resultMd, "utf-8");
605
+ atomicWriteFileSync(join(ctx.taskDir, "result.md"), resultMd);
551
606
 
552
607
  const resultJson = buildResultJson({
553
608
  ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning,
554
609
  });
555
- writeFileSync(join(ctx.taskDir, "result.json"), JSON.stringify(resultJson, null, 2), "utf-8");
610
+ atomicWriteJsonFileSync(join(ctx.taskDir, "result.json"), resultJson);
556
611
 
557
612
  if (state.finalError) {
558
613
  const structuredError = errorPayload(state.lastCaughtError);
559
- writeFileSync(join(ctx.taskDir, "error.log"), JSON.stringify({
614
+ atomicWriteJsonFileSync(join(ctx.taskDir, "error.log"), {
560
615
  summary: state.finalError,
561
616
  ...structuredError,
562
- }, null, 2), "utf-8");
617
+ });
563
618
  }
564
619
 
565
620
  const finishedAt = new Date().toISOString();
@@ -570,6 +625,10 @@ function finalizeTask(ctx: TaskContext, state: ExecutionState, evidence: Artifac
570
625
  last_heartbeat_at: finishedAt,
571
626
  finished_at: finishedAt,
572
627
  error: state.finalError,
628
+ termination_reason: state.finalStatus === "timeout"
629
+ ? "timeout"
630
+ : state.finalStatus === "canceled" ? "canceled" : null,
631
+ agent_failure_category: state.agentFailureCategory,
573
632
  changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
574
633
  artifact_hygiene_counts: changes.artifact_hygiene.counts,
575
634
  artifact_status: artifactStatus,
@@ -584,6 +643,8 @@ function finalizeTask(ctx: TaskContext, state: ExecutionState, evidence: Artifac
584
643
  verify_commands: ctx.verifyCommands,
585
644
  diff_available: changes.diff_available,
586
645
  diff_truncated: changes.diff_truncated,
646
+ diff_redacted: changes.diff_redacted === true,
647
+ diff_redaction_categories: changes.diff_redaction_categories ?? [],
587
648
  workspace_dirty_before: changes.workspace_dirty_before,
588
649
  workspace_dirty_after: changes.workspace_dirty_after,
589
650
  workspace_dirty: changes.workspace_dirty_after,
@@ -619,6 +680,7 @@ function buildResultJson(input: {
619
680
  preexistingExternalDirty, cleanupReport, artifactManifest, changedFileGroups } = evidence;
620
681
 
621
682
  return {
683
+ schema_version: ARTIFACT_SCHEMA_VERSION,
622
684
  task_id: ctx.taskId,
623
685
  status: state.finalStatus,
624
686
  agent: ctx.agentName,
@@ -629,6 +691,10 @@ function buildResultJson(input: {
629
691
  template: ctx.initialStatus.template || null,
630
692
  change_policy: ctx.changePolicy,
631
693
  summary: state.finalError || "Agent execution and configured verification completed successfully.",
694
+ termination_reason: state.finalStatus === "timeout"
695
+ ? "timeout"
696
+ : state.finalStatus === "canceled" ? "canceled" : null,
697
+ agent_failure_category: state.agentFailureCategory,
632
698
  changed_files: changes.changed_files,
633
699
  changed_file_groups: {
634
700
  source_changes: changedFileGroups.source_changes.length,
@@ -668,6 +734,9 @@ function buildResultJson(input: {
668
734
  warnings: [
669
735
  ...ctx.beforeSnapshot.warnings,
670
736
  ...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
737
+ ...(changes.diff_redacted ? [
738
+ `Credential-like diff content was redacted (${(changes.diff_redaction_categories ?? []).join(", ") || "sensitive_content"}).`,
739
+ ] : []),
671
740
  ...evidence.preexistingWarnings,
672
741
  ...(androidWarning ? [androidWarning] : []),
673
742
  ],
@@ -753,18 +822,34 @@ async function runManagedProcess(options: {
753
822
  deadlineMs: number;
754
823
  stdoutPath?: string;
755
824
  stderrPath?: string;
825
+ environmentVariableNames?: string[];
826
+ blockedEnvironmentVariableNames?: string[];
827
+ maxLogBytes?: number;
828
+ runnerInstanceId: string;
756
829
  }): Promise<ManagedProcessResult> {
757
830
  if (Date.now() >= options.deadlineMs) {
758
831
  return { exitCode: null, stdout: "", stderr: "", spawnError: null, terminationReason: "timeout" };
759
832
  }
760
833
 
834
+ const exactRedactionValues = allowedEnvironmentValues(options.environmentVariableNames);
835
+ const logCapture = new SecureProcessLogCapture(
836
+ [options.stdoutPath, options.stderrPath],
837
+ options.maxLogBytes,
838
+ );
761
839
  let child: ChildProcess;
762
840
  try {
763
- child = spawn(options.command, options.args, {
841
+ const env = buildChildEnvironment({
842
+ cwd: options.cwd,
843
+ allowedNames: options.environmentVariableNames,
844
+ blockedNames: options.blockedEnvironmentVariableNames,
845
+ });
846
+ const command = resolveTrustedExecutable(options.command, options.cwd, { pathValue: env.PATH });
847
+ child = spawn(command, options.args, {
764
848
  cwd: options.cwd,
765
849
  stdio: ["ignore", "pipe", "pipe"],
766
850
  detached: process.platform !== "win32",
767
851
  windowsHide: true,
852
+ env,
768
853
  });
769
854
  } catch (error) {
770
855
  return { exitCode: null, stdout: "", stderr: "", spawnError: errorMessage(error), terminationReason: null };
@@ -774,10 +859,11 @@ async function runManagedProcess(options: {
774
859
  writeTaskRuntime(options.taskDir, {
775
860
  child_pid: child.pid,
776
861
  child_started_at: new Date().toISOString(),
862
+ runner_pid: process.pid,
863
+ runner_instance_id: options.runnerInstanceId,
864
+ child_owned_by_runner_instance_id: options.runnerInstanceId,
777
865
  });
778
866
 
779
- const stdoutStream = openStream(options.stdoutPath);
780
- const stderrStream = openStream(options.stderrPath);
781
867
  let stdout = "";
782
868
  let stderr = "";
783
869
  let spawnError: string | null = null;
@@ -836,12 +922,12 @@ async function runManagedProcess(options: {
836
922
  child.stdout?.on("data", (chunk: Buffer) => {
837
923
  const text = chunk.toString("utf-8");
838
924
  stdout = appendBounded(stdout, text);
839
- stdoutStream?.write(text);
925
+ logCapture.append(options.stdoutPath, chunk);
840
926
  });
841
927
  child.stderr?.on("data", (chunk: Buffer) => {
842
928
  const text = chunk.toString("utf-8");
843
929
  stderr = appendBounded(stderr, text);
844
- stderrStream?.write(text);
930
+ logCapture.append(options.stderrPath, chunk);
845
931
  });
846
932
 
847
933
  heartbeat();
@@ -857,12 +943,6 @@ async function runManagedProcess(options: {
857
943
  };
858
944
  childExitFinish = finish;
859
945
  child.once("close", (code) => finish(code));
860
- child.once("exit", (code) => {
861
- // On Windows, "close" can lag behind "exit" when stdio pipes drain
862
- // after taskkill. Resolve on exit to avoid hanging the runner, but
863
- // still let "close" fire if it arrives first.
864
- finish(code);
865
- });
866
946
  child.once("error", (error) => {
867
947
  spawnError = error.message;
868
948
  finish(null);
@@ -871,8 +951,7 @@ async function runManagedProcess(options: {
871
951
 
872
952
  clearInterval(heartbeatTimer);
873
953
  if (forceTimer) clearTimeout(forceTimer);
874
- stdoutStream?.destroy();
875
- stderrStream?.destroy();
954
+ logCapture.flush(exactRedactionValues);
876
955
  writeTaskRuntime(options.taskDir, {
877
956
  phase: options.phase,
878
957
  last_heartbeat_at: new Date().toISOString(),
@@ -880,8 +959,16 @@ async function runManagedProcess(options: {
880
959
  runner_pid: process.pid,
881
960
  child_pid: undefined,
882
961
  });
962
+ const safeStdout = redactProcessOutput(stdout, exactRedactionValues);
963
+ const safeStderr = redactProcessOutput(stderr, exactRedactionValues);
883
964
 
884
- return { exitCode, stdout, stderr, spawnError, terminationReason };
965
+ return {
966
+ exitCode,
967
+ stdout: safeStdout.length > MAX_CAPTURE_CHARS ? safeStdout.slice(-MAX_CAPTURE_CHARS) : safeStdout,
968
+ stderr: safeStderr.length > MAX_CAPTURE_CHARS ? safeStderr.slice(-MAX_CAPTURE_CHARS) : safeStderr,
969
+ spawnError,
970
+ terminationReason,
971
+ };
885
972
  }
886
973
 
887
974
  async function runTrustedTestCommand(
@@ -889,7 +976,8 @@ async function runTrustedTestCommand(
889
976
  repoPath: string,
890
977
  taskDir: string,
891
978
  statusFile: string,
892
- deadlineMs: number
979
+ deadlineMs: number,
980
+ runnerInstanceId: string,
893
981
  ): Promise<TestExecutionResult> {
894
982
  const config = getConfig();
895
983
  const trusted = guardTestCommand(testCommand, config, repoPath);
@@ -899,9 +987,9 @@ async function runTrustedTestCommand(
899
987
  let command = parts[0];
900
988
  let args = parts.slice(1);
901
989
  if (process.platform === "win32" && /^(npm|npm\.cmd|pnpm|pnpm\.cmd)$/i.test(command)) {
902
- const shim = /^pnpm/i.test(command) ? "pnpm.cmd" : "npm.cmd";
903
- command = process.env.ComSpec || "cmd.exe";
904
- args = ["/d", "/s", "/c", shim, ...args];
990
+ const packageManager = resolvePackageManagerInvocation(command, repoPath);
991
+ command = packageManager.command;
992
+ args = [...packageManager.argsPrefix, ...args];
905
993
  }
906
994
  const result = await runManagedProcess({
907
995
  command,
@@ -912,6 +1000,7 @@ async function runTrustedTestCommand(
912
1000
  phase: "running_tests",
913
1001
  currentCommand: trusted,
914
1002
  deadlineMs,
1003
+ runnerInstanceId,
915
1004
  stdoutPath: join(taskDir, "test.stdout.log"),
916
1005
  stderrPath: join(taskDir, "test.stderr.log"),
917
1006
  });
@@ -1161,10 +1250,10 @@ function normalizeTimeout(value: unknown, config: ReturnType<typeof getConfig>):
1161
1250
  function failBeforeExecution(taskId: string, taskDir: string, message: string, caughtError?: unknown): TaskRunResult {
1162
1251
  // Phase 9: Preserve PatchWardenError structured info in error.log
1163
1252
  const structuredError = caughtError ? errorPayload(caughtError) : { error: message };
1164
- writeFileSync(join(taskDir, "error.log"), JSON.stringify({
1253
+ atomicWriteJsonFileSync(join(taskDir, "error.log"), {
1165
1254
  summary: message,
1166
1255
  ...structuredError,
1167
- }, null, 2), "utf-8");
1256
+ });
1168
1257
  const current = readStatus(join(taskDir, "status.json"));
1169
1258
  const now = new Date().toISOString();
1170
1259
  const verify: VerifyReport = {
@@ -1172,17 +1261,17 @@ function failBeforeExecution(taskId: string, taskDir: string, message: string, c
1172
1261
  requested_commands: Array.isArray(current.verify_commands) ? current.verify_commands : [],
1173
1262
  commands: [],
1174
1263
  };
1175
- writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verify, null, 2), "utf-8");
1176
- writeFileSync(join(taskDir, "verify.log"), `Verification did not run: ${message}\n`, "utf-8");
1177
- writeFileSync(join(taskDir, "test.log"), `(not run)\nExit code: not run\n${message}\n`, "utf-8");
1178
- writeFileSync(join(taskDir, "git.diff"), "(task failed before change capture)\n", "utf-8");
1179
- writeFileSync(join(taskDir, "diff.patch"), "(task failed before change capture)\n", "utf-8");
1180
- writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
1264
+ atomicWriteJsonFileSync(join(taskDir, "verify.json"), verify);
1265
+ atomicWriteFileSync(join(taskDir, "verify.log"), `Verification did not run: ${message}\n`);
1266
+ atomicWriteFileSync(join(taskDir, "test.log"), `(not run)\nExit code: not run\n${message}\n`);
1267
+ atomicWriteFileSync(join(taskDir, "git.diff"), "(task failed before change capture)\n");
1268
+ atomicWriteFileSync(join(taskDir, "diff.patch"), "(task failed before change capture)\n");
1269
+ atomicWriteJsonFileSync(join(taskDir, "file-stats.json"), {
1181
1270
  task_id: taskId,
1182
1271
  additions: 0,
1183
1272
  deletions: 0,
1184
1273
  files: [],
1185
- }, null, 2), "utf-8");
1274
+ });
1186
1275
  const result = {
1187
1276
  task_id: taskId,
1188
1277
  status: "failed",
@@ -1208,8 +1297,8 @@ function failBeforeExecution(taskId: string, taskDir: string, message: string, c
1208
1297
  safe_followup_prompt: "Inspect result.json and error.log, correct the task metadata or configuration, and retry without widening repository scope.",
1209
1298
  next_steps: ["Fix task metadata or configuration and retry the task."],
1210
1299
  };
1211
- writeFileSync(join(taskDir, "result.json"), JSON.stringify(result, null, 2), "utf-8");
1212
- writeFileSync(join(taskDir, "result.md"), `# PatchWarden Task Result\n\n## Status\nfailed\n\n## Summary\n${message}\n`, "utf-8");
1300
+ atomicWriteJsonFileSync(join(taskDir, "result.json"), result);
1301
+ atomicWriteFileSync(join(taskDir, "result.md"), `# PatchWarden Task Result\n\n## Status\nfailed\n\n## Summary\n${message}\n`);
1213
1302
  updateStatus(taskDir, {
1214
1303
  status: "failed",
1215
1304
  phase: "failed",
@@ -1242,13 +1331,11 @@ function setTaskPhase(
1242
1331
 
1243
1332
  function updateStatus(taskDir: string, patch: Record<string, unknown>): void {
1244
1333
  const statusFile = join(taskDir, "status.json");
1245
- const current = readStatus(statusFile);
1246
- const next = { ...current, ...patch, updated_at: new Date().toISOString() };
1247
- writeFileSync(statusFile, JSON.stringify(next, null, 2), "utf-8");
1334
+ updateTaskStatusFile(statusFile, patch);
1248
1335
  }
1249
1336
 
1250
- function readStatus(statusFile: string): Record<string, any> {
1251
- return JSON.parse(readFileSync(statusFile, "utf-8"));
1337
+ function readStatus(statusFile: string): TaskStatusRecord {
1338
+ return readTaskStatusFile(statusFile);
1252
1339
  }
1253
1340
 
1254
1341
  function gracefulKill(child: ChildProcess): void {
@@ -1262,10 +1349,13 @@ function forceKill(child: ChildProcess): void {
1262
1349
  if (!child.pid) return;
1263
1350
  try {
1264
1351
  if (process.platform === "win32") {
1265
- const result = spawnSync("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
1352
+ const systemRoot = process.env.SystemRoot || process.env.WINDIR || "C:\\Windows";
1353
+ const taskkill = resolveTrustedExecutable(`${systemRoot}\\System32\\taskkill.exe`, process.cwd());
1354
+ const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
1266
1355
  stdio: "ignore",
1267
1356
  timeout: 5000,
1268
1357
  windowsHide: true,
1358
+ env: buildChildEnvironment({ cwd: process.cwd() }),
1269
1359
  });
1270
1360
  if (result.status !== 0) child.kill("SIGKILL");
1271
1361
  } else {
@@ -1276,10 +1366,6 @@ function forceKill(child: ChildProcess): void {
1276
1366
  }
1277
1367
  }
1278
1368
 
1279
- function openStream(path?: string): WriteStream | null {
1280
- return path ? createWriteStream(path, { flags: "a" }) : null;
1281
- }
1282
-
1283
1369
  function appendBounded(current: string, next: string): string {
1284
1370
  const combined = current + next;
1285
1371
  return combined.length <= MAX_CAPTURE_CHARS
@@ -1290,3 +1376,17 @@ function appendBounded(current: string, next: string): string {
1290
1376
  function errorMessage(error: unknown): string {
1291
1377
  return error instanceof Error ? error.message : String(error);
1292
1378
  }
1379
+
1380
+ export function classifyAgentFailure(stderr: string): string | null {
1381
+ const normalized = stderr.toLowerCase();
1382
+ if (normalized.includes("insufficient balance") || normalized.includes("insufficient credits")) {
1383
+ return "provider_insufficient_balance";
1384
+ }
1385
+ if (normalized.includes("unauthorized") || normalized.includes("authentication failed") || normalized.includes("invalid api key")) {
1386
+ return "provider_authentication_failed";
1387
+ }
1388
+ if (normalized.includes("model not found") || normalized.includes("model access") || normalized.includes("permission denied")) {
1389
+ return "provider_model_unavailable";
1390
+ }
1391
+ return null;
1392
+ }