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
@@ -2,17 +2,20 @@ import {
2
2
  existsSync,
3
3
  readdirSync,
4
4
  readFileSync,
5
- writeFileSync,
6
- renameSync,
7
- unlinkSync,
8
5
  mkdirSync,
9
6
  statSync,
10
7
  type Dirent,
11
8
  } from "node:fs";
12
9
  import { join, resolve, dirname } from "node:path";
13
- import { getTasksDir, getConfig, type PatchWardenConfig } from "../config.js";
10
+ import { getTasksDir, getConfig, type PatchWardenConfig } from "../../config.js";
14
11
  import { diagnoseTask, type DiagnosisType, type DiagnosisConfidence, type SafeAction } from "./diagnoseTask.js";
15
- import { syncSubgoalOnTaskDone, readTaskGoalMeta } from "../goal/subgoalSync.js";
12
+ import { syncSubgoalOnTaskDone, readTaskGoalMeta } from "../../goal/subgoalSync.js";
13
+ import { mutateTaskStatus } from "../../runner/taskStatusStore.js";
14
+ import { writeTaskRuntime } from "../../runner/taskRuntime.js";
15
+ import type { TaskPhase } from "./createTask.js";
16
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../../utils/atomicFile.js";
17
+ import { appendBoundedTextFileSync } from "../../utils/boundedFile.js";
18
+ import { isActiveTaskStatus } from "./taskStates.js";
16
19
 
17
20
  // ── v0.7.0: reconcile_tasks types ──────────────────────────────────
18
21
 
@@ -22,6 +25,7 @@ export interface ReconcileTasksInput {
22
25
  max_age_minutes?: number;
23
26
  mode?: ReconcileMode;
24
27
  include_done_candidates?: boolean;
28
+ task_ids?: string[];
25
29
  }
26
30
 
27
31
  export interface ReconcileTaskReport {
@@ -33,7 +37,7 @@ export interface ReconcileTaskReport {
33
37
  reasons: string[];
34
38
  safe_actions: SafeAction[];
35
39
  age_seconds: number | null;
36
- action_taken: "left_unchanged" | "marked_failed_stale" | "marked_orphaned" | "marked_done_by_agent";
40
+ action_taken: "left_unchanged" | "marked_failed_stale" | "marked_orphaned" | "marked_done_by_agent" | "marked_canceled" | "marked_timed_out";
37
41
  previous_status: string | null;
38
42
  new_status: string | null;
39
43
  applied_at: string | null;
@@ -75,6 +79,9 @@ export function reconcileTasks(
75
79
  ? Math.min(input.max_age_minutes, 24 * 60)
76
80
  : DEFAULT_MAX_AGE_MINUTES;
77
81
  const includeDoneCandidates = input.include_done_candidates !== false; // default true
82
+ const requestedTaskIds = input.task_ids?.length
83
+ ? new Set(input.task_ids.filter((taskId) => /^task[-_][a-zA-Z0-9_-]+$/.test(taskId)))
84
+ : null;
78
85
  const maxAgeSeconds = maxAgeMinutes * 60;
79
86
 
80
87
  const tasksDir = getTasksDir(config);
@@ -118,8 +125,9 @@ export function reconcileTasks(
118
125
  const taskDirs = entries.filter((e) => e.isDirectory());
119
126
 
120
127
  for (const entry of taskDirs) {
121
- scanned += 1;
122
128
  const taskId = entry.name;
129
+ if (requestedTaskIds && !requestedTaskIds.has(taskId)) continue;
130
+ scanned += 1;
123
131
  const taskDir = resolve(tasksDir, taskId);
124
132
  const statusFile = join(taskDir, "status.json");
125
133
  if (!existsSync(statusFile)) continue;
@@ -133,18 +141,17 @@ export function reconcileTasks(
133
141
 
134
142
  const statusStr = typeof statusData.status === "string" ? statusData.status : "unknown";
135
143
 
136
- // Filter: only running / collecting_artifacts / (optionally) done_by_agent candidates
144
+ // Scan every persisted non-terminal lifecycle spelling, including legacy
145
+ // phase-as-status records left by older Watchers.
137
146
  const isCandidate =
138
- statusStr === "running" ||
139
- statusStr === "collecting_artifacts" ||
147
+ isActiveTaskStatus(statusStr) ||
140
148
  (includeDoneCandidates && statusStr === "done_by_agent");
141
149
  if (!isCandidate) continue;
142
150
 
143
- // Filter by age — use the oldest reliable timestamp on the task
151
+ // Use the oldest reliable timestamp for ordinary stale detection. Explicit
152
+ // cancel, persisted deadline, and Watcher-instance mismatch bypass this
153
+ // age gate so a restart cannot extend the task's total budget.
144
154
  const ageSeconds = taskAgeSeconds(taskDir, statusData, nowMs);
145
- if (ageSeconds !== null && ageSeconds < maxAgeSeconds) continue;
146
-
147
- candidates += 1;
148
155
 
149
156
  // Diagnose the task
150
157
  let diagnosis;
@@ -153,6 +160,14 @@ export function reconcileTasks(
153
160
  } catch {
154
161
  continue; // diagnosis failed — skip
155
162
  }
163
+ const cancellationRequested = statusData.cancel_requested === true || statusStr === "cancel_requested";
164
+ const deadlineExceeded = taskDeadlineExceeded(statusData, nowMs);
165
+ const urgentRecovery = cancellationRequested
166
+ || deadlineExceeded
167
+ || diagnosis.diagnosis === "orphaned_running";
168
+ if (ageSeconds !== null && ageSeconds < maxAgeSeconds && !urgentRecovery) continue;
169
+
170
+ candidates += 1;
156
171
 
157
172
  const report: ReconcileTaskReport = {
158
173
  task_id: taskId,
@@ -188,7 +203,9 @@ export function reconcileTasks(
188
203
  //
189
204
  // Anything else is left_unchanged and recorded for audit.
190
205
 
191
- if (mode === "safe_fix" && diagnosis.confidence === "high") {
206
+ const deterministicRecovery = !diagnosis.evidence.watcher_owns_task
207
+ && (cancellationRequested || deadlineExceeded);
208
+ if (mode === "safe_fix" && (diagnosis.confidence === "high" || deterministicRecovery)) {
192
209
  if (diagnosis.evidence.watcher_owns_task) {
193
210
  // Hard rule: do not touch tasks the live watcher still owns.
194
211
  skippedActiveWatcher += 1;
@@ -206,10 +223,9 @@ export function reconcileTasks(
206
223
  report.applied_by = "reconcile_tasks";
207
224
  reconciled += 1;
208
225
  } else {
209
- // Diagnosis type does not map to a safe_fix action — leave unchanged
210
226
  report.reasons = [
211
227
  ...report.reasons,
212
- `safe_fix skipped: diagnosis "${diagnosis.diagnosis}" has no automatic safe_fix action`,
228
+ `safe_fix skipped: ${fixResult.skip_reason || "task state could not be updated safely"}`,
213
229
  ];
214
230
  skippedLowConfidence += 1;
215
231
  }
@@ -254,8 +270,13 @@ interface SafeFixResult {
254
270
  previous_status: string;
255
271
  new_status: string;
256
272
  applied_at: string;
273
+ skip_reason: string | null;
257
274
  }
258
275
 
276
+ type SafeFixMutationOutcome =
277
+ | { applied: true }
278
+ | { applied: false; reason: string };
279
+
259
280
  function applySafeFix(
260
281
  taskDir: string,
261
282
  taskId: string,
@@ -280,7 +301,14 @@ function applySafeFix(
280
301
  let newStatus: string | null = null;
281
302
  let actionTaken: ReconcileTaskReport["action_taken"] = "left_unchanged";
282
303
 
283
- switch (diagnosis) {
304
+ const cancellationRequested = currentStatus.cancel_requested === true || previousStatus === "cancel_requested";
305
+ if (!evidence.watcher_owns_task && cancellationRequested) {
306
+ newStatus = "canceled";
307
+ actionTaken = "marked_canceled";
308
+ } else if (!evidence.watcher_owns_task && taskDeadlineExceeded(currentStatus)) {
309
+ newStatus = "timeout";
310
+ actionTaken = "marked_timed_out";
311
+ } else switch (diagnosis) {
284
312
  case "stale_running":
285
313
  newStatus = "failed_stale";
286
314
  actionTaken = "marked_failed_stale";
@@ -306,6 +334,7 @@ function applySafeFix(
306
334
  previous_status: previousStatus,
307
335
  new_status: previousStatus,
308
336
  applied_at: new Date().toISOString(),
337
+ skip_reason: `diagnosis "${diagnosis}" has no automatic safe_fix action`,
309
338
  };
310
339
  }
311
340
 
@@ -313,75 +342,114 @@ function applySafeFix(
313
342
  const statusFile = join(taskDir, "status.json");
314
343
  const backupFile = join(taskDir, "status.json.bak");
315
344
 
316
- // 1. Write status.json.bak with current contents (overwrite any prior backup)
345
+ // Revalidate and update under the shared task status lock. Any heartbeat or
346
+ // ownership-related status change after diagnosis makes this repair stale.
317
347
  try {
318
- const currentRaw = readFileSync(statusFile, "utf-8");
319
- writeFileSync(backupFile, currentRaw, "utf-8");
320
- } catch {
321
- // If we cannot back up, refuse to apply the fix.
322
- return {
323
- applied: false,
324
- action_taken: "left_unchanged",
325
- previous_status: previousStatus,
326
- new_status: previousStatus,
327
- applied_at: appliedAt,
328
- };
329
- }
348
+ const outcome = mutateTaskStatus<SafeFixMutationOutcome>(statusFile, (current) => {
349
+ if (JSON.stringify(current) !== JSON.stringify(currentStatus)) {
350
+ return { result: { applied: false as const, reason: "task status changed after diagnosis" } };
351
+ }
330
352
 
331
- // 2. Build the new status record with full audit trail
332
- const next: Record<string, unknown> = {
333
- ...currentStatus,
334
- status: newStatus,
335
- phase: newStatus,
336
- updated_at: appliedAt,
337
- last_heartbeat_at: appliedAt,
338
- finished_at: appliedAt,
339
- // v0.7.0 audit fields
340
- previous_status: previousStatus,
341
- diagnosis: {
342
- type: diagnosis,
343
- confidence: "high" as DiagnosisConfidence,
344
- applied_by: "reconcile_tasks",
345
- applied_at: appliedAt,
346
- reasons,
347
- evidence: {
348
- heartbeat_age_seconds: evidence.heartbeat_age_seconds,
349
- stdout_age_seconds: evidence.stdout_age_seconds,
350
- child_pid: evidence.child_pid,
351
- child_pid_alive: evidence.child_pid_alive,
352
- watcher_instance_id: evidence.watcher_instance_id,
353
- current_watcher_instance_id: evidence.current_watcher_instance_id,
354
- },
355
- },
356
- };
353
+ const lockedPreviousStatus = typeof current.status === "string" ? current.status : "unknown";
354
+ const next: Record<string, unknown> = {
355
+ ...current,
356
+ status: newStatus,
357
+ phase: newStatus,
358
+ updated_at: appliedAt,
359
+ last_heartbeat_at: appliedAt,
360
+ finished_at: appliedAt,
361
+ previous_status: lockedPreviousStatus,
362
+ diagnosis: {
363
+ type: diagnosis,
364
+ confidence: "high" as DiagnosisConfidence,
365
+ applied_by: "reconcile_tasks",
366
+ applied_at: appliedAt,
367
+ reasons,
368
+ evidence: {
369
+ heartbeat_age_seconds: evidence.heartbeat_age_seconds,
370
+ stdout_age_seconds: evidence.stdout_age_seconds,
371
+ child_pid: evidence.child_pid,
372
+ child_pid_alive: evidence.child_pid_alive,
373
+ watcher_instance_id: evidence.watcher_instance_id,
374
+ current_watcher_instance_id: evidence.current_watcher_instance_id,
375
+ },
376
+ },
377
+ process_cleanup_required: !evidence.watcher_owns_task,
378
+ process_cleanup_reason: !evidence.watcher_owns_task
379
+ ? "The previous runner is no longer owned by this Watcher; child-process termination could not be confirmed and no untrusted PID was killed."
380
+ : null,
381
+ };
357
382
 
358
- // For done_by_agent, set acceptance_status=pending (per roadmap 3.6)
359
- if (newStatus === "done_by_agent") {
360
- next.acceptance_status = "pending";
361
- next.legacy_status = "done";
362
- }
383
+ if (newStatus === "canceled") {
384
+ next.canceled_at = appliedAt;
385
+ next.cancel_reason = "Cancellation converged after the original task runner stopped responding.";
386
+ next.termination_reason = "canceled";
387
+ next.error_code = "runner_lost_during_cancel";
388
+ } else if (actionTaken === "marked_timed_out") {
389
+ next.error = `Task exceeded its configured timeout of ${Number(current.timeout_seconds)} seconds after the original runner stopped responding.`;
390
+ next.termination_reason = "timeout";
391
+ next.error_code = "task_timeout";
392
+ } else if (diagnosis === "artifact_collection_stuck") {
393
+ next.error = "Artifact collection was interrupted and could not be safely resumed after the original runner stopped responding.";
394
+ next.error_code = "artifact_collection_interrupted";
395
+ } else if (diagnosis === "orphaned_running") {
396
+ next.error = "The original task runner was lost and its child process ownership could not be re-established.";
397
+ next.error_code = "agent_lost";
398
+ } else if (diagnosis === "stale_running") {
399
+ next.error = "The task runner heartbeat expired and no current Runner ownership could be proven.";
400
+ next.error_code = "agent_lost";
401
+ }
363
402
 
364
- // 3. Atomic write: write to temp file then rename
365
- const tmpFile = join(taskDir, "status.json.tmp");
366
- try {
367
- writeFileSync(tmpFile, JSON.stringify(next, null, 2), "utf-8");
368
- renameSync(tmpFile, statusFile);
369
- } catch {
370
- // Best-effort cleanup of temp file
371
- try {
372
- if (existsSync(tmpFile)) {
373
- unlinkSync(tmpFile);
403
+ if (newStatus === "done_by_agent") {
404
+ next.acceptance_status = "pending";
405
+ next.legacy_status = "done";
374
406
  }
375
- } catch { /* ignore */ }
407
+
408
+ // The backup is created while the same lock protects the exact record
409
+ // being replaced, so it cannot capture an unrelated newer state.
410
+ if (!existsSync(backupFile)) atomicWriteFileSync(backupFile, readFileSync(statusFile, "utf-8"));
411
+ return {
412
+ next,
413
+ result: { applied: true as const },
414
+ };
415
+ });
416
+ if (!outcome.applied) {
417
+ return {
418
+ applied: false,
419
+ action_taken: "left_unchanged",
420
+ previous_status: previousStatus,
421
+ new_status: previousStatus,
422
+ applied_at: appliedAt,
423
+ skip_reason: outcome.reason,
424
+ };
425
+ }
426
+ } catch {
376
427
  return {
377
428
  applied: false,
378
429
  action_taken: "left_unchanged",
379
430
  previous_status: previousStatus,
380
431
  new_status: previousStatus,
381
432
  applied_at: appliedAt,
433
+ skip_reason: "task status could not be locked, backed up, and updated",
382
434
  };
383
435
  }
384
436
 
437
+ if (newStatus !== "done_by_agent") {
438
+ writeRecoveryArtifacts(taskDir, taskId, currentStatus, newStatus, actionTaken, appliedAt);
439
+ }
440
+
441
+ // Keep the persisted runtime view terminal as well. get_task_status merges
442
+ // runtime.json over status.json, so leaving an old executing phase here
443
+ // would make a successfully reconciled task appear active again.
444
+ writeTaskRuntime(taskDir, {
445
+ phase: newStatus as TaskPhase,
446
+ current_command: null,
447
+ last_heartbeat_at: appliedAt,
448
+ child_pid: undefined,
449
+ child_started_at: undefined,
450
+ child_owned_by_runner_instance_id: undefined,
451
+ });
452
+
385
453
  // v0.8.0: 当状态变为 done_by_agent 时,同步关联 subgoal 状态(running → done_by_agent)
386
454
  if (newStatus === "done_by_agent") {
387
455
  const goalMeta = readTaskGoalMeta(taskDir);
@@ -396,9 +464,95 @@ function applySafeFix(
396
464
  previous_status: previousStatus,
397
465
  new_status: newStatus,
398
466
  applied_at: appliedAt,
467
+ skip_reason: null,
399
468
  };
400
469
  }
401
470
 
471
+ function writeRecoveryArtifacts(
472
+ taskDir: string,
473
+ taskId: string,
474
+ status: Record<string, unknown>,
475
+ newStatus: string,
476
+ actionTaken: ReconcileTaskReport["action_taken"],
477
+ recoveredAt: string,
478
+ ): void {
479
+ const timeoutSeconds = Number(status.timeout_seconds);
480
+ const summary = actionTaken === "marked_timed_out"
481
+ ? `Task exceeded its configured timeout of ${timeoutSeconds} seconds after the original runner stopped responding.`
482
+ : actionTaken === "marked_canceled"
483
+ ? "Cancellation converged after the original task runner stopped responding."
484
+ : `Task was reconciled to ${newStatus} after the original runner stopped responding.`;
485
+ const errorCode = actionTaken === "marked_timed_out"
486
+ ? "task_timeout"
487
+ : actionTaken === "marked_canceled"
488
+ ? "runner_lost_during_cancel"
489
+ : newStatus === "orphaned"
490
+ ? "agent_lost"
491
+ : newStatus === "failed_stale" && String(status.phase || status.status) === "collecting_artifacts"
492
+ ? "artifact_collection_interrupted"
493
+ : "agent_lost";
494
+ const requestedCommands = Array.isArray(status.verify_commands)
495
+ ? status.verify_commands.map(String)
496
+ : [];
497
+ const artifacts = {
498
+ "verify.json": {
499
+ status: "failed",
500
+ requested_commands: requestedCommands,
501
+ commands: [],
502
+ failure_reason: "runner_recovery",
503
+ error_code: errorCode,
504
+ },
505
+ "result.json": {
506
+ task_id: taskId,
507
+ status: newStatus,
508
+ agent: status.agent || "",
509
+ repo_path: status.repo_path || "",
510
+ resolved_repo_path: status.resolved_repo_path || "",
511
+ summary,
512
+ artifact_status: "partial",
513
+ changed_files: [],
514
+ diff_available: false,
515
+ verify_status: "failed",
516
+ failure_reason: "runner_recovery",
517
+ error_code: errorCode,
518
+ termination_reason: actionTaken === "marked_timed_out"
519
+ ? "timeout"
520
+ : actionTaken === "marked_canceled"
521
+ ? "canceled"
522
+ : "runner_stale",
523
+ recovered_at: recoveredAt,
524
+ warnings: ["The original runner stopped before complete change and verification evidence could be collected."],
525
+ },
526
+ };
527
+ for (const [name, value] of Object.entries(artifacts)) {
528
+ const path = join(taskDir, name);
529
+ if (!existsSync(path)) atomicWriteJsonFileSync(path, value);
530
+ }
531
+ const textArtifacts: Record<string, string> = {
532
+ "result.md": `# PatchWarden Task Result\n\n## Status\n${newStatus}\n\n## Summary\n${summary}\n\n## Artifact Status\npartial\n`,
533
+ "test.log": `(not run)\nExit code: not run\n${summary}\n`,
534
+ "git.diff": `(unavailable: ${summary})\n`,
535
+ "diff.patch": `(unavailable: ${summary})\n`,
536
+ };
537
+ for (const [name, content] of Object.entries(textArtifacts)) {
538
+ const path = join(taskDir, name);
539
+ if (!existsSync(path)) atomicWriteFileSync(path, content);
540
+ }
541
+ }
542
+
543
+ function taskDeadlineExceeded(status: Record<string, unknown>, nowMs = Date.now()): boolean {
544
+ const timeoutSeconds = Number(status.timeout_seconds);
545
+ const startedAt = typeof status.started_at === "string"
546
+ ? Date.parse(status.started_at)
547
+ : typeof status.created_at === "string"
548
+ ? Date.parse(status.created_at)
549
+ : NaN;
550
+ return Number.isFinite(startedAt)
551
+ && Number.isFinite(timeoutSeconds)
552
+ && timeoutSeconds > 0
553
+ && nowMs >= startedAt + timeoutSeconds * 1000;
554
+ }
555
+
402
556
  // ── reconcile.log writer ───────────────────────────────────────────
403
557
 
404
558
  function writeReconcileLog(
@@ -428,11 +582,8 @@ function writeReconcileLog(
428
582
  evidence: report.evidence_summary,
429
583
  }));
430
584
  }
431
- // Append — do not overwrite history
432
- const existing = existsSync(logPath) ? readFileSync(logPath, "utf-8") : "";
433
- const nextContent = existing + (existing && !existing.endsWith("\n") ? "\n" : "") + lines.join("\n") + "\n";
434
585
  try {
435
- writeFileSync(logPath, nextContent, "utf-8");
586
+ appendBoundedTextFileSync(logPath, lines.join("\n") + "\n");
436
587
  } catch {
437
588
  // If we cannot write the log, the status change still happened —
438
589
  // the status.json itself contains the diagnosis audit fields.
@@ -0,0 +1,69 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getTasksDir, getConfig } from "../../config.js";
4
+ import { guardReadPath } from "../../security/pathGuard.js";
5
+ import { createTask } from "./createTask.js";
6
+ import type { ChangePolicy } from "../taskTemplates.js";
7
+
8
+ export async function retryTask(taskId: string) {
9
+ const config = getConfig();
10
+ const tasksDir = getTasksDir(config);
11
+ const taskDir = join(tasksDir, taskId);
12
+ const statusFile = join(taskDir, "status.json");
13
+
14
+ guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
15
+
16
+ if (!existsSync(statusFile)) {
17
+ throw new Error(`Task not found: "${taskId}"`);
18
+ }
19
+
20
+ const parsed: unknown = JSON.parse(readFileSync(statusFile, "utf-8"));
21
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
22
+ throw new Error(`Task "${taskId}" has an invalid status record.`);
23
+ }
24
+ const data = parsed as Record<string, unknown>;
25
+
26
+ if (typeof data.plan_id !== "string" || !data.plan_id ||
27
+ typeof data.agent !== "string" || !data.agent) {
28
+ throw new Error(`Task "${taskId}" is missing plan_id or agent. Cannot retry.`);
29
+ }
30
+
31
+ const priorRetryCount = typeof data.retry_count === "number" &&
32
+ Number.isSafeInteger(data.retry_count) && data.retry_count >= 0
33
+ ? data.retry_count
34
+ : 0;
35
+ const planSource = data.plan_source === "inline" || data.plan_source === "template"
36
+ ? data.plan_source
37
+ : "saved";
38
+ const changePolicy: ChangePolicy = data.change_policy === "no_changes"
39
+ ? "no_changes"
40
+ : "repo_scoped_changes";
41
+
42
+ // Create a new task with the same parameters
43
+ const newTask = await createTask({
44
+ plan_id: data.plan_id,
45
+ agent: data.agent,
46
+ repo_path: typeof data.repo_path === "string" ? data.repo_path : undefined,
47
+ test_command: typeof data.test_command === "string" ? data.test_command : undefined,
48
+ verify_commands: Array.isArray(data.verify_commands)
49
+ ? data.verify_commands.filter((value): value is string => typeof value === "string")
50
+ : undefined,
51
+ timeout_seconds: typeof data.timeout_seconds === "number" ? data.timeout_seconds : undefined,
52
+ retry_metadata: {
53
+ retry_of: taskId,
54
+ retry_count: priorRetryCount + 1,
55
+ plan_source: planSource,
56
+ template: typeof data.template === "string" ? data.template : null,
57
+ change_policy: changePolicy,
58
+ },
59
+ });
60
+
61
+ return {
62
+ original_task_id: taskId,
63
+ new_task_id: newTask.task_id,
64
+ plan_id: newTask.plan_id,
65
+ agent: newTask.agent,
66
+ status: newTask.status,
67
+ message: `New task created from retry of ${taskId}. Original task is unchanged.`,
68
+ };
69
+ }