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
@@ -17,15 +17,30 @@
17
17
  * or: npm run watch
18
18
  */
19
19
 
20
- import { readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
20
+ import {
21
+ closeSync,
22
+ existsSync,
23
+ mkdirSync,
24
+ openSync,
25
+ readFileSync,
26
+ readdirSync,
27
+ unlinkSync,
28
+ writeFileSync,
29
+ } from "node:fs";
21
30
  import { resolve, join, dirname } from "node:path";
31
+ import { pathToFileURL } from "node:url";
22
32
  import { loadConfig, getConfig, getTasksDir, resolveWorkspaceRoot } from "../config.js";
23
33
  import { guardWorkspacePath } from "../security/pathGuard.js";
24
34
  import { guardAgentCommand, guardTestCommand } from "../security/commandGuard.js";
25
35
  import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
26
36
  import { captureRepoSnapshot } from "./changeCapture.js";
27
- import { runTask } from "./runTask.js";
37
+ import { mutateTaskStatus } from "./taskStatusStore.js";
28
38
  import { logger } from "../logging.js";
39
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
40
+ import { atomicWriteFileSync } from "../utils/atomicFile.js";
41
+ // Note: `runTask` is imported lazily inside `tick()` via `await import("./runTask.js")`.
42
+ // This keeps `import { acquireWatcherLock }` side-effect-free for unit tests and
43
+ // avoids eagerly pulling in the full tools/dispatch graph at module load.
29
44
 
30
45
  // ── Bootstrap ─────────────────────────────────────────────────────
31
46
 
@@ -36,37 +51,175 @@ const wsRoot = resolveWorkspaceRoot(config);
36
51
 
37
52
  const POLL_INTERVAL_MS = 4000;
38
53
  const WATCHER_HEARTBEAT_FILE = join(dirname(tasksDir), "watcher-heartbeat.json");
54
+ const WATCHER_LOCK_FILE = join(dirname(tasksDir), "watcher.lock");
39
55
  const WATCHER_STARTED_AT = new Date().toISOString();
40
56
  const WATCHER_INSTANCE_ID = process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || `standalone-${process.pid}-${Date.now()}`;
41
57
  const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID))
42
58
  ? Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID)
43
59
  : null;
44
- mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
45
-
46
- logger.info(`[watcher] Workspace: ${wsRoot}`);
47
- logger.info(`[watcher] Tasks: ${tasksDir}`);
48
- logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
49
- logger.info(`[watcher] Press Ctrl+C to stop`);
50
-
51
60
  // Track executed tasks to prevent re-execution
52
61
  const executedTasks = new Set<string>();
62
+ let consecutiveTickFailures = 0;
63
+
64
+ // ── Single-instance lock ─────────────────────────────────────────
65
+ // watch.ts is a long-running poller; without a lock, two watchers started
66
+ // against the same workspace would race on the same pending tasks. The lock
67
+ // is best-effort: it detects a live PID via process.kill(pid, 0) and refuses
68
+ // to start when one is found. A dead PID (crashed watcher) is taken over.
69
+
70
+ export class WatcherAlreadyRunningError extends Error {
71
+ constructor(message: string) {
72
+ super(message);
73
+ this.name = "WatcherAlreadyRunningError";
74
+ }
75
+ }
76
+
77
+ type WatcherLockFile = {
78
+ pid?: number;
79
+ instance_id?: string;
80
+ started_at?: string;
81
+ launcher_pid?: number | null;
82
+ };
83
+
84
+ function errorCode(error: unknown): string | null {
85
+ return error && typeof error === "object" && "code" in error
86
+ ? String((error as NodeJS.ErrnoException).code || "")
87
+ : null;
88
+ }
89
+
90
+ function readWatcherLock(lockFilePath: string): WatcherLockFile | null {
91
+ try {
92
+ const parsed: unknown = JSON.parse(readFileSync(lockFilePath, "utf-8"));
93
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
94
+ ? parsed as WatcherLockFile
95
+ : null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ function processIsAlive(pid: number): boolean {
102
+ try {
103
+ process.kill(pid, 0);
104
+ return true;
105
+ } catch (error) {
106
+ // EPERM means the process exists but the current user cannot signal it.
107
+ return errorCode(error) === "EPERM";
108
+ }
109
+ }
110
+
111
+ function runningWatcherError(lockData: WatcherLockFile | null): WatcherAlreadyRunningError {
112
+ return new WatcherAlreadyRunningError(
113
+ `[watcher] Already running (pid=${lockData?.pid ?? "unknown"}, instance=${lockData?.instance_id ?? "unknown"}). Exiting.`,
114
+ );
115
+ }
116
+
117
+ function createWatcherLockExclusive(lockFilePath: string, content: string): void {
118
+ const descriptor = openSync(lockFilePath, "wx", 0o600);
119
+ let writeError: unknown = null;
120
+ try {
121
+ writeFileSync(descriptor, content, "utf-8");
122
+ } catch (error) {
123
+ writeError = error;
124
+ } finally {
125
+ closeSync(descriptor);
126
+ }
127
+ if (writeError) {
128
+ try { unlinkSync(lockFilePath); } catch { /* best effort after a partial write */ }
129
+ throw writeError;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Acquire the watcher lock at `lockFilePath`. If a lock file exists and its
135
+ * recorded PID is still alive (per `process.kill(pid, 0)`), throws
136
+ * {@link WatcherAlreadyRunningError}. Otherwise overwrites the lock atomically.
137
+ *
138
+ * Exported for unit testing; `lockFilePath` defaults to the module-level
139
+ * {@link WATCHER_LOCK_FILE} but tests pass a temp path.
140
+ */
141
+ export function acquireWatcherLock(lockFilePath: string = WATCHER_LOCK_FILE): void {
142
+ const lockContent = JSON.stringify({
143
+ pid: process.pid,
144
+ instance_id: WATCHER_INSTANCE_ID,
145
+ started_at: WATCHER_STARTED_AT,
146
+ launcher_pid: WATCHER_LAUNCHER_PID,
147
+ }, null, 2);
148
+ try {
149
+ createWatcherLockExclusive(lockFilePath, lockContent);
150
+ return;
151
+ } catch (error) {
152
+ if (errorCode(error) !== "EEXIST") throw error;
153
+ }
154
+
155
+ const existing = readWatcherLock(lockFilePath);
156
+ if (typeof existing?.pid === "number" && processIsAlive(existing.pid)) {
157
+ throw runningWatcherError(existing);
158
+ }
159
+
160
+ try {
161
+ unlinkSync(lockFilePath);
162
+ } catch (error) {
163
+ if (errorCode(error) !== "ENOENT") throw error;
164
+ }
165
+
166
+ try {
167
+ createWatcherLockExclusive(lockFilePath, lockContent);
168
+ } catch (error) {
169
+ if (errorCode(error) === "EEXIST") {
170
+ throw runningWatcherError(readWatcherLock(lockFilePath));
171
+ }
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ export function releaseWatcherLock(
177
+ lockFilePath: string = WATCHER_LOCK_FILE,
178
+ instanceId: string = WATCHER_INSTANCE_ID,
179
+ pid: number = process.pid,
180
+ ): boolean {
181
+ const existing = readWatcherLock(lockFilePath);
182
+ if (existing?.pid !== pid || existing.instance_id !== instanceId) return false;
183
+ try {
184
+ unlinkSync(lockFilePath);
185
+ return true;
186
+ } catch (error) {
187
+ if (errorCode(error) === "ENOENT") return false;
188
+ throw error;
189
+ }
190
+ }
191
+
192
+ export function createNonOverlappingRunner(
193
+ operation: () => Promise<void>,
194
+ onSkipped: () => void = () => undefined,
195
+ ): () => Promise<boolean> {
196
+ let running = false;
197
+ return async () => {
198
+ if (running) {
199
+ onSkipped();
200
+ return false;
201
+ }
202
+ running = true;
203
+ try {
204
+ await operation();
205
+ return true;
206
+ } finally {
207
+ running = false;
208
+ }
209
+ };
210
+ }
53
211
 
54
212
  // ── Main loop ─────────────────────────────────────────────────────
55
213
 
56
214
  async function tick() {
57
215
  try {
58
- const heartbeatTemporary = `${WATCHER_HEARTBEAT_FILE}.${WATCHER_INSTANCE_ID}.tmp`;
59
- writeFileSync(heartbeatTemporary, JSON.stringify({
60
- status: "running",
61
- pid: process.pid,
62
- started_at: WATCHER_STARTED_AT,
63
- last_heartbeat_at: new Date().toISOString(),
64
- instance_id: WATCHER_INSTANCE_ID,
65
- launcher_pid: WATCHER_LAUNCHER_PID,
66
- }, null, 2), "utf-8");
67
- renameSync(heartbeatTemporary, WATCHER_HEARTBEAT_FILE);
216
+ writeWatcherHeartbeat("running");
68
217
  // Ensure tasks directory exists
69
- if (!existsSync(tasksDir)) return;
218
+ if (!existsSync(tasksDir)) {
219
+ consecutiveTickFailures = 0;
220
+ writeWatcherHeartbeat("running");
221
+ return;
222
+ }
70
223
 
71
224
  const entries = readdirSync(tasksDir, { withFileTypes: true });
72
225
  const taskDirs = entries.filter((e) => e.isDirectory());
@@ -82,11 +235,19 @@ async function tick() {
82
235
 
83
236
  if (!existsSync(statusFile)) continue;
84
237
 
85
- let statusData: any;
238
+ let statusData: Record<string, unknown>;
86
239
  try {
87
- statusData = JSON.parse(readFileSync(statusFile, "utf-8"));
88
- } catch {
89
- continue; // corrupted status, skip
240
+ const parsed: unknown = JSON.parse(readFileSync(statusFile, "utf-8"));
241
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
242
+ throw new Error("status.json must contain an object");
243
+ }
244
+ statusData = parsed as Record<string, unknown>;
245
+ } catch (error) {
246
+ logger.warn("[watcher] Skipping unreadable task status", {
247
+ task_id: taskId,
248
+ error: error instanceof Error ? error.message : String(error),
249
+ });
250
+ continue;
90
251
  }
91
252
 
92
253
  if (statusData.status !== "pending") continue;
@@ -94,13 +255,18 @@ async function tick() {
94
255
  // ── Pre-flight safety checks ──
95
256
  try {
96
257
  // Check repo_path
97
- const resolvedRepoPath = guardWorkspacePath(statusData.resolved_repo_path || statusData.repo_path || wsRoot, wsRoot);
258
+ const repoPath = typeof statusData.resolved_repo_path === "string"
259
+ ? statusData.resolved_repo_path
260
+ : typeof statusData.repo_path === "string"
261
+ ? statusData.repo_path
262
+ : wsRoot;
263
+ const resolvedRepoPath = guardWorkspacePath(repoPath, wsRoot);
98
264
 
99
265
  // Check agent
100
- guardAgentCommand(statusData.agent, config);
266
+ guardAgentCommand(typeof statusData.agent === "string" ? statusData.agent : "", config);
101
267
 
102
268
  // Check test_command
103
- if (statusData.test_command) {
269
+ if (typeof statusData.test_command === "string" && statusData.test_command) {
104
270
  guardTestCommand(statusData.test_command, config, resolvedRepoPath);
105
271
  }
106
272
  if (Array.isArray(statusData.verify_commands)) {
@@ -110,25 +276,32 @@ async function tick() {
110
276
  }
111
277
 
112
278
  // Assessment freshness revalidation
113
- if (statusData.assessment_id) {
279
+ if (typeof statusData.assessment_id === "string" && statusData.assessment_id) {
114
280
  const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
115
- const validation = validateAssessmentFreshness(String(statusData.assessment_id), preExecSnapshot);
281
+ const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
116
282
  if (!validation.valid) {
117
283
  throw new Error(`assessment validation failed: ${validation.failure_reason}`);
118
284
  }
119
285
  }
120
286
  } catch (err) {
121
- const errMsg = `[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`;
287
+ const errMsg = redactSensitiveContent(
288
+ `[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`,
289
+ ).content;
122
290
  logger.error(errMsg);
123
291
 
124
292
  // Write error and mark as failed so it doesn't get re-picked
125
293
  try {
126
- writeFileSync(join(taskDir, "error.log"), errMsg, "utf-8");
127
- const data = JSON.parse(readFileSync(statusFile, "utf-8"));
128
- data.status = "failed";
129
- data.error = errMsg;
130
- data.updated_at = new Date().toISOString();
131
- writeFileSync(statusFile, JSON.stringify(data, null, 2), "utf-8");
294
+ atomicWriteFileSync(join(taskDir, "error.log"), errMsg);
295
+ mutateTaskStatus(statusFile, (current) => {
296
+ if (current.status !== "pending") return { result: undefined };
297
+ const next = {
298
+ ...current,
299
+ status: "failed",
300
+ error: errMsg,
301
+ updated_at: new Date().toISOString(),
302
+ };
303
+ return { next, result: undefined };
304
+ });
132
305
  } catch (err) {
133
306
  logger.warn("watcher error-log write failed", { error: err instanceof Error ? err.message : String(err) });
134
307
  }
@@ -141,31 +314,88 @@ async function tick() {
141
314
  executedTasks.add(taskId);
142
315
 
143
316
  try {
317
+ const { runTask } = await import("./runTask.js");
144
318
  const result = await runTask(taskId);
145
319
  logger.info(`[watcher] ${taskId} → ${result.status}`);
146
320
  } catch (err) {
147
321
  logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
148
322
  }
149
323
  }
324
+ consecutiveTickFailures = 0;
325
+ writeWatcherHeartbeat("running");
150
326
  } catch (err) {
151
- logger.error(`[watcher] Tick error: ${err instanceof Error ? err.message : String(err)}`);
327
+ consecutiveTickFailures += 1;
328
+ const message = err instanceof Error ? err.message : String(err);
329
+ logger.error(`[watcher] Tick error (${consecutiveTickFailures} consecutive): ${message}`);
330
+ try {
331
+ writeWatcherHeartbeat(consecutiveTickFailures >= 3 ? "degraded" : "running", message);
332
+ } catch (heartbeatError) {
333
+ logger.error("[watcher] Failed to persist degraded heartbeat", {
334
+ error: heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError),
335
+ });
336
+ }
152
337
  }
153
338
  }
154
339
 
155
- // ── Start ─────────────────────────────────────────────────────────
340
+ function writeWatcherHeartbeat(status: "running" | "degraded", lastError?: string): void {
341
+ atomicWriteFileSync(WATCHER_HEARTBEAT_FILE, JSON.stringify({
342
+ status,
343
+ pid: process.pid,
344
+ started_at: WATCHER_STARTED_AT,
345
+ last_heartbeat_at: new Date().toISOString(),
346
+ instance_id: WATCHER_INSTANCE_ID,
347
+ launcher_pid: WATCHER_LAUNCHER_PID,
348
+ consecutive_failures: consecutiveTickFailures,
349
+ last_error: lastError ? lastError.slice(0, 500) : null,
350
+ }, null, 2));
351
+ }
352
+
353
+ // ── Start (only when executed as a script) ─────────────────────────
354
+ // The bootstrap below has process-wide side effects (mkdir, signal handlers,
355
+ // polling interval, lock acquisition). Guarding it with a main-module check
356
+ // keeps `import { acquireWatcherLock }` safe for unit tests — without it,
357
+ // importing this module would start polling the real tasks dir and register
358
+ // SIGINT/SIGTERM handlers that interfere with the test runner.
359
+
360
+ const isMainModule = !!process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
361
+
362
+ if (isMainModule) {
363
+ mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
364
+
365
+ logger.info(`[watcher] Workspace: ${wsRoot}`);
366
+ logger.info(`[watcher] Tasks: ${tasksDir}`);
367
+ logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
368
+ logger.info(`[watcher] Press Ctrl+C to stop`);
156
369
 
157
- logger.info("[watcher] Started");
158
- setInterval(tick, POLL_INTERVAL_MS);
370
+ try {
371
+ acquireWatcherLock(WATCHER_LOCK_FILE);
372
+ } catch (err) {
373
+ if (err instanceof WatcherAlreadyRunningError) {
374
+ logger.error(err.message);
375
+ process.exit(1);
376
+ }
377
+ throw err;
378
+ }
159
379
 
160
- // Run first tick immediately
161
- tick();
380
+ logger.info("[watcher] Started");
381
+ const scheduledTick = createNonOverlappingRunner(tick, () => {
382
+ logger.warn("[watcher] Previous tick is still running; skipping overlapping poll");
383
+ });
384
+ setInterval(() => { void scheduledTick(); }, POLL_INTERVAL_MS);
162
385
 
163
- // Graceful shutdown
164
- process.on("SIGINT", () => {
165
- logger.info("[watcher] Stopped");
166
- process.exit(0);
167
- });
168
- process.on("SIGTERM", () => {
169
- logger.info("[watcher] Stopped");
170
- process.exit(0);
171
- });
386
+ // Run first tick immediately
387
+ void scheduledTick();
388
+
389
+ // Graceful shutdown
390
+ function shutdown(): void {
391
+ logger.info("[watcher] Stopped");
392
+ try { releaseWatcherLock(WATCHER_LOCK_FILE); } catch (error) {
393
+ logger.warn("[watcher] Failed to release watcher lock", {
394
+ error: error instanceof Error ? error.message : String(error),
395
+ });
396
+ }
397
+ process.exit(0);
398
+ }
399
+ process.once("SIGINT", shutdown);
400
+ process.once("SIGTERM", shutdown);
401
+ }
@@ -39,13 +39,13 @@ const RULES: RedactionRule[] = [
39
39
  {
40
40
  category: "credential_assignment",
41
41
  reason: "matched credential assignment",
42
- pattern: /\b((?:access[_ -]?token|api[_ -]?key|secret|password|credential|token)\s*[:=]\s*)(["']?)([^\s,"'\]}]{8,})(["']?)/gi,
42
+ pattern: /\b((?:(?:[A-Za-z][A-Za-z0-9]*[_ -])*(?:access[_ -]?token|refresh[_ -]?token|api[_ -]?key|secret(?:[_ -]?access)?[_ -]?key|client[_ -]?secret|private[_ -]?key|secret|password|credential|token))\s*[:=]\s*)(["']?)([^\s,"'\]}]{8,})(["']?)/gi,
43
43
  replace: "$1$2[REDACTED]$4",
44
44
  },
45
45
  {
46
46
  category: "known_token_format",
47
47
  reason: "matched known token format",
48
- pattern: /\b(?:sk|ghp|github_pat)_[A-Za-z0-9_-]{16,}\b/g,
48
+ pattern: /\b(?:sk-(?:proj-|ant-)?[A-Za-z0-9_-]{16,}|sk_[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_-]{16,}|github_pat_[A-Za-z0-9_-]{16,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{16,}|(?:hf|npm)_[A-Za-z0-9_-]{16,}|(?:sk|rk)_live_[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
49
49
  replace: "[REDACTED TOKEN]",
50
50
  },
51
51
  ];
@@ -12,8 +12,8 @@
12
12
 
13
13
  import { randomBytes } from "node:crypto";
14
14
  import { PatchWardenError } from "../errors.js";
15
- import type { ToolRisk } from "../tools/toolRegistry.js";
16
- import type { ToolProfile } from "../tools/toolCatalog.js";
15
+ import type { ToolRisk } from "../tools/catalog/toolRegistry.js";
16
+ import type { ToolProfile } from "../tools/catalog/toolCatalog.js";
17
17
 
18
18
  // ── 类型定义 ──────────────────────────────────────────────────────
19
19
 
@@ -42,6 +42,7 @@ export interface IssueTokenInput {
42
42
  // ── 常量 ──────────────────────────────────────────────────────────
43
43
 
44
44
  const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
45
+ export const MAX_ACTIVE_DISCOVERY_TOKENS = 1024;
45
46
 
46
47
  // ── 模块级 token store(单例,进程内共享) ───────────────────────
47
48
 
@@ -62,6 +63,17 @@ function generateTokenId(): string {
62
63
  return `dst_${datePart}_${randomPart}`;
63
64
  }
64
65
 
66
+ function pruneTokenStore(nowMs: number): void {
67
+ for (const [token, record] of tokenStore) {
68
+ if (Date.parse(record.expiresAt) <= nowMs) tokenStore.delete(token);
69
+ }
70
+ while (tokenStore.size >= MAX_ACTIVE_DISCOVERY_TOKENS) {
71
+ const oldest = tokenStore.keys().next().value as string | undefined;
72
+ if (!oldest) break;
73
+ tokenStore.delete(oldest);
74
+ }
75
+ }
76
+
65
77
  // ── 公共 API ──────────────────────────────────────────────────────
66
78
 
67
79
  /**
@@ -69,9 +81,11 @@ function generateTokenId(): string {
69
81
  * token id 格式:dst_{YYYYMMDD}_{randomHex12}
70
82
  */
71
83
  export function issueToken(input: IssueTokenInput): string {
72
- const now = new Date();
84
+ const nowMs = Date.now();
85
+ pruneTokenStore(nowMs);
86
+ const now = new Date(nowMs);
73
87
  const issuedAt = now.toISOString();
74
- const expiresAt = new Date(Date.now() + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
88
+ const expiresAt = new Date(nowMs + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
75
89
  const token = generateTokenId();
76
90
 
77
91
  const record: DiscoveryTokenRecord = {
@@ -0,0 +1,19 @@
1
+ import { firstHeaderValue } from "./secretComparison.js";
2
+
3
+ const LOOPBACK_HOST_RE = /^(127\.0\.0\.1|localhost)(?::([0-9]{1,5}))?$/i;
4
+
5
+ /** Validate an HTTP Host header for a service bound to IPv4 loopback. */
6
+ export function isTrustedLoopbackHostHeader(
7
+ value: string | string[] | undefined,
8
+ expectedPort: number,
9
+ ): boolean {
10
+ const raw = firstHeaderValue(value).trim();
11
+ const match = LOOPBACK_HOST_RE.exec(raw);
12
+ if (!match) return false;
13
+ if (match[2] === undefined) return true;
14
+ const declaredPort = Number(match[2]);
15
+ return Number.isInteger(declaredPort)
16
+ && declaredPort >= 1
17
+ && declaredPort <= 65_535
18
+ && declaredPort === expectedPort;
19
+ }
@@ -7,7 +7,7 @@ import {
7
7
  resolve,
8
8
  sep,
9
9
  } from "node:path";
10
- import { realpathSync } from "node:fs";
10
+ import { lstatSync, readlinkSync, realpathSync } from "node:fs";
11
11
  import { PatchWardenError } from "../errors.js";
12
12
 
13
13
  export function guardPath(
@@ -112,7 +112,7 @@ function realPathOrSelf(value: string): string {
112
112
  }
113
113
  }
114
114
 
115
- function realPathOrExistingPrefix(candidate: string): string {
115
+ function realPathOrExistingPrefix(candidate: string, seenLinks = new Set<string>()): string {
116
116
  const normalized = normalize(candidate);
117
117
  const root = parse(normalized).root;
118
118
  const suffix: string[] = [];
@@ -121,7 +121,36 @@ function realPathOrExistingPrefix(candidate: string): string {
121
121
  while (current && current !== root) {
122
122
  try {
123
123
  return resolve(realpathSync(current), ...suffix);
124
- } catch {
124
+ } catch (error) {
125
+ try {
126
+ const metadata = lstatSync(current);
127
+ if (metadata.isSymbolicLink()) {
128
+ const linkKey = process.platform === "win32" ? current.toLowerCase() : current;
129
+ if (seenLinks.has(linkKey)) {
130
+ throw new PatchWardenError(
131
+ "workspace_path_escape",
132
+ `Symbolic link cycle detected while resolving "${candidate}"`,
133
+ "Remove the cyclic link and use a path inside the configured workspace.",
134
+ true,
135
+ { path: candidate, operation: "path_access", safe_alternative: "Use a non-link path inside workspaceRoot." }
136
+ );
137
+ }
138
+ const nextSeen = new Set(seenLinks);
139
+ nextSeen.add(linkKey);
140
+ const linkTarget = readlinkSync(current);
141
+ const resolvedTarget = isAbsolute(linkTarget)
142
+ ? resolve(linkTarget)
143
+ : resolve(dirname(current), linkTarget);
144
+ return resolve(realPathOrExistingPrefix(resolvedTarget, nextSeen), ...suffix);
145
+ }
146
+ } catch (linkError) {
147
+ if (linkError instanceof PatchWardenError) throw linkError;
148
+ const linkCode = (linkError as NodeJS.ErrnoException).code;
149
+ if (linkCode !== "ENOENT" && linkCode !== "ENOTDIR") throw linkError;
150
+ }
151
+
152
+ const code = (error as NodeJS.ErrnoException).code;
153
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
125
154
  suffix.unshift(basename(current));
126
155
  const parent = dirname(current);
127
156
  if (parent === current) break;
@@ -129,11 +158,7 @@ function realPathOrExistingPrefix(candidate: string): string {
129
158
  }
130
159
  }
131
160
 
132
- try {
133
- return resolve(realpathSync(root || current), ...suffix);
134
- } catch {
135
- return normalized;
136
- }
161
+ return resolve(realpathSync(root || current), ...suffix);
137
162
  }
138
163
 
139
164
  function assertInside(
@@ -144,16 +169,10 @@ function assertInside(
144
169
  ): void {
145
170
  const normalizedRoot = normalize(root);
146
171
  const normalizedCandidate = normalize(candidate);
147
- const checkRoot = process.platform === "win32"
148
- ? normalizedRoot.toLowerCase()
149
- : normalizedRoot;
150
- const checkCandidate = process.platform === "win32"
151
- ? normalizedCandidate.toLowerCase()
152
- : normalizedCandidate;
153
172
 
154
173
  if (
155
- checkCandidate !== checkRoot &&
156
- !checkCandidate.startsWith(checkRoot + sep)
174
+ normalizedCandidate !== normalizedRoot &&
175
+ !normalizedCandidate.startsWith(normalizedRoot + sep)
157
176
  ) {
158
177
  if (message.startsWith("Path escapes workspace")) {
159
178
  throw new PatchWardenError(
@@ -1,4 +1,5 @@
1
1
  import { PatchWardenError } from "../errors.js";
2
+ import { redactSensitiveContent } from "./contentRedaction.js";
2
3
 
3
4
  interface PlanRule {
4
5
  id: string;
@@ -49,7 +50,7 @@ export function guardPlanContent(title: string, content: string): void {
49
50
  {
50
51
  operation: "save_plan",
51
52
  matched_category: rule.category,
52
- matched_text: match[0].slice(0, 160),
53
+ matched_text: redactSensitiveContent(match[0]).content.slice(0, 160),
53
54
  }
54
55
  );
55
56
  }
@@ -1,6 +1,7 @@
1
1
  import { resolve, join, dirname } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { PatchWardenError } from "../errors.js";
4
+ import { isPathChildOf, isSamePath } from "../utils/platform.js";
4
5
 
5
6
  const CRITICAL_RUNTIME_DIRS = ["dist", "src", "scripts", "release"];
6
7
 
@@ -10,19 +11,21 @@ const CRITICAL_RUNTIME_DIRS = ["dist", "src", "scripts", "release"];
10
11
  * and direct-session flows share the same protection.
11
12
  */
12
13
  export function guardRuntimeSelfModification(resolvedRepoPath: string): void {
13
- const runtimeRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
14
+ // runtimeGuard lives at <package-root>/{src,dist}/security/runtimeGuard.*.
15
+ const runtimeRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
16
+ const isRuntimeRoot = isSamePath(resolvedRepoPath, runtimeRoot);
14
17
  if (
15
- resolvedRepoPath === runtimeRoot ||
16
- resolvedRepoPath.startsWith(runtimeRoot + resolve("/")[0])
18
+ isRuntimeRoot ||
19
+ isPathChildOf(resolvedRepoPath, runtimeRoot)
17
20
  ) {
18
21
  const isCritical = CRITICAL_RUNTIME_DIRS.some((dir) => {
19
22
  const full = join(runtimeRoot, dir);
20
23
  return (
21
- resolvedRepoPath === full ||
22
- resolvedRepoPath.startsWith(full + resolve("/")[0])
24
+ isSamePath(resolvedRepoPath, full) ||
25
+ isPathChildOf(resolvedRepoPath, full)
23
26
  );
24
27
  });
25
- if (resolvedRepoPath === runtimeRoot || isCritical) {
28
+ if (isRuntimeRoot || isCritical) {
26
29
  throw new PatchWardenError(
27
30
  "runtime_self_modification_blocked",
28
31
  `repo_path points to the active PatchWarden runtime or its critical subdirectories.`,
@@ -0,0 +1,13 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+
3
+ /** Compare secret text without content-dependent byte comparison. */
4
+ export function timingSafeStringEqual(actual: string, expected: string): boolean {
5
+ const actualBytes = Buffer.from(actual, "utf-8");
6
+ const expectedBytes = Buffer.from(expected, "utf-8");
7
+ return actualBytes.length === expectedBytes.length
8
+ && timingSafeEqual(actualBytes, expectedBytes);
9
+ }
10
+
11
+ export function firstHeaderValue(value: string | string[] | undefined): string {
12
+ return Array.isArray(value) ? (value[0] || "") : (value || "");
13
+ }