patchwarden 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (573) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +152 -25
  3. package/README.md +120 -23
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentStore.js +42 -34
  6. package/dist/config.d.ts +15 -0
  7. package/dist/config.js +66 -13
  8. package/dist/control/fileManager.d.ts +2 -0
  9. package/dist/control/fileManager.js +39 -0
  10. package/dist/control/middleware/auth.d.ts +6 -0
  11. package/dist/control/middleware/auth.js +14 -5
  12. package/dist/control/middleware/static.js +16 -3
  13. package/dist/control/routeTable.d.ts +24 -0
  14. package/dist/control/routeTable.js +263 -0
  15. package/dist/control/routes/audit.js +17 -7
  16. package/dist/control/routes/evidence.js +1 -1
  17. package/dist/control/routes/lineage.js +1 -1
  18. package/dist/control/routes/process.d.ts +7 -0
  19. package/dist/control/routes/process.js +329 -42
  20. package/dist/control/routes/sessions.d.ts +9 -1
  21. package/dist/control/routes/sessions.js +84 -21
  22. package/dist/control/routes/status.d.ts +46 -1
  23. package/dist/control/routes/status.js +72 -15
  24. package/dist/control/routes/taskActions.js +24 -60
  25. package/dist/control/routes/tasks.d.ts +4 -0
  26. package/dist/control/routes/tasks.js +61 -62
  27. package/dist/control/routes/workspace.js +50 -23
  28. package/dist/control/runtime.d.ts +3 -2
  29. package/dist/control/runtime.js +63 -30
  30. package/dist/control/server.js +36 -365
  31. package/dist/control/shared.d.ts +25 -1
  32. package/dist/control/shared.js +64 -11
  33. package/dist/direct/directAudit.js +234 -225
  34. package/dist/direct/directGuards.d.ts +2 -1
  35. package/dist/direct/directGuards.js +42 -29
  36. package/dist/direct/directPatch.d.ts +6 -1
  37. package/dist/direct/directPatch.js +55 -8
  38. package/dist/direct/directSessionStore.d.ts +16 -6
  39. package/dist/direct/directSessionStore.js +127 -35
  40. package/dist/direct/directVerification.js +8 -16
  41. package/dist/doctor.js +62 -52
  42. package/dist/goal/acceptanceEngine.d.ts +1 -1
  43. package/dist/goal/goalProgress.js +75 -69
  44. package/dist/goal/goalReport.js +5 -12
  45. package/dist/goal/goalStatus.d.ts +2 -0
  46. package/dist/goal/goalStatus.js +10 -3
  47. package/dist/goal/goalStore.d.ts +9 -2
  48. package/dist/goal/goalStore.js +152 -45
  49. package/dist/goal/handoffExport.js +6 -6
  50. package/dist/goal/specKitImport.js +97 -96
  51. package/dist/goal/subgoalSync.js +11 -9
  52. package/dist/goal/worktreeManager.d.ts +2 -0
  53. package/dist/goal/worktreeManager.js +209 -136
  54. package/dist/httpServer.js +111 -32
  55. package/dist/logging.js +5 -3
  56. package/dist/policy/projectPolicy.js +5 -2
  57. package/dist/release/releaseGate.d.ts +1 -1
  58. package/dist/release/releaseGate.js +78 -25
  59. package/dist/runner/agentInvocation.d.ts +14 -0
  60. package/dist/runner/agentInvocation.js +96 -4
  61. package/dist/runner/changeCapture.d.ts +13 -5
  62. package/dist/runner/changeCapture.js +243 -61
  63. package/dist/runner/postTaskCleanup.js +48 -19
  64. package/dist/runner/processSecurity.d.ts +56 -0
  65. package/dist/runner/processSecurity.js +373 -0
  66. package/dist/runner/runTask.d.ts +1 -1
  67. package/dist/runner/runTask.js +115 -87
  68. package/dist/runner/simpleProcess.d.ts +3 -0
  69. package/dist/runner/simpleProcess.js +50 -36
  70. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  71. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  72. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  73. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  74. package/dist/runner/taskStatusStore.d.ts +11 -0
  75. package/dist/runner/taskStatusStore.js +24 -0
  76. package/dist/runner/watch.d.ts +14 -1
  77. package/dist/runner/watch.js +256 -48
  78. package/dist/security/contentRedaction.js +2 -2
  79. package/dist/security/discoveryTokenStore.d.ts +3 -2
  80. package/dist/security/discoveryTokenStore.js +17 -2
  81. package/dist/security/loopbackHost.d.ts +2 -0
  82. package/dist/security/loopbackHost.js +16 -0
  83. package/dist/security/pathGuard.js +32 -17
  84. package/dist/security/planGuard.js +2 -1
  85. package/dist/security/runtimeGuard.js +9 -6
  86. package/dist/security/secretComparison.d.ts +3 -0
  87. package/dist/security/secretComparison.js +11 -0
  88. package/dist/security/sensitiveGuard.d.ts +5 -0
  89. package/dist/security/sensitiveGuard.js +27 -8
  90. package/dist/security/toolInvocationGuard.d.ts +2 -2
  91. package/dist/security/toolInvocationGuard.js +1 -1
  92. package/dist/security/workspaceRootGuard.d.ts +8 -0
  93. package/dist/security/workspaceRootGuard.js +47 -0
  94. package/dist/smoke-test.js +24 -23
  95. package/dist/tools/catalog/index.d.ts +4 -0
  96. package/dist/tools/catalog/index.js +4 -0
  97. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  98. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  99. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  100. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  101. package/dist/tools/definitions/toolDefs.js +1177 -0
  102. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  103. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  104. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
  105. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
  106. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  107. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
  108. package/dist/tools/diagnostics/index.d.ts +6 -0
  109. package/dist/tools/diagnostics/index.js +6 -0
  110. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  111. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  112. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  113. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  114. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  115. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  116. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  117. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  118. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  119. package/dist/tools/direct/index.d.ts +3 -0
  120. package/dist/tools/direct/index.js +3 -0
  121. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  122. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  123. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  124. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  125. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  126. package/dist/tools/discovery/index.d.ts +3 -0
  127. package/dist/tools/discovery/index.js +3 -0
  128. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  129. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  130. package/dist/tools/dispatch/coreDispatch.js +32 -31
  131. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  132. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  133. package/dist/tools/dispatch/directDispatch.js +11 -10
  134. package/dist/tools/dispatch/goalDispatch.js +5 -4
  135. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  136. package/dist/tools/dispatch/validation.d.ts +7 -0
  137. package/dist/tools/dispatch/validation.js +71 -0
  138. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  139. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  140. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  141. package/dist/tools/goals/index.d.ts +3 -0
  142. package/dist/tools/goals/index.js +3 -0
  143. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  144. package/dist/tools/registry.d.ts +7 -11
  145. package/dist/tools/registry.js +17 -1178
  146. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  147. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  148. package/dist/tools/release/index.d.ts +2 -0
  149. package/dist/tools/release/index.js +2 -0
  150. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  151. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  152. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  153. package/dist/tools/tasks/cancelTask.js +85 -0
  154. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
  155. package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
  156. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  157. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  158. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  159. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  160. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  161. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  162. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  163. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  164. package/dist/tools/tasks/index.d.ts +18 -0
  165. package/dist/tools/tasks/index.js +18 -0
  166. package/dist/tools/tasks/killTask.d.ts +1 -0
  167. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  168. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  169. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  170. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  171. package/dist/tools/tasks/retryTask.js +60 -0
  172. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  173. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
  174. package/dist/tools/tasks/runVerification.js +38 -0
  175. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  176. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  177. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  178. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  179. package/dist/tools/workspace/applyPatch.js +53 -0
  180. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  181. package/dist/tools/workspace/index.d.ts +10 -0
  182. package/dist/tools/workspace/index.js +10 -0
  183. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  184. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  185. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  186. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  187. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  188. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  189. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  190. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  191. package/dist/tools/workspace/syncFile.js +134 -0
  192. package/dist/utils/atomicFile.d.ts +7 -0
  193. package/dist/utils/atomicFile.js +48 -0
  194. package/dist/utils/boundedFile.d.ts +8 -0
  195. package/dist/utils/boundedFile.js +93 -0
  196. package/dist/utils/lockedJsonFile.d.ts +15 -0
  197. package/dist/utils/lockedJsonFile.js +224 -0
  198. package/dist/utils/platform.d.ts +11 -0
  199. package/dist/utils/platform.js +26 -0
  200. package/dist/utils/stableJson.d.ts +2 -0
  201. package/dist/utils/stableJson.js +12 -0
  202. package/dist/version.d.ts +3 -2
  203. package/dist/version.js +3 -2
  204. package/dist/watcherStatus.d.ts +2 -2
  205. package/dist/watcherStatus.js +18 -0
  206. package/docs/CODE_WIKI.md +808 -143
  207. package/docs/architecture.md +15 -55
  208. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  209. package/docs/control-center/README.md +4 -4
  210. package/docs/dashboard-overview.md +4 -4
  211. package/docs/desktop-app.md +175 -0
  212. package/docs/evidence-pack-schema.md +3 -3
  213. package/docs/open-source-application.md +17 -11
  214. package/docs/release-checklist.md +1 -1
  215. package/docs/release-evidence.md +39 -29
  216. package/docs/task-safe-review-workflow.md +5 -5
  217. package/docs/threat-model.md +19 -1
  218. package/examples/config.example.json +4 -2
  219. package/package.json +27 -10
  220. package/scripts/README.md +15 -6
  221. package/scripts/build.js +11 -0
  222. package/scripts/checks/brand-check.js +3 -0
  223. package/scripts/checks/build-output-check.js +37 -0
  224. package/scripts/checks/control-center-smoke.js +91 -6
  225. package/scripts/checks/control-smoke.js +19 -8
  226. package/scripts/checks/http-mcp-smoke.js +82 -4
  227. package/scripts/checks/lifecycle-smoke.js +10 -10
  228. package/scripts/checks/mcp-manifest-check.js +1 -1
  229. package/scripts/checks/mcp-smoke.js +27 -3
  230. package/scripts/checks/package-manifest-check.js +20 -7
  231. package/scripts/checks/unit-tests.js +3 -5
  232. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  233. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  234. package/scripts/control/manage-patchwarden.ps1 +69 -4
  235. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  236. package/scripts/control/run-background-supervisor.ps1 +41 -0
  237. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  238. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  239. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  240. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  241. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  242. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  243. package/scripts/lib/clean-generated-output.js +11 -0
  244. package/scripts/lib/file-discovery.js +12 -0
  245. package/scripts/release/desktop-preflight.js +211 -0
  246. package/scripts/release/pack-clean.js +59 -14
  247. package/src/assessments/agentAssessor.ts +26 -12
  248. package/src/assessments/assessmentStore.ts +52 -40
  249. package/src/config.ts +70 -13
  250. package/src/control/fileManager.ts +40 -0
  251. package/src/control/middleware/auth.ts +22 -4
  252. package/src/control/middleware/static.ts +16 -3
  253. package/src/control/routeTable.ts +301 -0
  254. package/src/control/routes/audit.ts +14 -6
  255. package/src/control/routes/evidence.ts +1 -1
  256. package/src/control/routes/lineage.ts +1 -1
  257. package/src/control/routes/process.ts +341 -40
  258. package/src/control/routes/sessions.ts +88 -25
  259. package/src/control/routes/status.ts +97 -17
  260. package/src/control/routes/taskActions.ts +24 -58
  261. package/src/control/routes/tasks.ts +66 -58
  262. package/src/control/routes/workspace.ts +56 -25
  263. package/src/control/runtime.ts +69 -33
  264. package/src/control/server.ts +36 -342
  265. package/src/control/shared.ts +77 -11
  266. package/src/direct/directAudit.ts +17 -9
  267. package/src/direct/directGuards.ts +72 -44
  268. package/src/direct/directPatch.ts +95 -8
  269. package/src/direct/directSessionStore.ts +211 -55
  270. package/src/direct/directVerification.ts +9 -23
  271. package/src/doctor.ts +73 -59
  272. package/src/goal/acceptanceEngine.ts +1 -1
  273. package/src/goal/goalProgress.ts +17 -13
  274. package/src/goal/goalReport.ts +5 -13
  275. package/src/goal/goalStatus.ts +19 -3
  276. package/src/goal/goalStore.ts +241 -54
  277. package/src/goal/handoffExport.ts +6 -6
  278. package/src/goal/specKitImport.ts +12 -12
  279. package/src/goal/subgoalSync.ts +11 -8
  280. package/src/goal/worktreeManager.ts +139 -22
  281. package/src/httpServer.ts +113 -30
  282. package/src/logging.ts +10 -3
  283. package/src/policy/projectPolicy.ts +5 -2
  284. package/src/release/releaseGate.ts +74 -27
  285. package/src/runner/agentInvocation.ts +126 -4
  286. package/src/runner/changeCapture.ts +291 -57
  287. package/src/runner/postTaskCleanup.ts +41 -17
  288. package/src/runner/processSecurity.ts +433 -0
  289. package/src/runner/runTask.ts +138 -91
  290. package/src/runner/simpleProcess.ts +66 -31
  291. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  292. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  293. package/src/runner/taskStatusStore.ts +54 -0
  294. package/src/runner/watch.ts +281 -51
  295. package/src/security/contentRedaction.ts +2 -2
  296. package/src/security/discoveryTokenStore.ts +18 -4
  297. package/src/security/loopbackHost.ts +19 -0
  298. package/src/security/pathGuard.ts +35 -16
  299. package/src/security/planGuard.ts +2 -1
  300. package/src/security/runtimeGuard.ts +9 -6
  301. package/src/security/secretComparison.ts +13 -0
  302. package/src/security/sensitiveGuard.ts +27 -8
  303. package/src/security/toolInvocationGuard.ts +2 -2
  304. package/src/security/workspaceRootGuard.ts +53 -0
  305. package/src/smoke-test.ts +25 -24
  306. package/src/tools/catalog/index.ts +4 -0
  307. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  308. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  309. package/src/tools/definitions/toolDefs.ts +1264 -0
  310. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  311. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
  312. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
  313. package/src/tools/diagnostics/index.ts +6 -0
  314. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  315. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  316. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  317. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  318. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  319. package/src/tools/direct/index.ts +3 -0
  320. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  321. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  322. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  323. package/src/tools/discovery/index.ts +3 -0
  324. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  325. package/src/tools/dispatch/coreDispatch.ts +36 -31
  326. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  327. package/src/tools/dispatch/directDispatch.ts +11 -10
  328. package/src/tools/dispatch/goalDispatch.ts +5 -4
  329. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  330. package/src/tools/dispatch/validation.ts +78 -0
  331. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  332. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  333. package/src/tools/goals/index.ts +3 -0
  334. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  335. package/src/tools/registry.ts +17 -1261
  336. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  337. package/src/tools/release/index.ts +2 -0
  338. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  339. package/src/tools/tasks/cancelTask.ts +109 -0
  340. package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
  341. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  342. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  343. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  344. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  345. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  346. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  347. package/src/tools/tasks/index.ts +18 -0
  348. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  349. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  350. package/src/tools/tasks/retryTask.ts +69 -0
  351. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
  352. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  353. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  354. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  355. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  356. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  357. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  358. package/src/tools/workspace/index.ts +10 -0
  359. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  360. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  361. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  362. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  363. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  364. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  365. package/src/tools/workspace/syncFile.ts +292 -0
  366. package/src/utils/atomicFile.ts +58 -0
  367. package/src/utils/boundedFile.ts +116 -0
  368. package/src/utils/lockedJsonFile.ts +251 -0
  369. package/src/utils/platform.ts +31 -0
  370. package/src/utils/stableJson.ts +12 -0
  371. package/src/version.ts +3 -2
  372. package/src/watcherStatus.ts +19 -1
  373. package/ui/desktop-bootstrap.js +11 -0
  374. package/ui/desktop-bridge.js +36 -0
  375. package/ui/desktop.css +160 -0
  376. package/ui/getting-started.js +93 -0
  377. package/ui/i18n.js +484 -0
  378. package/ui/log-parser.js +30 -0
  379. package/ui/pages/audit.html +12 -0
  380. package/ui/pages/dashboard.html +90 -147
  381. package/ui/pages/direct-sessions.html +123 -26
  382. package/ui/pages/getting-started.html +43 -0
  383. package/ui/pages/logs.html +105 -22
  384. package/ui/pages/settings.html +101 -0
  385. package/ui/pages/task-detail.html +12 -0
  386. package/ui/pages/tasks.html +54 -8
  387. package/ui/pages/workspace.html +38 -1
  388. package/ui/settings.js +245 -0
  389. package/PatchWarden-Control.cmd +0 -6
  390. package/Restart-PatchWarden-Control.cmd +0 -6
  391. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  392. package/dist/test/unit/acceptance-engine.test.js +0 -228
  393. package/dist/test/unit/agent-router.test.d.ts +0 -1
  394. package/dist/test/unit/agent-router.test.js +0 -287
  395. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  396. package/dist/test/unit/android-doctor.test.js +0 -118
  397. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  398. package/dist/test/unit/apply-patch.test.js +0 -225
  399. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  400. package/dist/test/unit/audit-checks.test.js +0 -350
  401. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  402. package/dist/test/unit/chinese-path.test.js +0 -91
  403. package/dist/test/unit/command-guard.test.d.ts +0 -1
  404. package/dist/test/unit/command-guard.test.js +0 -160
  405. package/dist/test/unit/create-task.test.d.ts +0 -1
  406. package/dist/test/unit/create-task.test.js +0 -197
  407. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  408. package/dist/test/unit/diagnose-task.test.js +0 -457
  409. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  410. package/dist/test/unit/direct-guards.test.js +0 -328
  411. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  412. package/dist/test/unit/discovery-token-store.test.js +0 -139
  413. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  414. package/dist/test/unit/evidence-pack.test.js +0 -224
  415. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  416. package/dist/test/unit/get-task-status.test.js +0 -174
  417. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  418. package/dist/test/unit/get-task-summary.test.js +0 -146
  419. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  420. package/dist/test/unit/goal-graph.test.js +0 -298
  421. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  422. package/dist/test/unit/goal-progress.test.js +0 -381
  423. package/dist/test/unit/goal-report.test.d.ts +0 -1
  424. package/dist/test/unit/goal-report.test.js +0 -159
  425. package/dist/test/unit/goal-status.test.d.ts +0 -1
  426. package/dist/test/unit/goal-status.test.js +0 -215
  427. package/dist/test/unit/goal-store.test.d.ts +0 -1
  428. package/dist/test/unit/goal-store.test.js +0 -253
  429. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  430. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  431. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  432. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  433. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  434. package/dist/test/unit/handoff-export.test.js +0 -263
  435. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  436. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  437. package/dist/test/unit/logging.test.d.ts +0 -1
  438. package/dist/test/unit/logging.test.js +0 -397
  439. package/dist/test/unit/path-guard.test.d.ts +0 -1
  440. package/dist/test/unit/path-guard.test.js +0 -133
  441. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  442. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  443. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  444. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  445. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  446. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  447. package/dist/test/unit/release-gate.test.d.ts +0 -1
  448. package/dist/test/unit/release-gate.test.js +0 -242
  449. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  450. package/dist/test/unit/run-task-loop.test.js +0 -380
  451. package/dist/test/unit/safe-status.test.d.ts +0 -1
  452. package/dist/test/unit/safe-status.test.js +0 -165
  453. package/dist/test/unit/safe-views.test.d.ts +0 -1
  454. package/dist/test/unit/safe-views.test.js +0 -171
  455. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  456. package/dist/test/unit/schema-drift-check.test.js +0 -176
  457. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  458. package/dist/test/unit/sensitive-guard.test.js +0 -104
  459. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  460. package/dist/test/unit/spec-kit-import.test.js +0 -341
  461. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  462. package/dist/test/unit/subgoal-sync.test.js +0 -183
  463. package/dist/test/unit/sync-file.test.d.ts +0 -1
  464. package/dist/test/unit/sync-file.test.js +0 -154
  465. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  466. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  467. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  468. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  469. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  470. package/dist/test/unit/toolSearch.test.js +0 -571
  471. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  472. package/dist/test/unit/wait-for-task.test.js +0 -144
  473. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  474. package/dist/test/unit/watcher-status.test.js +0 -169
  475. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  476. package/dist/test/unit/worktree-manager.test.js +0 -176
  477. package/dist/tools/applyPatch.js +0 -41
  478. package/dist/tools/cancelTask.d.ts +0 -30
  479. package/dist/tools/cancelTask.js +0 -64
  480. package/dist/tools/finalizeDirectSession.js +0 -84
  481. package/dist/tools/goalSubgoalTask.js +0 -110
  482. package/dist/tools/killTask.d.ts +0 -15
  483. package/dist/tools/retryTask.js +0 -45
  484. package/dist/tools/runVerification.js +0 -36
  485. package/dist/tools/syncFile.js +0 -65
  486. package/docs/control-center/control-center-daily-driver.md +0 -211
  487. package/docs/control-center/control-center-mvp.md +0 -205
  488. package/docs/control-center/control-center-phase2.md +0 -159
  489. package/docs/execution-plan-2026-07-09.md +0 -315
  490. package/docs/release-v0.6.0.md +0 -71
  491. package/docs/release-v0.6.1.md +0 -75
  492. package/docs/release-v0.6.4.md +0 -45
  493. package/docs/roadmap-execution-and-acceptance.md +0 -365
  494. package/src/test/unit/acceptance-engine.test.ts +0 -261
  495. package/src/test/unit/agent-router.test.ts +0 -342
  496. package/src/test/unit/android-doctor.test.ts +0 -158
  497. package/src/test/unit/apply-patch.test.ts +0 -293
  498. package/src/test/unit/audit-checks.test.ts +0 -567
  499. package/src/test/unit/chinese-path.test.ts +0 -106
  500. package/src/test/unit/command-guard.test.ts +0 -221
  501. package/src/test/unit/create-task.test.ts +0 -255
  502. package/src/test/unit/diagnose-task.test.ts +0 -544
  503. package/src/test/unit/direct-guards.test.ts +0 -467
  504. package/src/test/unit/discovery-token-store.test.ts +0 -181
  505. package/src/test/unit/evidence-pack.test.ts +0 -251
  506. package/src/test/unit/get-task-status.test.ts +0 -203
  507. package/src/test/unit/get-task-summary.test.ts +0 -173
  508. package/src/test/unit/goal-graph.test.ts +0 -347
  509. package/src/test/unit/goal-progress.test.ts +0 -538
  510. package/src/test/unit/goal-report.test.ts +0 -189
  511. package/src/test/unit/goal-status.test.ts +0 -270
  512. package/src/test/unit/goal-store.test.ts +0 -318
  513. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  514. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  515. package/src/test/unit/handoff-export.test.ts +0 -295
  516. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  517. package/src/test/unit/logging.test.ts +0 -497
  518. package/src/test/unit/path-guard.test.ts +0 -180
  519. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  520. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  521. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  522. package/src/test/unit/release-gate.test.ts +0 -314
  523. package/src/test/unit/run-task-loop.test.ts +0 -425
  524. package/src/test/unit/safe-status.test.ts +0 -187
  525. package/src/test/unit/safe-views.test.ts +0 -184
  526. package/src/test/unit/schema-drift-check.test.ts +0 -260
  527. package/src/test/unit/sensitive-guard.test.ts +0 -124
  528. package/src/test/unit/spec-kit-import.test.ts +0 -429
  529. package/src/test/unit/subgoal-sync.test.ts +0 -236
  530. package/src/test/unit/sync-file.test.ts +0 -231
  531. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  532. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  533. package/src/test/unit/toolSearch.test.ts +0 -756
  534. package/src/test/unit/wait-for-task.test.ts +0 -176
  535. package/src/test/unit/watcher-status.test.ts +0 -190
  536. package/src/test/unit/worktree-manager.test.ts +0 -247
  537. package/src/tools/cancelTask.ts +0 -70
  538. package/src/tools/retryTask.ts +0 -53
  539. package/src/tools/syncFile.ts +0 -122
  540. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  541. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  542. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  543. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  544. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  545. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  546. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  547. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  548. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  549. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  550. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  551. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  552. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  553. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  554. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  555. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  556. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  557. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  558. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  559. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  560. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  561. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  562. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  563. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  564. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  565. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  566. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  567. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  568. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  569. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  570. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  571. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  572. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  573. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -1,176 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { tmpdir } from "node:os";
6
- import { reloadConfig } from "../../config.js";
7
- import { waitForTask } from "../../tools/waitForTask.js";
8
-
9
- let tempDir: string;
10
- let tasksDir: string;
11
- let prevConfigEnv: string | undefined;
12
-
13
- function writeConfig(): void {
14
- const configPath = join(tempDir, "patchwarden.config.json");
15
- writeFileSync(
16
- configPath,
17
- JSON.stringify({
18
- workspaceRoot: tempDir,
19
- tasksDir: ".patchwarden/tasks",
20
- plansDir: ".patchwarden/plans",
21
- assessmentsDir: ".patchwarden/assessments",
22
- directSessionsDir: ".patchwarden/direct-sessions",
23
- agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
24
- allowedTestCommands: ["npm test"],
25
- }),
26
- "utf-8"
27
- );
28
- prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
29
- process.env.PATCHWARDEN_CONFIG = configPath;
30
- reloadConfig();
31
- }
32
-
33
- function writeTaskStatus(taskId: string, status: Record<string, unknown>): string {
34
- const taskDir = join(tasksDir, taskId);
35
- mkdirSync(taskDir, { recursive: true });
36
- writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
37
- return taskDir;
38
- }
39
-
40
- function baseStatus(taskId: string, overrides: Record<string, unknown> = {}): Record<string, unknown> {
41
- return {
42
- task_id: taskId,
43
- plan_id: "plan-001",
44
- agent: "codex",
45
- workspace_root: tempDir,
46
- repo_path: "repo",
47
- resolved_repo_path: join(tempDir, "repo"),
48
- created_at: "2026-07-12T10:00:00Z",
49
- started_at: "2026-07-12T10:00:05Z",
50
- finished_at: "2026-07-12T10:05:00Z",
51
- updated_at: "2026-07-12T10:05:00Z",
52
- timeout_seconds: 900,
53
- error: null,
54
- ...overrides,
55
- };
56
- }
57
-
58
- describe("waitForTask", () => {
59
- beforeEach(() => {
60
- tempDir = mkdtempSync(join(tmpdir(), "pw-waitfortask-"));
61
- tasksDir = join(tempDir, ".patchwarden", "tasks");
62
- mkdirSync(tasksDir, { recursive: true });
63
- writeConfig();
64
- });
65
-
66
- afterEach(() => {
67
- if (prevConfigEnv === undefined) delete process.env.PATCHWARDEN_CONFIG;
68
- else process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
69
- reloadConfig();
70
- rmSync(tempDir, { recursive: true, force: true });
71
- });
72
-
73
- // ── already terminal: done ──
74
- it("returns immediately when task is already done", async () => {
75
- writeTaskStatus("task-done-001", baseStatus("task-done-001", {
76
- status: "done",
77
- phase: "completed",
78
- verify_status: "passed",
79
- }));
80
-
81
- const result = await waitForTask("task-done-001", 5);
82
-
83
- assert.equal(result.task_id, "task-done-001");
84
- assert.equal(result.status, "done");
85
- assert.equal(result.terminal, true);
86
- assert.equal(result.timed_out, false);
87
- assert.equal(result.continuation_required, false);
88
- // terminal → includes compact summary, next tool is audit_task
89
- assert.ok(result.summary);
90
- assert.equal(result.next_tool_call.name, "audit_task");
91
- assert.equal(result.progress_summary, undefined);
92
- });
93
-
94
- // ── already terminal: failed ──
95
- it("returns immediately when task is already failed", async () => {
96
- writeTaskStatus("task-failed-001", baseStatus("task-failed-001", {
97
- status: "failed",
98
- phase: "failed",
99
- error: "Agent exited with code 1",
100
- }));
101
-
102
- const result = await waitForTask("task-failed-001", 5);
103
-
104
- assert.equal(result.terminal, true);
105
- assert.equal(result.status, "failed");
106
- assert.equal(result.timed_out, false);
107
- assert.equal(result.continuation_required, false);
108
- assert.ok(result.summary);
109
- assert.equal(result.next_tool_call.name, "audit_task");
110
- });
111
-
112
- // ── running + timeout → continuation_required ──
113
- it("returns continuation_required when running task times out", async () => {
114
- writeTaskStatus("task-running-001", baseStatus("task-running-001", {
115
- status: "running",
116
- phase: "executing_agent",
117
- finished_at: undefined,
118
- }));
119
-
120
- const result = await waitForTask("task-running-001", 1);
121
-
122
- assert.equal(result.task_id, "task-running-001");
123
- assert.equal(result.status, "running");
124
- assert.equal(result.terminal, false);
125
- assert.equal(result.timed_out, true);
126
- assert.equal(result.continuation_required, true);
127
- // non-terminal → includes progress_summary, next tool is wait_for_task
128
- assert.ok(result.progress_summary);
129
- assert.equal(result.next_tool_call.name, "wait_for_task");
130
- assert.equal(result.summary, undefined);
131
- // waited approximately 1 second
132
- assert.ok(result.waited_ms >= 900 && result.waited_ms <= 3000);
133
- });
134
-
135
- // ── canceled (terminal) ──
136
- it("returns terminal result for canceled task", async () => {
137
- writeTaskStatus("task-canceled-001", baseStatus("task-canceled-001", {
138
- status: "canceled",
139
- phase: "canceled",
140
- }));
141
-
142
- const result = await waitForTask("task-canceled-001", 5);
143
-
144
- assert.equal(result.terminal, true);
145
- assert.equal(result.status, "canceled");
146
- assert.equal(result.timed_out, false);
147
- assert.equal(result.continuation_required, false);
148
- assert.ok(result.summary);
149
- assert.equal(result.next_tool_call.name, "audit_task");
150
- });
151
-
152
- // ── task not found ──
153
- it("throws when task does not exist", async () => {
154
- await assert.rejects(
155
- () => waitForTask("non-existent-task", 1),
156
- /(File not found|Task not found)/
157
- );
158
- });
159
-
160
- // ── invalid wait_seconds ──
161
- it("throws when wait_seconds is out of range", async () => {
162
- writeTaskStatus("task-validate-001", baseStatus("task-validate-001", {
163
- status: "done",
164
- phase: "completed",
165
- }));
166
-
167
- await assert.rejects(
168
- () => waitForTask("task-validate-001", 0),
169
- /wait_seconds must be an integer from 1 to 30/
170
- );
171
- await assert.rejects(
172
- () => waitForTask("task-validate-001", 31),
173
- /wait_seconds must be an integer from 1 to 30/
174
- );
175
- });
176
- });
@@ -1,190 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync, existsSync, mkdir } from "node:fs";
4
- import { join, dirname } from "node:path";
5
- import { tmpdir } from "node:os";
6
- import type { PatchWardenConfig } from "../../config.js";
7
- import { readWatcherStatus, getWatcherHeartbeatPath } from "../../watcherStatus.js";
8
-
9
- function makeConfig(workspaceRoot: string): PatchWardenConfig {
10
- return {
11
- workspaceRoot,
12
- plansDir: ".patchwarden/plans",
13
- tasksDir: ".patchwarden/tasks",
14
- assessmentsDir: ".patchwarden/assessments",
15
- assessmentTtlSeconds: 3600,
16
- agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
17
- allowedTestCommands: ["npm test"],
18
- repoAllowedTestCommands: {},
19
- maxReadFileBytes: 200_000,
20
- defaultTaskTimeoutSeconds: 900,
21
- maxTaskTimeoutSeconds: 3600,
22
- watcherStaleSeconds: 30,
23
- directSessionsDir: ".patchwarden/direct-sessions",
24
- directSessionTtlSeconds: 3600,
25
- directMaxPatchBytes: 200_000,
26
- directMaxFileBytes: 500_000,
27
- };
28
- }
29
-
30
- describe("readWatcherStatus — watcher stale fallback", () => {
31
- let tempDir: string;
32
- let config: PatchWardenConfig;
33
-
34
- beforeEach(() => {
35
- tempDir = mkdtempSync(join(tmpdir(), "pw-watcher-test-"));
36
- config = makeConfig(tempDir);
37
- mkdirSync(join(tempDir, ".patchwarden", "tasks"), { recursive: true });
38
- });
39
-
40
- afterEach(() => {
41
- rmSync(tempDir, { recursive: true, force: true });
42
- });
43
-
44
- it("returns missing when no heartbeat and no running task", () => {
45
- const result = readWatcherStatus(config, Date.now());
46
- assert.equal(result.status, "missing");
47
- assert.equal(result.available, false);
48
- assert.equal(result.activity, null);
49
- });
50
-
51
- it("returns healthy when watcher heartbeat is fresh", () => {
52
- const heartbeatPath = getWatcherHeartbeatPath(config);
53
- mkdirSync(dirname(heartbeatPath), { recursive: true });
54
- writeFileSync(heartbeatPath, JSON.stringify({
55
- status: "running",
56
- pid: process.pid,
57
- last_heartbeat_at: new Date().toISOString(),
58
- }), "utf-8");
59
-
60
- const result = readWatcherStatus(config, Date.now());
61
- assert.equal(result.status, "healthy");
62
- assert.equal(result.available, true);
63
- assert.equal(result.activity, null);
64
- });
65
-
66
- it("returns stale when watcher heartbeat is old and no running task", () => {
67
- const heartbeatPath = getWatcherHeartbeatPath(config);
68
- mkdirSync(dirname(heartbeatPath), { recursive: true });
69
- const oldTime = new Date(Date.now() - 120_000).toISOString();
70
- writeFileSync(heartbeatPath, JSON.stringify({
71
- status: "running",
72
- pid: process.pid,
73
- last_heartbeat_at: oldTime,
74
- }), "utf-8");
75
-
76
- const result = readWatcherStatus(config, Date.now());
77
- assert.equal(result.status, "stale");
78
- assert.equal(result.available, false);
79
- assert.equal(result.activity, null);
80
- });
81
-
82
- it("returns healthy when watcher heartbeat is stale but a running task has fresh heartbeat", () => {
83
- // Write stale watcher heartbeat
84
- const heartbeatPath = getWatcherHeartbeatPath(config);
85
- mkdirSync(dirname(heartbeatPath), { recursive: true });
86
- const oldTime = new Date(Date.now() - 120_000).toISOString();
87
- writeFileSync(heartbeatPath, JSON.stringify({
88
- status: "running",
89
- pid: process.pid,
90
- last_heartbeat_at: oldTime,
91
- }), "utf-8");
92
-
93
- // Create a running task with fresh heartbeat
94
- const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-001");
95
- mkdirSync(taskDir, { recursive: true });
96
- writeFileSync(join(taskDir, "status.json"), JSON.stringify({
97
- status: "running",
98
- task_id: "test-task-001",
99
- }), "utf-8");
100
- writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
101
- phase: "executing_agent",
102
- last_heartbeat_at: new Date().toISOString(),
103
- current_command: "codex exec",
104
- }), "utf-8");
105
-
106
- const result = readWatcherStatus(config, Date.now());
107
- assert.equal(result.status, "healthy");
108
- assert.equal(result.available, true);
109
- assert.ok(result.activity);
110
- assert.match(result.activity!, /test-task-001/);
111
- });
112
-
113
- it("returns healthy when watcher heartbeat is missing but a running task has fresh heartbeat", () => {
114
- // No watcher heartbeat file at all
115
- // Create a running task with fresh heartbeat
116
- const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-002");
117
- mkdirSync(taskDir, { recursive: true });
118
- writeFileSync(join(taskDir, "status.json"), JSON.stringify({
119
- status: "running",
120
- task_id: "test-task-002",
121
- }), "utf-8");
122
- writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
123
- phase: "running_tests",
124
- last_heartbeat_at: new Date().toISOString(),
125
- current_command: "npm test",
126
- }), "utf-8");
127
-
128
- const result = readWatcherStatus(config, Date.now());
129
- assert.equal(result.status, "healthy");
130
- assert.equal(result.available, true);
131
- assert.ok(result.activity);
132
- assert.match(result.activity!, /test-task-002/);
133
- });
134
-
135
- it("returns stale when both watcher heartbeat and task heartbeat are old", () => {
136
- const heartbeatPath = getWatcherHeartbeatPath(config);
137
- mkdirSync(dirname(heartbeatPath), { recursive: true });
138
- const oldTime = new Date(Date.now() - 120_000).toISOString();
139
- writeFileSync(heartbeatPath, JSON.stringify({
140
- status: "running",
141
- pid: process.pid,
142
- last_heartbeat_at: oldTime,
143
- }), "utf-8");
144
-
145
- const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-003");
146
- mkdirSync(taskDir, { recursive: true });
147
- writeFileSync(join(taskDir, "status.json"), JSON.stringify({
148
- status: "running",
149
- task_id: "test-task-003",
150
- }), "utf-8");
151
- writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
152
- phase: "executing_agent",
153
- last_heartbeat_at: oldTime,
154
- current_command: "codex exec",
155
- }), "utf-8");
156
-
157
- const result = readWatcherStatus(config, Date.now());
158
- assert.equal(result.status, "stale");
159
- assert.equal(result.available, false);
160
- assert.equal(result.activity, null);
161
- });
162
-
163
- it("does not fall back to non-running tasks", () => {
164
- const heartbeatPath = getWatcherHeartbeatPath(config);
165
- mkdirSync(dirname(heartbeatPath), { recursive: true });
166
- const oldTime = new Date(Date.now() - 120_000).toISOString();
167
- writeFileSync(heartbeatPath, JSON.stringify({
168
- status: "running",
169
- pid: process.pid,
170
- last_heartbeat_at: oldTime,
171
- }), "utf-8");
172
-
173
- // Done task with fresh heartbeat should not trigger fallback
174
- const taskDir = join(tempDir, ".patchwarden", "tasks", "test-task-004");
175
- mkdirSync(taskDir, { recursive: true });
176
- writeFileSync(join(taskDir, "status.json"), JSON.stringify({
177
- status: "done",
178
- task_id: "test-task-004",
179
- }), "utf-8");
180
- writeFileSync(join(taskDir, "runtime.json"), JSON.stringify({
181
- phase: "completed",
182
- last_heartbeat_at: new Date().toISOString(),
183
- current_command: null,
184
- }), "utf-8");
185
-
186
- const result = readWatcherStatus(config, Date.now());
187
- assert.equal(result.status, "stale");
188
- assert.equal(result.available, false);
189
- });
190
- });
@@ -1,247 +0,0 @@
1
- import { describe, it, before, after } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import {
4
- mkdtempSync,
5
- rmSync,
6
- writeFileSync,
7
- readFileSync,
8
- existsSync,
9
- readdirSync,
10
- unlinkSync,
11
- } from "node:fs";
12
- import { join } from "node:path";
13
- import { tmpdir } from "node:os";
14
- import { execSync } from "node:child_process";
15
- import {
16
- createWorktree,
17
- mergeWorktree,
18
- discardWorktree,
19
- readWorktreeStatus,
20
- getWorktreesDir,
21
- type WorktreeStatus,
22
- } from "../../goal/worktreeManager.js";
23
- import { PatchWardenError } from "../../errors.js";
24
-
25
- // ── Test fixtures ─────────────────────────────────────────────────
26
-
27
- /**
28
- * 在 dir 创建一个真实的 git 仓库:git init + 配置 user + 初始 commit。
29
- * createWorktree 要求 workspaceRoot 是一个至少有一个 commit 的 git 仓库。
30
- */
31
- function initGitRepo(dir: string): void {
32
- execSync("git init", { cwd: dir, stdio: "ignore" });
33
- execSync('git config user.email "test@patchwarden.local"', { cwd: dir, stdio: "ignore" });
34
- execSync('git config user.name "PatchWarden Test"', { cwd: dir, stdio: "ignore" });
35
- execSync('git config commit.gpgsign false', { cwd: dir, stdio: "ignore" });
36
- writeFileSync(join(dir, "README.md"), "init\n", "utf-8");
37
- execSync("git add README.md", { cwd: dir, stdio: "ignore" });
38
- execSync("git commit -m init", { cwd: dir, stdio: "ignore" });
39
- }
40
-
41
- function gitBranchList(cwd: string): string {
42
- return execSync("git branch --list", { cwd, stdio: ["pipe", "pipe", "ignore"] }).toString("utf-8");
43
- }
44
-
45
- function assertPatchWardenError(fn: () => void, reason: string): void {
46
- try {
47
- fn();
48
- assert.fail(`expected PatchWardenError("${reason}") to be thrown`);
49
- } catch (err) {
50
- assert.ok(err instanceof PatchWardenError, `expected PatchWardenError, got: ${err}`);
51
- assert.equal((err as PatchWardenError).reason, reason);
52
- }
53
- }
54
-
55
- // ── Tests ─────────────────────────────────────────────────────────
56
-
57
- describe("worktreeManager", () => {
58
- let workspaceRoot: string;
59
-
60
- before(() => {
61
- workspaceRoot = mkdtempSync(join(tmpdir(), "pw-wt-"));
62
- initGitRepo(workspaceRoot);
63
- });
64
-
65
- after(() => {
66
- rmSync(workspaceRoot, { recursive: true, force: true });
67
- });
68
-
69
- // ── 1. createWorktree 成功 ──
70
- it("createWorktree creates an active worktree with valid status.json", () => {
71
- const result = createWorktree("goal_20260101_demo", "subgoal-001", workspaceRoot);
72
-
73
- assert.ok(result.worktreeId.startsWith("wt_"), `worktreeId should start with "wt_", got: ${result.worktreeId}`);
74
- assert.ok(result.branch.startsWith("pw-goal_20260101_demo-subgoal-001"), `branch should be pw-<goal>-<subgoal>, got: ${result.branch}`);
75
- assert.ok(existsSync(result.worktreePath), "worktreePath should exist");
76
-
77
- const statusFile = join(result.worktreePath, "worktree_status.json");
78
- assert.ok(existsSync(statusFile), "worktree_status.json should exist");
79
-
80
- // 原子写:不应残留 .tmp 文件
81
- assert.ok(!existsSync(statusFile + ".tmp"), "status.json.tmp should not remain after atomic write");
82
-
83
- // JSON 合法且字段正确
84
- const status = JSON.parse(readFileSync(statusFile, "utf-8")) as WorktreeStatus;
85
- assert.equal(status.worktree_id, result.worktreeId);
86
- assert.equal(status.goal_id, "goal_20260101_demo");
87
- assert.equal(status.subgoal_id, "subgoal-001");
88
- assert.equal(status.status, "active");
89
- assert.equal(status.branch, result.branch);
90
- assert.equal(status.path, result.worktreePath);
91
- assert.ok(typeof status.created_at === "string" && status.created_at.length > 0);
92
-
93
- // worktree 内应检出主仓库的文件(README.md)
94
- assert.ok(existsSync(join(result.worktreePath, "README.md")), "worktree should check out README.md");
95
-
96
- // branch 应在 git 中存在
97
- assert.ok(gitBranchList(workspaceRoot).includes(result.branch), "branch should exist in git");
98
-
99
- // readWorktreeStatus 应返回一致的状态
100
- const readBack = readWorktreeStatus(result.worktreeId, workspaceRoot);
101
- assert.ok(readBack !== null);
102
- assert.equal(readBack.status, "active");
103
- });
104
-
105
- // ── 2. createWorktree git 命令失败清理 ──
106
- it("createWorktree cleans up and throws when git fails (non-git workspaceRoot)", () => {
107
- const nonGitDir = mkdtempSync(join(tmpdir(), "pw-wt-nogit-"));
108
- try {
109
- // 非 git 目录 → git worktree add 失败
110
- assertPatchWardenError(
111
- () => createWorktree("goal_x", "subgoal-001", nonGitDir),
112
- "worktree_create_failed"
113
- );
114
-
115
- // 不应残留任何 worktree 目录
116
- const worktreesDir = getWorktreesDir(nonGitDir);
117
- if (existsSync(worktreesDir)) {
118
- // 若 _workspacetrees 被创建,应为空(无半成品 worktree)
119
- const entries = readdirSync(worktreesDir);
120
- assert.equal(entries.length, 0, "no leftover worktree directories should remain after failure");
121
- }
122
- } finally {
123
- rmSync(nonGitDir, { recursive: true, force: true });
124
- }
125
- });
126
-
127
- // ── 3. mergeWorktree 成功 ──
128
- it("mergeWorktree merges the branch back and marks status merged", () => {
129
- const created = createWorktree("goal_merge", "subgoal-001", workspaceRoot);
130
-
131
- // 在 worktree 中产生一个新 commit,使 merge 有实际内容
132
- writeFileSync(join(created.worktreePath, "feature.txt"), "merged feature\n", "utf-8");
133
- execSync("git add feature.txt", { cwd: created.worktreePath, stdio: "ignore" });
134
- execSync("git commit -m feature", { cwd: created.worktreePath, stdio: "ignore" });
135
-
136
- const result = mergeWorktree(created.worktreeId, workspaceRoot);
137
- assert.equal(result.status, "merged");
138
-
139
- // 主工作区应出现 feature.txt(fast-forward 合并)
140
- assert.ok(existsSync(join(workspaceRoot, "feature.txt")), "feature.txt should appear in main workspace after merge");
141
-
142
- // status 应更新为 merged,merged_at 非空
143
- const status = readWorktreeStatus(created.worktreeId, workspaceRoot);
144
- assert.ok(status !== null);
145
- assert.equal(status.status, "merged");
146
- assert.ok(typeof status.merged_at === "string" && status.merged_at.length > 0, "merged_at should be set");
147
- });
148
-
149
- // ── 4. discardWorktree 成功 ──
150
- it("discardWorktree removes worktree, deletes branch, and archives status", () => {
151
- const created = createWorktree("goal_discard", "subgoal-001", workspaceRoot);
152
- const branch = created.branch;
153
- const worktreePath = created.worktreePath;
154
-
155
- const result = discardWorktree(created.worktreeId, workspaceRoot);
156
- assert.equal(result.status, "discarded");
157
-
158
- // worktree 目录应被移除
159
- assert.ok(!existsSync(worktreePath), "worktree directory should be removed after discard");
160
-
161
- // 临时 branch 应被删除
162
- assert.ok(!gitBranchList(workspaceRoot).includes(branch), "temporary branch should be deleted after discard");
163
-
164
- // 归档文件应存在且 status="discarded"
165
- const archiveFile = join(workspaceRoot, ".patchwarden", "worktree-archive", `${created.worktreeId}.json`);
166
- assert.ok(existsSync(archiveFile), "archived status file should exist");
167
- const archived = JSON.parse(readFileSync(archiveFile, "utf-8")) as WorktreeStatus;
168
- assert.equal(archived.status, "discarded");
169
- assert.ok(typeof archived.discarded_at === "string" && archived.discarded_at.length > 0, "discarded_at should be set");
170
- assert.equal(archived.worktree_id, created.worktreeId);
171
- });
172
-
173
- // ── 5. 路径逃逸拦截 ──
174
- it("blocks path escape via worktreeId containing ..", () => {
175
- // worktreeId 在 read/merge/discard 中由调用方提供,构造逃逸路径
176
- assertPatchWardenError(
177
- () => readWorktreeStatus("../../../outside", workspaceRoot),
178
- "workspace_path_escape"
179
- );
180
- assertPatchWardenError(
181
- () => mergeWorktree("../../../outside", workspaceRoot),
182
- "workspace_path_escape"
183
- );
184
- assertPatchWardenError(
185
- () => discardWorktree("../../../outside", workspaceRoot),
186
- "workspace_path_escape"
187
- );
188
- });
189
-
190
- // ── 6. 敏感路径拦截 ──
191
- it("blocks sensitive worktreeId via guardSensitivePath", () => {
192
- // "credentials" 命中 sensitiveGuard 的敏感模式
193
- assertPatchWardenError(
194
- () => readWorktreeStatus("credentials", workspaceRoot),
195
- "sensitive_path_blocked"
196
- );
197
- assertPatchWardenError(
198
- () => mergeWorktree("credentials", workspaceRoot),
199
- "sensitive_path_blocked"
200
- );
201
- assertPatchWardenError(
202
- () => discardWorktree("credentials", workspaceRoot),
203
- "sensitive_path_blocked"
204
- );
205
- });
206
-
207
- // ── 7. readWorktreeStatus 返回 null 当 status 不存在 ──
208
- it("readWorktreeStatus returns null when status file does not exist", () => {
209
- // 创建一个 worktree 目录但不含 status 文件
210
- const created = createWorktree("goal_missing", "subgoal-001", workspaceRoot);
211
- // 手动删除 status 文件模拟缺失
212
- unlinkSync(join(created.worktreePath, "worktree_status.json"));
213
- assert.equal(readWorktreeStatus(created.worktreeId, workspaceRoot), null);
214
- // 清理:删除该 worktree(避免污染后续测试)
215
- try {
216
- execSync(`git worktree remove --force "${created.worktreePath}"`, { cwd: workspaceRoot, stdio: "ignore" });
217
- } catch { /* ignore */ }
218
- try {
219
- execSync(`git branch -D ${created.branch}`, { cwd: workspaceRoot, stdio: "ignore" });
220
- } catch { /* ignore */ }
221
- });
222
-
223
- // ── 8. 非法状态转换:merge/discard 已 merged 的 worktree 抛错 ──
224
- it("rejects merge/discard on non-active worktree", () => {
225
- // 复用 #3 已 merged 的 worktree(goal_merge/subgoal-001)
226
- // 先找到它的 worktreeId:扫描 _workspacetrees 目录
227
- const worktreesDir = getWorktreesDir(workspaceRoot);
228
- let mergedId: string | null = null;
229
- for (const entry of readdirSync(worktreesDir)) {
230
- const s = readWorktreeStatus(entry, workspaceRoot);
231
- if (s && s.status === "merged" && s.goal_id === "goal_merge") {
232
- mergedId = entry;
233
- break;
234
- }
235
- }
236
- assert.ok(mergedId !== null, "should find the merged worktree from the merge test");
237
-
238
- assertPatchWardenError(
239
- () => mergeWorktree(mergedId as string, workspaceRoot),
240
- "invalid_worktree_state"
241
- );
242
- assertPatchWardenError(
243
- () => discardWorktree(mergedId as string, workspaceRoot),
244
- "invalid_worktree_state"
245
- );
246
- });
247
- });
@@ -1,70 +0,0 @@
1
- import { readFileSync, writeFileSync, 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 { writeTaskProgress } from "../taskProgress.js";
6
- import type { TaskStatus } from "./createTask.js";
7
-
8
- export function cancelTask(taskId: string) {
9
- return requestTaskTermination(taskId, false);
10
- }
11
-
12
- export function requestTaskTermination(taskId: string, force: boolean) {
13
- const config = getConfig();
14
- const taskDir = join(getTasksDir(config), taskId);
15
- const statusFile = join(taskDir, "status.json");
16
- guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
17
-
18
- if (!existsSync(statusFile)) throw new Error(`Task not found: "${taskId}"`);
19
-
20
- const data = JSON.parse(readFileSync(statusFile, "utf-8"));
21
- const currentStatus: TaskStatus = data.status;
22
- if (["done", "done_by_agent", "failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled"].includes(currentStatus)) {
23
- return {
24
- task_id: taskId,
25
- previous_status: currentStatus,
26
- new_status: currentStatus,
27
- message: `Task is already ${currentStatus}. No action taken.`,
28
- };
29
- }
30
-
31
- const now = new Date().toISOString();
32
- if (currentStatus === "pending") {
33
- data.status = "canceled";
34
- data.phase = "canceled";
35
- data.canceled_at = now;
36
- data.cancel_reason = force ? "Killed before execution by user request." : "Canceled by user request.";
37
- data.updated_at = now;
38
- writeFileSync(statusFile, JSON.stringify(data, null, 2), "utf-8");
39
- writeTaskProgress(taskDir, "canceled", { note: data.cancel_reason, heartbeatAt: now });
40
- return {
41
- task_id: taskId,
42
- previous_status: "pending",
43
- new_status: "canceled",
44
- message: "Pending task canceled. It will not be executed by watcher.",
45
- };
46
- }
47
-
48
- data.cancel_requested = true;
49
- data.cancel_requested_at = now;
50
- data.force_kill_requested = force;
51
- if (force) data.kill_requested_at = now;
52
- data.phase = force ? "terminating" : "canceling";
53
- data.updated_at = now;
54
- writeFileSync(statusFile, JSON.stringify(data, null, 2), "utf-8");
55
- writeTaskProgress(taskDir, data.phase, {
56
- note: force ? "Immediate termination requested." : "Graceful cancellation requested.",
57
- heartbeatAt: now,
58
- });
59
-
60
- return {
61
- task_id: taskId,
62
- previous_status: "running",
63
- new_status: "running",
64
- cancel_requested: true,
65
- force_kill_requested: force,
66
- message: force
67
- ? "Kill requested. The runner that owns the child process will terminate it."
68
- : "Cancel requested. The runner will stop the child process safely.",
69
- };
70
- }