patchwarden 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (597) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +194 -26
  3. package/README.md +160 -22
  4. package/dist/assessments/agentAssessor.d.ts +1 -1
  5. package/dist/assessments/agentAssessor.js +24 -15
  6. package/dist/assessments/assessmentStore.d.ts +1 -1
  7. package/dist/assessments/assessmentStore.js +44 -36
  8. package/dist/assessments/confirmCli.js +5 -4
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +98 -11
  11. package/dist/control/fileManager.d.ts +2 -0
  12. package/dist/control/fileManager.js +39 -0
  13. package/dist/control/middleware/auth.d.ts +16 -0
  14. package/dist/control/middleware/auth.js +17 -0
  15. package/dist/control/middleware/static.d.ts +3 -0
  16. package/dist/control/middleware/static.js +78 -0
  17. package/dist/control/routeTable.d.ts +24 -0
  18. package/dist/control/routeTable.js +263 -0
  19. package/dist/control/routes/audit.d.ts +15 -0
  20. package/dist/control/routes/audit.js +287 -0
  21. package/dist/control/routes/evidence.d.ts +4 -0
  22. package/dist/control/routes/evidence.js +96 -0
  23. package/dist/control/routes/lineage.d.ts +3 -0
  24. package/dist/control/routes/lineage.js +71 -0
  25. package/dist/control/routes/policy.d.ts +3 -0
  26. package/dist/control/routes/policy.js +81 -0
  27. package/dist/control/routes/process.d.ts +12 -0
  28. package/dist/control/routes/process.js +487 -0
  29. package/dist/control/routes/sessions.d.ts +30 -0
  30. package/dist/control/routes/sessions.js +287 -0
  31. package/dist/control/routes/status.d.ts +51 -0
  32. package/dist/control/routes/status.js +307 -0
  33. package/dist/control/routes/taskActions.d.ts +21 -0
  34. package/dist/control/routes/taskActions.js +197 -0
  35. package/dist/control/routes/tasks.d.ts +24 -0
  36. package/dist/control/routes/tasks.js +309 -0
  37. package/dist/control/routes/workspace.d.ts +15 -0
  38. package/dist/control/routes/workspace.js +220 -0
  39. package/dist/control/runtime.d.ts +92 -0
  40. package/dist/control/runtime.js +425 -0
  41. package/dist/control/server.d.ts +13 -0
  42. package/dist/control/server.js +150 -0
  43. package/dist/control/shared.d.ts +59 -0
  44. package/dist/control/shared.js +341 -0
  45. package/dist/controlCenter.d.ts +6 -0
  46. package/dist/controlCenter.js +7 -2197
  47. package/dist/direct/directAudit.js +234 -225
  48. package/dist/direct/directGuards.d.ts +2 -1
  49. package/dist/direct/directGuards.js +72 -37
  50. package/dist/direct/directPatch.d.ts +6 -1
  51. package/dist/direct/directPatch.js +55 -8
  52. package/dist/direct/directSessionStore.d.ts +16 -6
  53. package/dist/direct/directSessionStore.js +127 -35
  54. package/dist/direct/directVerification.js +8 -16
  55. package/dist/doctor.d.ts +18 -1
  56. package/dist/doctor.js +614 -373
  57. package/dist/goal/acceptanceEngine.d.ts +1 -1
  58. package/dist/goal/goalProgress.js +75 -69
  59. package/dist/goal/goalReport.d.ts +54 -0
  60. package/dist/goal/goalReport.js +197 -0
  61. package/dist/goal/goalStatus.d.ts +8 -0
  62. package/dist/goal/goalStatus.js +10 -3
  63. package/dist/goal/goalStore.d.ts +9 -2
  64. package/dist/goal/goalStore.js +152 -45
  65. package/dist/goal/handoffExport.js +6 -6
  66. package/dist/goal/specKitImport.d.ts +63 -0
  67. package/dist/goal/specKitImport.js +221 -0
  68. package/dist/goal/subgoalSync.js +13 -10
  69. package/dist/goal/worktreeManager.d.ts +2 -0
  70. package/dist/goal/worktreeManager.js +209 -136
  71. package/dist/httpServer.js +126 -44
  72. package/dist/index.js +7 -4
  73. package/dist/logging.d.ts +7 -1
  74. package/dist/logging.js +13 -3
  75. package/dist/policy/projectPolicy.js +5 -2
  76. package/dist/release/releaseGate.d.ts +1 -1
  77. package/dist/release/releaseGate.js +78 -25
  78. package/dist/runner/agentInvocation.d.ts +14 -0
  79. package/dist/runner/agentInvocation.js +96 -4
  80. package/dist/runner/changeCapture.d.ts +14 -6
  81. package/dist/runner/changeCapture.js +287 -81
  82. package/dist/runner/cli.js +7 -6
  83. package/dist/runner/postTaskCleanup.js +60 -7
  84. package/dist/runner/processSecurity.d.ts +56 -0
  85. package/dist/runner/processSecurity.js +373 -0
  86. package/dist/runner/runTask.d.ts +1 -1
  87. package/dist/runner/runTask.js +339 -287
  88. package/dist/runner/simpleProcess.d.ts +3 -0
  89. package/dist/runner/simpleProcess.js +52 -38
  90. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  91. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  92. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  93. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  94. package/dist/runner/taskStatusStore.d.ts +11 -0
  95. package/dist/runner/taskStatusStore.js +24 -0
  96. package/dist/runner/watch.d.ts +14 -1
  97. package/dist/runner/watch.js +265 -54
  98. package/dist/security/contentRedaction.d.ts +6 -0
  99. package/dist/security/contentRedaction.js +24 -2
  100. package/dist/security/discoveryTokenStore.d.ts +3 -2
  101. package/dist/security/discoveryTokenStore.js +17 -2
  102. package/dist/security/loopbackHost.d.ts +2 -0
  103. package/dist/security/loopbackHost.js +16 -0
  104. package/dist/security/pathGuard.js +32 -17
  105. package/dist/security/planGuard.js +2 -1
  106. package/dist/security/runtimeGuard.js +9 -6
  107. package/dist/security/secretComparison.d.ts +3 -0
  108. package/dist/security/secretComparison.js +11 -0
  109. package/dist/security/sensitiveGuard.d.ts +5 -0
  110. package/dist/security/sensitiveGuard.js +27 -8
  111. package/dist/security/toolInvocationGuard.d.ts +2 -2
  112. package/dist/security/toolInvocationGuard.js +1 -1
  113. package/dist/security/workspaceRootGuard.d.ts +8 -0
  114. package/dist/security/workspaceRootGuard.js +47 -0
  115. package/dist/smoke-test.js +280 -273
  116. package/dist/tools/catalog/index.d.ts +4 -0
  117. package/dist/tools/catalog/index.js +4 -0
  118. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  119. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  120. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
  121. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  122. package/dist/tools/definitions/toolDefs.js +1177 -0
  123. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  124. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  125. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
  126. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
  127. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  128. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
  129. package/dist/tools/diagnostics/index.d.ts +6 -0
  130. package/dist/tools/diagnostics/index.js +6 -0
  131. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  132. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  133. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
  134. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
  135. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  136. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  137. package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
  138. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
  139. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
  140. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  141. package/dist/tools/direct/index.d.ts +3 -0
  142. package/dist/tools/direct/index.js +3 -0
  143. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  144. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  145. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  146. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  147. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  148. package/dist/tools/discovery/index.d.ts +3 -0
  149. package/dist/tools/discovery/index.js +3 -0
  150. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  151. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  152. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  153. package/dist/tools/dispatch/coreDispatch.js +283 -0
  154. package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
  155. package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
  156. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  157. package/dist/tools/dispatch/directDispatch.js +116 -0
  158. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  159. package/dist/tools/dispatch/goalDispatch.js +92 -0
  160. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  161. package/dist/tools/dispatch/releaseDispatch.js +46 -0
  162. package/dist/tools/dispatch/types.d.ts +23 -0
  163. package/dist/tools/dispatch/types.js +15 -0
  164. package/dist/tools/dispatch/validation.d.ts +7 -0
  165. package/dist/tools/dispatch/validation.js +71 -0
  166. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  167. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
  168. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  169. package/dist/tools/goals/index.d.ts +3 -0
  170. package/dist/tools/goals/index.js +3 -0
  171. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  172. package/dist/tools/registry.d.ts +10 -14
  173. package/dist/tools/registry.js +47 -1651
  174. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  175. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  176. package/dist/tools/release/index.d.ts +2 -0
  177. package/dist/tools/release/index.js +2 -0
  178. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  179. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  180. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  181. package/dist/tools/tasks/cancelTask.js +85 -0
  182. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  183. package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
  184. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  185. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  186. package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
  187. package/dist/tools/tasks/evidencePack.js +387 -0
  188. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  189. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  190. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  191. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  192. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  193. package/dist/tools/tasks/index.d.ts +18 -0
  194. package/dist/tools/tasks/index.js +18 -0
  195. package/dist/tools/tasks/killTask.d.ts +1 -0
  196. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  197. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  198. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  199. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  200. package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
  201. package/dist/tools/tasks/retryTask.js +60 -0
  202. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  203. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
  204. package/dist/tools/tasks/runVerification.js +38 -0
  205. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  206. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  207. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  208. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  209. package/dist/tools/workspace/applyPatch.js +53 -0
  210. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  211. package/dist/tools/workspace/index.d.ts +10 -0
  212. package/dist/tools/workspace/index.js +10 -0
  213. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  214. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  215. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  216. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  217. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  218. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  219. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  220. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  221. package/dist/tools/workspace/syncFile.js +134 -0
  222. package/dist/utils/atomicFile.d.ts +7 -0
  223. package/dist/utils/atomicFile.js +48 -0
  224. package/dist/utils/boundedFile.d.ts +8 -0
  225. package/dist/utils/boundedFile.js +93 -0
  226. package/dist/utils/lockedJsonFile.d.ts +15 -0
  227. package/dist/utils/lockedJsonFile.js +224 -0
  228. package/dist/utils/platform.d.ts +11 -0
  229. package/dist/utils/platform.js +26 -0
  230. package/dist/utils/stableJson.d.ts +2 -0
  231. package/dist/utils/stableJson.js +12 -0
  232. package/dist/version.d.ts +3 -2
  233. package/dist/version.js +3 -2
  234. package/dist/watcherStatus.d.ts +2 -2
  235. package/dist/watcherStatus.js +18 -0
  236. package/docs/CODE_WIKI.md +1648 -0
  237. package/docs/agentseal-integration.md +150 -0
  238. package/docs/architecture.md +23 -0
  239. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  240. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  241. package/docs/control-center/README.md +4 -4
  242. package/docs/dashboard-overview.md +86 -0
  243. package/docs/demo.md +8 -0
  244. package/docs/desktop-app.md +175 -0
  245. package/docs/direct-session-workflow.md +98 -0
  246. package/docs/evidence-pack-schema.md +215 -0
  247. package/docs/lineage-evidence-pack-workflow.md +127 -0
  248. package/docs/mcp-inspector-testing.md +200 -0
  249. package/docs/open-source-application.md +168 -0
  250. package/docs/opencode-worker.md +151 -0
  251. package/docs/openhands-worker.md +181 -0
  252. package/docs/release-checklist.md +1 -1
  253. package/docs/release-evidence.md +82 -0
  254. package/docs/spec-kit-integration.md +131 -0
  255. package/docs/task-safe-review-workflow.md +98 -0
  256. package/docs/threat-model.md +97 -0
  257. package/docs/user-feedback.md +40 -0
  258. package/docs/why-patchwarden.md +110 -0
  259. package/examples/config.example.json +4 -2
  260. package/package.json +28 -11
  261. package/scripts/README.md +15 -6
  262. package/scripts/build.js +11 -0
  263. package/scripts/checks/brand-check.js +3 -0
  264. package/scripts/checks/build-output-check.js +37 -0
  265. package/scripts/checks/control-center-smoke.js +447 -6
  266. package/scripts/checks/control-smoke.js +19 -8
  267. package/scripts/checks/http-mcp-smoke.js +82 -4
  268. package/scripts/checks/lifecycle-smoke.js +39 -33
  269. package/scripts/checks/mcp-manifest-check.js +1 -1
  270. package/scripts/checks/mcp-smoke.js +30 -4
  271. package/scripts/checks/package-manifest-check.js +20 -7
  272. package/scripts/checks/unit-tests.js +3 -5
  273. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  274. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  275. package/scripts/control/manage-patchwarden.ps1 +69 -4
  276. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  277. package/scripts/control/run-background-supervisor.ps1 +41 -0
  278. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  279. package/scripts/generate-demo-gif.py +320 -0
  280. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  281. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  282. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  283. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  284. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  285. package/scripts/lib/clean-generated-output.js +11 -0
  286. package/scripts/lib/file-discovery.js +12 -0
  287. package/scripts/release/desktop-preflight.js +211 -0
  288. package/scripts/release/pack-clean.js +59 -14
  289. package/src/assessments/agentAssessor.ts +29 -15
  290. package/src/assessments/assessmentStore.ts +55 -43
  291. package/src/assessments/confirmCli.ts +5 -4
  292. package/src/config.ts +105 -11
  293. package/src/control/fileManager.ts +40 -0
  294. package/src/control/middleware/auth.ts +35 -0
  295. package/src/control/middleware/static.ts +84 -0
  296. package/src/control/routeTable.ts +301 -0
  297. package/src/control/routes/audit.ts +329 -0
  298. package/src/control/routes/evidence.ts +107 -0
  299. package/src/control/routes/lineage.ts +92 -0
  300. package/src/control/routes/policy.ts +81 -0
  301. package/src/control/routes/process.ts +505 -0
  302. package/src/control/routes/sessions.ts +314 -0
  303. package/src/control/routes/status.ts +405 -0
  304. package/src/control/routes/taskActions.ts +214 -0
  305. package/src/control/routes/tasks.ts +331 -0
  306. package/src/control/routes/workspace.ts +234 -0
  307. package/src/control/runtime.ts +508 -0
  308. package/src/control/server.ts +165 -0
  309. package/src/control/shared.ts +360 -0
  310. package/src/controlCenter.ts +7 -2347
  311. package/src/direct/directAudit.ts +17 -9
  312. package/src/direct/directGuards.ts +100 -51
  313. package/src/direct/directPatch.ts +95 -8
  314. package/src/direct/directSessionStore.ts +211 -55
  315. package/src/direct/directVerification.ts +9 -23
  316. package/src/doctor.ts +766 -492
  317. package/src/goal/acceptanceEngine.ts +1 -1
  318. package/src/goal/goalProgress.ts +17 -13
  319. package/src/goal/goalReport.ts +263 -0
  320. package/src/goal/goalStatus.ts +25 -3
  321. package/src/goal/goalStore.ts +241 -54
  322. package/src/goal/handoffExport.ts +6 -6
  323. package/src/goal/specKitImport.ts +355 -0
  324. package/src/goal/subgoalSync.ts +15 -10
  325. package/src/goal/worktreeManager.ts +139 -22
  326. package/src/httpServer.ts +130 -44
  327. package/src/index.ts +7 -4
  328. package/src/logging.ts +20 -4
  329. package/src/policy/projectPolicy.ts +5 -2
  330. package/src/release/releaseGate.ts +74 -27
  331. package/src/runner/agentInvocation.ts +126 -4
  332. package/src/runner/changeCapture.ts +342 -80
  333. package/src/runner/cli.ts +7 -6
  334. package/src/runner/postTaskCleanup.ts +55 -7
  335. package/src/runner/processSecurity.ts +433 -0
  336. package/src/runner/runTask.ts +441 -292
  337. package/src/runner/simpleProcess.ts +68 -33
  338. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  339. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  340. package/src/runner/taskStatusStore.ts +54 -0
  341. package/src/runner/watch.ts +290 -57
  342. package/src/security/contentRedaction.ts +31 -2
  343. package/src/security/discoveryTokenStore.ts +18 -4
  344. package/src/security/loopbackHost.ts +19 -0
  345. package/src/security/pathGuard.ts +35 -16
  346. package/src/security/planGuard.ts +2 -1
  347. package/src/security/runtimeGuard.ts +9 -6
  348. package/src/security/secretComparison.ts +13 -0
  349. package/src/security/sensitiveGuard.ts +27 -8
  350. package/src/security/toolInvocationGuard.ts +2 -2
  351. package/src/security/workspaceRootGuard.ts +53 -0
  352. package/src/smoke-test.ts +276 -273
  353. package/src/tools/catalog/index.ts +4 -0
  354. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  355. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
  356. package/src/tools/definitions/toolDefs.ts +1264 -0
  357. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  358. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
  359. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
  360. package/src/tools/diagnostics/index.ts +6 -0
  361. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  362. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
  363. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  364. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
  365. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
  366. package/src/tools/direct/index.ts +3 -0
  367. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  368. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  369. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  370. package/src/tools/discovery/index.ts +3 -0
  371. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  372. package/src/tools/dispatch/coreDispatch.ts +379 -0
  373. package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
  374. package/src/tools/dispatch/directDispatch.ts +168 -0
  375. package/src/tools/dispatch/goalDispatch.ts +128 -0
  376. package/src/tools/dispatch/releaseDispatch.ts +66 -0
  377. package/src/tools/dispatch/types.ts +24 -0
  378. package/src/tools/dispatch/validation.ts +78 -0
  379. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  380. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
  381. package/src/tools/goals/index.ts +3 -0
  382. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  383. package/src/tools/registry.ts +54 -1858
  384. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  385. package/src/tools/release/index.ts +2 -0
  386. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  387. package/src/tools/tasks/cancelTask.ts +109 -0
  388. package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
  389. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  390. package/src/tools/tasks/evidencePack.ts +504 -0
  391. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  392. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  393. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  394. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  395. package/src/tools/tasks/index.ts +18 -0
  396. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  397. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  398. package/src/tools/tasks/retryTask.ts +69 -0
  399. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
  400. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  401. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  402. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  403. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  404. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  405. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  406. package/src/tools/workspace/index.ts +10 -0
  407. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  408. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  409. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  410. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  411. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  412. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  413. package/src/tools/workspace/syncFile.ts +292 -0
  414. package/src/utils/atomicFile.ts +58 -0
  415. package/src/utils/boundedFile.ts +116 -0
  416. package/src/utils/lockedJsonFile.ts +251 -0
  417. package/src/utils/platform.ts +31 -0
  418. package/src/utils/stableJson.ts +12 -0
  419. package/src/version.ts +3 -2
  420. package/src/watcherStatus.ts +19 -1
  421. package/ui/desktop-bootstrap.js +11 -0
  422. package/ui/desktop-bridge.js +36 -0
  423. package/ui/desktop.css +160 -0
  424. package/ui/getting-started.js +93 -0
  425. package/ui/i18n.js +484 -0
  426. package/ui/log-parser.js +30 -0
  427. package/ui/pages/audit.html +204 -3
  428. package/ui/pages/dashboard.html +937 -79
  429. package/ui/pages/direct-sessions.html +609 -60
  430. package/ui/pages/getting-started.html +43 -0
  431. package/ui/pages/logs.html +105 -22
  432. package/ui/pages/settings.html +101 -0
  433. package/ui/pages/task-detail.html +468 -438
  434. package/ui/pages/tasks.html +648 -65
  435. package/ui/pages/workspace.html +38 -1
  436. package/ui/settings.js +245 -0
  437. package/PatchWarden-Control.cmd +0 -6
  438. package/Restart-PatchWarden-Control.cmd +0 -6
  439. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  440. package/dist/test/unit/acceptance-engine.test.js +0 -228
  441. package/dist/test/unit/agent-router.test.d.ts +0 -1
  442. package/dist/test/unit/agent-router.test.js +0 -287
  443. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  444. package/dist/test/unit/android-doctor.test.js +0 -118
  445. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  446. package/dist/test/unit/audit-checks.test.js +0 -350
  447. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  448. package/dist/test/unit/chinese-path.test.js +0 -91
  449. package/dist/test/unit/command-guard.test.d.ts +0 -1
  450. package/dist/test/unit/command-guard.test.js +0 -160
  451. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  452. package/dist/test/unit/diagnose-task.test.js +0 -457
  453. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  454. package/dist/test/unit/direct-guards.test.js +0 -213
  455. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  456. package/dist/test/unit/discovery-token-store.test.js +0 -139
  457. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  458. package/dist/test/unit/evidence-pack.test.js +0 -130
  459. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  460. package/dist/test/unit/goal-graph.test.js +0 -298
  461. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  462. package/dist/test/unit/goal-progress.test.js +0 -381
  463. package/dist/test/unit/goal-status.test.d.ts +0 -1
  464. package/dist/test/unit/goal-status.test.js +0 -215
  465. package/dist/test/unit/goal-store.test.d.ts +0 -1
  466. package/dist/test/unit/goal-store.test.js +0 -253
  467. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  468. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  469. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  470. package/dist/test/unit/goal-tools-registry.test.js +0 -190
  471. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  472. package/dist/test/unit/handoff-export.test.js +0 -263
  473. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  474. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  475. package/dist/test/unit/logging.test.d.ts +0 -1
  476. package/dist/test/unit/logging.test.js +0 -397
  477. package/dist/test/unit/path-guard.test.d.ts +0 -1
  478. package/dist/test/unit/path-guard.test.js +0 -109
  479. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  480. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  481. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  482. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  483. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  484. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  485. package/dist/test/unit/release-gate.test.d.ts +0 -1
  486. package/dist/test/unit/release-gate.test.js +0 -242
  487. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  488. package/dist/test/unit/run-task-loop.test.js +0 -380
  489. package/dist/test/unit/safe-status.test.d.ts +0 -1
  490. package/dist/test/unit/safe-status.test.js +0 -165
  491. package/dist/test/unit/safe-views.test.d.ts +0 -1
  492. package/dist/test/unit/safe-views.test.js +0 -171
  493. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  494. package/dist/test/unit/schema-drift-check.test.js +0 -176
  495. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  496. package/dist/test/unit/sensitive-guard.test.js +0 -104
  497. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  498. package/dist/test/unit/subgoal-sync.test.js +0 -183
  499. package/dist/test/unit/sync-file.test.d.ts +0 -1
  500. package/dist/test/unit/sync-file.test.js +0 -154
  501. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  502. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  503. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  504. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  505. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  506. package/dist/test/unit/toolSearch.test.js +0 -571
  507. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  508. package/dist/test/unit/watcher-status.test.js +0 -169
  509. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  510. package/dist/test/unit/worktree-manager.test.js +0 -176
  511. package/dist/tools/applyPatch.js +0 -41
  512. package/dist/tools/cancelTask.d.ts +0 -30
  513. package/dist/tools/cancelTask.js +0 -64
  514. package/dist/tools/evidencePack.js +0 -168
  515. package/dist/tools/finalizeDirectSession.js +0 -84
  516. package/dist/tools/goalSubgoalTask.js +0 -110
  517. package/dist/tools/killTask.d.ts +0 -15
  518. package/dist/tools/retryTask.js +0 -45
  519. package/dist/tools/runVerification.js +0 -36
  520. package/dist/tools/syncFile.js +0 -65
  521. package/docs/control-center/control-center-daily-driver.md +0 -211
  522. package/docs/control-center/control-center-mvp.md +0 -205
  523. package/docs/control-center/control-center-phase2.md +0 -159
  524. package/docs/release-v0.6.0.md +0 -71
  525. package/docs/release-v0.6.1.md +0 -75
  526. package/docs/release-v0.6.4.md +0 -45
  527. package/src/test/unit/acceptance-engine.test.ts +0 -261
  528. package/src/test/unit/agent-router.test.ts +0 -342
  529. package/src/test/unit/android-doctor.test.ts +0 -158
  530. package/src/test/unit/audit-checks.test.ts +0 -567
  531. package/src/test/unit/chinese-path.test.ts +0 -106
  532. package/src/test/unit/command-guard.test.ts +0 -221
  533. package/src/test/unit/diagnose-task.test.ts +0 -544
  534. package/src/test/unit/direct-guards.test.ts +0 -297
  535. package/src/test/unit/discovery-token-store.test.ts +0 -181
  536. package/src/test/unit/evidence-pack.test.ts +0 -142
  537. package/src/test/unit/goal-graph.test.ts +0 -347
  538. package/src/test/unit/goal-progress.test.ts +0 -538
  539. package/src/test/unit/goal-status.test.ts +0 -270
  540. package/src/test/unit/goal-store.test.ts +0 -318
  541. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  542. package/src/test/unit/goal-tools-registry.test.ts +0 -243
  543. package/src/test/unit/handoff-export.test.ts +0 -295
  544. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  545. package/src/test/unit/logging.test.ts +0 -497
  546. package/src/test/unit/path-guard.test.ts +0 -150
  547. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  548. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  549. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  550. package/src/test/unit/release-gate.test.ts +0 -314
  551. package/src/test/unit/run-task-loop.test.ts +0 -425
  552. package/src/test/unit/safe-status.test.ts +0 -187
  553. package/src/test/unit/safe-views.test.ts +0 -184
  554. package/src/test/unit/schema-drift-check.test.ts +0 -260
  555. package/src/test/unit/sensitive-guard.test.ts +0 -124
  556. package/src/test/unit/subgoal-sync.test.ts +0 -236
  557. package/src/test/unit/sync-file.test.ts +0 -231
  558. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  559. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  560. package/src/test/unit/toolSearch.test.ts +0 -756
  561. package/src/test/unit/watcher-status.test.ts +0 -190
  562. package/src/test/unit/worktree-manager.test.ts +0 -247
  563. package/src/tools/cancelTask.ts +0 -70
  564. package/src/tools/evidencePack.ts +0 -205
  565. package/src/tools/retryTask.ts +0 -53
  566. package/src/tools/syncFile.ts +0 -122
  567. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  568. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  569. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  570. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  571. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  572. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  573. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  574. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  575. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  576. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  577. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  578. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  579. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  580. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  581. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  582. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  583. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  584. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  585. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  586. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  587. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  588. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  589. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  590. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  591. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  592. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  593. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  594. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  595. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  596. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  597. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -16,14 +16,21 @@
16
16
  * Run: node dist/runner/watch.js
17
17
  * or: npm run watch
18
18
  */
19
- import { readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
19
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
20
20
  import { resolve, join, dirname } from "node:path";
21
+ import { pathToFileURL } from "node:url";
21
22
  import { loadConfig, getConfig, getTasksDir, resolveWorkspaceRoot } from "../config.js";
22
23
  import { guardWorkspacePath } from "../security/pathGuard.js";
23
24
  import { guardAgentCommand, guardTestCommand } from "../security/commandGuard.js";
24
25
  import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
25
26
  import { captureRepoSnapshot } from "./changeCapture.js";
26
- import { runTask } from "./runTask.js";
27
+ import { mutateTaskStatus } from "./taskStatusStore.js";
28
+ import { logger } from "../logging.js";
29
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
30
+ import { atomicWriteFileSync } from "../utils/atomicFile.js";
31
+ // Note: `runTask` is imported lazily inside `tick()` via `await import("./runTask.js")`.
32
+ // This keeps `import { acquireWatcherLock }` side-effect-free for unit tests and
33
+ // avoids eagerly pulling in the full tools/dispatch graph at module load.
27
34
  // ── Bootstrap ─────────────────────────────────────────────────────
28
35
  loadConfig();
29
36
  const config = getConfig();
@@ -31,34 +38,160 @@ const tasksDir = getTasksDir(config);
31
38
  const wsRoot = resolveWorkspaceRoot(config);
32
39
  const POLL_INTERVAL_MS = 4000;
33
40
  const WATCHER_HEARTBEAT_FILE = join(dirname(tasksDir), "watcher-heartbeat.json");
41
+ const WATCHER_LOCK_FILE = join(dirname(tasksDir), "watcher.lock");
34
42
  const WATCHER_STARTED_AT = new Date().toISOString();
35
43
  const WATCHER_INSTANCE_ID = process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || `standalone-${process.pid}-${Date.now()}`;
36
44
  const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID))
37
45
  ? Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID)
38
46
  : null;
39
- mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
40
- console.error(`[watcher] Workspace: ${wsRoot}`);
41
- console.error(`[watcher] Tasks: ${tasksDir}`);
42
- console.error(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
43
- console.error(`[watcher] Press Ctrl+C to stop`);
44
47
  // Track executed tasks to prevent re-execution
45
48
  const executedTasks = new Set();
49
+ let consecutiveTickFailures = 0;
50
+ // ── Single-instance lock ─────────────────────────────────────────
51
+ // watch.ts is a long-running poller; without a lock, two watchers started
52
+ // against the same workspace would race on the same pending tasks. The lock
53
+ // is best-effort: it detects a live PID via process.kill(pid, 0) and refuses
54
+ // to start when one is found. A dead PID (crashed watcher) is taken over.
55
+ export class WatcherAlreadyRunningError extends Error {
56
+ constructor(message) {
57
+ super(message);
58
+ this.name = "WatcherAlreadyRunningError";
59
+ }
60
+ }
61
+ function errorCode(error) {
62
+ return error && typeof error === "object" && "code" in error
63
+ ? String(error.code || "")
64
+ : null;
65
+ }
66
+ function readWatcherLock(lockFilePath) {
67
+ try {
68
+ const parsed = JSON.parse(readFileSync(lockFilePath, "utf-8"));
69
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
70
+ ? parsed
71
+ : null;
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ function processIsAlive(pid) {
78
+ try {
79
+ process.kill(pid, 0);
80
+ return true;
81
+ }
82
+ catch (error) {
83
+ // EPERM means the process exists but the current user cannot signal it.
84
+ return errorCode(error) === "EPERM";
85
+ }
86
+ }
87
+ function runningWatcherError(lockData) {
88
+ return new WatcherAlreadyRunningError(`[watcher] Already running (pid=${lockData?.pid ?? "unknown"}, instance=${lockData?.instance_id ?? "unknown"}). Exiting.`);
89
+ }
90
+ function createWatcherLockExclusive(lockFilePath, content) {
91
+ const descriptor = openSync(lockFilePath, "wx", 0o600);
92
+ let writeError = null;
93
+ try {
94
+ writeFileSync(descriptor, content, "utf-8");
95
+ }
96
+ catch (error) {
97
+ writeError = error;
98
+ }
99
+ finally {
100
+ closeSync(descriptor);
101
+ }
102
+ if (writeError) {
103
+ try {
104
+ unlinkSync(lockFilePath);
105
+ }
106
+ catch { /* best effort after a partial write */ }
107
+ throw writeError;
108
+ }
109
+ }
110
+ /**
111
+ * Acquire the watcher lock at `lockFilePath`. If a lock file exists and its
112
+ * recorded PID is still alive (per `process.kill(pid, 0)`), throws
113
+ * {@link WatcherAlreadyRunningError}. Otherwise overwrites the lock atomically.
114
+ *
115
+ * Exported for unit testing; `lockFilePath` defaults to the module-level
116
+ * {@link WATCHER_LOCK_FILE} but tests pass a temp path.
117
+ */
118
+ export function acquireWatcherLock(lockFilePath = WATCHER_LOCK_FILE) {
119
+ const lockContent = JSON.stringify({
120
+ pid: process.pid,
121
+ instance_id: WATCHER_INSTANCE_ID,
122
+ started_at: WATCHER_STARTED_AT,
123
+ launcher_pid: WATCHER_LAUNCHER_PID,
124
+ }, null, 2);
125
+ try {
126
+ createWatcherLockExclusive(lockFilePath, lockContent);
127
+ return;
128
+ }
129
+ catch (error) {
130
+ if (errorCode(error) !== "EEXIST")
131
+ throw error;
132
+ }
133
+ const existing = readWatcherLock(lockFilePath);
134
+ if (typeof existing?.pid === "number" && processIsAlive(existing.pid)) {
135
+ throw runningWatcherError(existing);
136
+ }
137
+ try {
138
+ unlinkSync(lockFilePath);
139
+ }
140
+ catch (error) {
141
+ if (errorCode(error) !== "ENOENT")
142
+ throw error;
143
+ }
144
+ try {
145
+ createWatcherLockExclusive(lockFilePath, lockContent);
146
+ }
147
+ catch (error) {
148
+ if (errorCode(error) === "EEXIST") {
149
+ throw runningWatcherError(readWatcherLock(lockFilePath));
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+ export function releaseWatcherLock(lockFilePath = WATCHER_LOCK_FILE, instanceId = WATCHER_INSTANCE_ID, pid = process.pid) {
155
+ const existing = readWatcherLock(lockFilePath);
156
+ if (existing?.pid !== pid || existing.instance_id !== instanceId)
157
+ return false;
158
+ try {
159
+ unlinkSync(lockFilePath);
160
+ return true;
161
+ }
162
+ catch (error) {
163
+ if (errorCode(error) === "ENOENT")
164
+ return false;
165
+ throw error;
166
+ }
167
+ }
168
+ export function createNonOverlappingRunner(operation, onSkipped = () => undefined) {
169
+ let running = false;
170
+ return async () => {
171
+ if (running) {
172
+ onSkipped();
173
+ return false;
174
+ }
175
+ running = true;
176
+ try {
177
+ await operation();
178
+ return true;
179
+ }
180
+ finally {
181
+ running = false;
182
+ }
183
+ };
184
+ }
46
185
  // ── Main loop ─────────────────────────────────────────────────────
47
186
  async function tick() {
48
187
  try {
49
- const heartbeatTemporary = `${WATCHER_HEARTBEAT_FILE}.${WATCHER_INSTANCE_ID}.tmp`;
50
- writeFileSync(heartbeatTemporary, JSON.stringify({
51
- status: "running",
52
- pid: process.pid,
53
- started_at: WATCHER_STARTED_AT,
54
- last_heartbeat_at: new Date().toISOString(),
55
- instance_id: WATCHER_INSTANCE_ID,
56
- launcher_pid: WATCHER_LAUNCHER_PID,
57
- }, null, 2), "utf-8");
58
- renameSync(heartbeatTemporary, WATCHER_HEARTBEAT_FILE);
188
+ writeWatcherHeartbeat("running");
59
189
  // Ensure tasks directory exists
60
- if (!existsSync(tasksDir))
190
+ if (!existsSync(tasksDir)) {
191
+ consecutiveTickFailures = 0;
192
+ writeWatcherHeartbeat("running");
61
193
  return;
194
+ }
62
195
  const entries = readdirSync(tasksDir, { withFileTypes: true });
63
196
  const taskDirs = entries.filter((e) => e.isDirectory());
64
197
  for (const entry of taskDirs) {
@@ -72,21 +205,34 @@ async function tick() {
72
205
  continue;
73
206
  let statusData;
74
207
  try {
75
- statusData = JSON.parse(readFileSync(statusFile, "utf-8"));
208
+ const parsed = JSON.parse(readFileSync(statusFile, "utf-8"));
209
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
210
+ throw new Error("status.json must contain an object");
211
+ }
212
+ statusData = parsed;
76
213
  }
77
- catch {
78
- continue; // corrupted status, skip
214
+ catch (error) {
215
+ logger.warn("[watcher] Skipping unreadable task status", {
216
+ task_id: taskId,
217
+ error: error instanceof Error ? error.message : String(error),
218
+ });
219
+ continue;
79
220
  }
80
221
  if (statusData.status !== "pending")
81
222
  continue;
82
223
  // ── Pre-flight safety checks ──
83
224
  try {
84
225
  // Check repo_path
85
- const resolvedRepoPath = guardWorkspacePath(statusData.resolved_repo_path || statusData.repo_path || wsRoot, wsRoot);
226
+ const repoPath = typeof statusData.resolved_repo_path === "string"
227
+ ? statusData.resolved_repo_path
228
+ : typeof statusData.repo_path === "string"
229
+ ? statusData.repo_path
230
+ : wsRoot;
231
+ const resolvedRepoPath = guardWorkspacePath(repoPath, wsRoot);
86
232
  // Check agent
87
- guardAgentCommand(statusData.agent, config);
233
+ guardAgentCommand(typeof statusData.agent === "string" ? statusData.agent : "", config);
88
234
  // Check test_command
89
- if (statusData.test_command) {
235
+ if (typeof statusData.test_command === "string" && statusData.test_command) {
90
236
  guardTestCommand(statusData.test_command, config, resolvedRepoPath);
91
237
  }
92
238
  if (Array.isArray(statusData.verify_commands)) {
@@ -95,57 +241,122 @@ async function tick() {
95
241
  }
96
242
  }
97
243
  // Assessment freshness revalidation
98
- if (statusData.assessment_id) {
99
- const preExecSnapshot = captureRepoSnapshot(resolvedRepoPath);
100
- const validation = validateAssessmentFreshness(String(statusData.assessment_id), preExecSnapshot);
244
+ if (typeof statusData.assessment_id === "string" && statusData.assessment_id) {
245
+ const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
246
+ const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
101
247
  if (!validation.valid) {
102
248
  throw new Error(`assessment validation failed: ${validation.failure_reason}`);
103
249
  }
104
250
  }
105
251
  }
106
252
  catch (err) {
107
- const errMsg = `[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`;
108
- console.error(errMsg);
253
+ const errMsg = redactSensitiveContent(`[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`).content;
254
+ logger.error(errMsg);
109
255
  // Write error and mark as failed so it doesn't get re-picked
110
256
  try {
111
- writeFileSync(join(taskDir, "error.log"), errMsg, "utf-8");
112
- const data = JSON.parse(readFileSync(statusFile, "utf-8"));
113
- data.status = "failed";
114
- data.error = errMsg;
115
- data.updated_at = new Date().toISOString();
116
- writeFileSync(statusFile, JSON.stringify(data, null, 2), "utf-8");
257
+ atomicWriteFileSync(join(taskDir, "error.log"), errMsg);
258
+ mutateTaskStatus(statusFile, (current) => {
259
+ if (current.status !== "pending")
260
+ return { result: undefined };
261
+ const next = {
262
+ ...current,
263
+ status: "failed",
264
+ error: errMsg,
265
+ updated_at: new Date().toISOString(),
266
+ };
267
+ return { next, result: undefined };
268
+ });
269
+ }
270
+ catch (err) {
271
+ logger.warn("watcher error-log write failed", { error: err instanceof Error ? err.message : String(err) });
117
272
  }
118
- catch { }
119
273
  executedTasks.add(taskId);
120
274
  continue;
121
275
  }
122
276
  // ── Execute ──
123
- console.error(`[watcher] Executing: ${taskId}`);
277
+ logger.info(`[watcher] Executing: ${taskId}`);
124
278
  executedTasks.add(taskId);
125
279
  try {
280
+ const { runTask } = await import("./runTask.js");
126
281
  const result = await runTask(taskId);
127
- console.error(`[watcher] ${taskId} → ${result.status}`);
282
+ logger.info(`[watcher] ${taskId} → ${result.status}`);
128
283
  }
129
284
  catch (err) {
130
- console.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
285
+ logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
131
286
  }
132
287
  }
288
+ consecutiveTickFailures = 0;
289
+ writeWatcherHeartbeat("running");
290
+ }
291
+ catch (err) {
292
+ consecutiveTickFailures += 1;
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ logger.error(`[watcher] Tick error (${consecutiveTickFailures} consecutive): ${message}`);
295
+ try {
296
+ writeWatcherHeartbeat(consecutiveTickFailures >= 3 ? "degraded" : "running", message);
297
+ }
298
+ catch (heartbeatError) {
299
+ logger.error("[watcher] Failed to persist degraded heartbeat", {
300
+ error: heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError),
301
+ });
302
+ }
303
+ }
304
+ }
305
+ function writeWatcherHeartbeat(status, lastError) {
306
+ atomicWriteFileSync(WATCHER_HEARTBEAT_FILE, JSON.stringify({
307
+ status,
308
+ pid: process.pid,
309
+ started_at: WATCHER_STARTED_AT,
310
+ last_heartbeat_at: new Date().toISOString(),
311
+ instance_id: WATCHER_INSTANCE_ID,
312
+ launcher_pid: WATCHER_LAUNCHER_PID,
313
+ consecutive_failures: consecutiveTickFailures,
314
+ last_error: lastError ? lastError.slice(0, 500) : null,
315
+ }, null, 2));
316
+ }
317
+ // ── Start (only when executed as a script) ─────────────────────────
318
+ // The bootstrap below has process-wide side effects (mkdir, signal handlers,
319
+ // polling interval, lock acquisition). Guarding it with a main-module check
320
+ // keeps `import { acquireWatcherLock }` safe for unit tests — without it,
321
+ // importing this module would start polling the real tasks dir and register
322
+ // SIGINT/SIGTERM handlers that interfere with the test runner.
323
+ const isMainModule = !!process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
324
+ if (isMainModule) {
325
+ mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
326
+ logger.info(`[watcher] Workspace: ${wsRoot}`);
327
+ logger.info(`[watcher] Tasks: ${tasksDir}`);
328
+ logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
329
+ logger.info(`[watcher] Press Ctrl+C to stop`);
330
+ try {
331
+ acquireWatcherLock(WATCHER_LOCK_FILE);
133
332
  }
134
333
  catch (err) {
135
- console.error(`[watcher] Tick error: ${err instanceof Error ? err.message : String(err)}`);
334
+ if (err instanceof WatcherAlreadyRunningError) {
335
+ logger.error(err.message);
336
+ process.exit(1);
337
+ }
338
+ throw err;
339
+ }
340
+ logger.info("[watcher] Started");
341
+ const scheduledTick = createNonOverlappingRunner(tick, () => {
342
+ logger.warn("[watcher] Previous tick is still running; skipping overlapping poll");
343
+ });
344
+ setInterval(() => { void scheduledTick(); }, POLL_INTERVAL_MS);
345
+ // Run first tick immediately
346
+ void scheduledTick();
347
+ // Graceful shutdown
348
+ function shutdown() {
349
+ logger.info("[watcher] Stopped");
350
+ try {
351
+ releaseWatcherLock(WATCHER_LOCK_FILE);
352
+ }
353
+ catch (error) {
354
+ logger.warn("[watcher] Failed to release watcher lock", {
355
+ error: error instanceof Error ? error.message : String(error),
356
+ });
357
+ }
358
+ process.exit(0);
136
359
  }
360
+ process.once("SIGINT", shutdown);
361
+ process.once("SIGTERM", shutdown);
137
362
  }
138
- // ── Start ─────────────────────────────────────────────────────────
139
- console.error("[watcher] Started");
140
- setInterval(tick, POLL_INTERVAL_MS);
141
- // Run first tick immediately
142
- tick();
143
- // Graceful shutdown
144
- process.on("SIGINT", () => {
145
- console.error("[watcher] Stopped");
146
- process.exit(0);
147
- });
148
- process.on("SIGTERM", () => {
149
- console.error("[watcher] Stopped");
150
- process.exit(0);
151
- });
@@ -8,5 +8,11 @@ export interface StructuredRedactionResult<T> {
8
8
  redacted: boolean;
9
9
  redaction_categories: string[];
10
10
  }
11
+ export interface RedactionCategoryCount {
12
+ category: string;
13
+ reason: string;
14
+ count: number;
15
+ }
16
+ export declare function countRedactionsByCategory(input: string): RedactionCategoryCount[];
11
17
  export declare function redactSensitiveContent(input: string): RedactionResult;
12
18
  export declare function redactSensitiveValue<T>(input: T): StructuredRedactionResult<T>;
@@ -1,30 +1,52 @@
1
1
  const RULES = [
2
2
  {
3
3
  category: "private_key",
4
+ reason: "matched private key block",
4
5
  pattern: /-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi,
5
6
  replace: "[REDACTED PRIVATE KEY]",
6
7
  },
7
8
  {
8
9
  category: "bearer_token",
10
+ reason: "matched bearer token",
9
11
  pattern: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}={0,2}/gi,
10
12
  replace: "Bearer [REDACTED]",
11
13
  },
12
14
  {
13
15
  category: "npm_token",
16
+ reason: "matched npm auth token",
14
17
  pattern: /(\b_?authToken\s*=\s*)[^\s\r\n]+/gi,
15
18
  replace: "$1[REDACTED]",
16
19
  },
17
20
  {
18
21
  category: "credential_assignment",
19
- pattern: /\b((?:access[_ -]?token|api[_ -]?key|secret|password|credential|token)\s*[:=]\s*)(["']?)([^\s,"'\]}]{8,})(["']?)/gi,
22
+ reason: "matched credential assignment",
23
+ 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,
20
24
  replace: "$1$2[REDACTED]$4",
21
25
  },
22
26
  {
23
27
  category: "known_token_format",
24
- pattern: /\b(?:sk|ghp|github_pat)_[A-Za-z0-9_-]{16,}\b/g,
28
+ reason: "matched known token format",
29
+ 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,
25
30
  replace: "[REDACTED TOKEN]",
26
31
  },
27
32
  ];
33
+ export function countRedactionsByCategory(input) {
34
+ const counts = new Map();
35
+ for (const rule of RULES) {
36
+ rule.pattern.lastIndex = 0;
37
+ const matches = input.match(rule.pattern);
38
+ if (matches && matches.length > 0) {
39
+ counts.set(rule.category, (counts.get(rule.category) || 0) + matches.length);
40
+ }
41
+ }
42
+ const result = [];
43
+ for (const rule of RULES) {
44
+ const count = counts.get(rule.category);
45
+ if (count)
46
+ result.push({ category: rule.category, reason: rule.reason, count });
47
+ }
48
+ return result;
49
+ }
28
50
  export function redactSensitiveContent(input) {
29
51
  let content = input;
30
52
  const categories = [];
@@ -9,8 +9,8 @@
9
9
  * - 不持久化到磁盘(内存 Map,进程内共享,单例)
10
10
  * - token 单次使用语义(consumeToken 后即删除)
11
11
  */
12
- import type { ToolRisk } from "../tools/toolRegistry.js";
13
- import type { ToolProfile } from "../tools/toolCatalog.js";
12
+ import type { ToolRisk } from "../tools/catalog/toolRegistry.js";
13
+ import type { ToolProfile } from "../tools/catalog/toolCatalog.js";
14
14
  export interface DiscoveryTokenRecord {
15
15
  token: string;
16
16
  toolName: string;
@@ -31,6 +31,7 @@ export interface IssueTokenInput {
31
31
  allowedScope?: string[];
32
32
  ttlMs?: number;
33
33
  }
34
+ export declare const MAX_ACTIVE_DISCOVERY_TOKENS = 1024;
34
35
  /**
35
36
  * 生成并保存 discovery token,返回 token id。
36
37
  * token id 格式:dst_{YYYYMMDD}_{randomHex12}
@@ -13,6 +13,7 @@ import { randomBytes } from "node:crypto";
13
13
  import { PatchWardenError } from "../errors.js";
14
14
  // ── 常量 ──────────────────────────────────────────────────────────
15
15
  const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
16
+ export const MAX_ACTIVE_DISCOVERY_TOKENS = 1024;
16
17
  // ── 模块级 token store(单例,进程内共享) ───────────────────────
17
18
  const tokenStore = new Map();
18
19
  // ── 内部工具 ──────────────────────────────────────────────────────
@@ -27,15 +28,29 @@ function generateTokenId() {
27
28
  const randomPart = randomBytes(6).toString("hex"); // 12 位 hex
28
29
  return `dst_${datePart}_${randomPart}`;
29
30
  }
31
+ function pruneTokenStore(nowMs) {
32
+ for (const [token, record] of tokenStore) {
33
+ if (Date.parse(record.expiresAt) <= nowMs)
34
+ tokenStore.delete(token);
35
+ }
36
+ while (tokenStore.size >= MAX_ACTIVE_DISCOVERY_TOKENS) {
37
+ const oldest = tokenStore.keys().next().value;
38
+ if (!oldest)
39
+ break;
40
+ tokenStore.delete(oldest);
41
+ }
42
+ }
30
43
  // ── 公共 API ──────────────────────────────────────────────────────
31
44
  /**
32
45
  * 生成并保存 discovery token,返回 token id。
33
46
  * token id 格式:dst_{YYYYMMDD}_{randomHex12}
34
47
  */
35
48
  export function issueToken(input) {
36
- const now = new Date();
49
+ const nowMs = Date.now();
50
+ pruneTokenStore(nowMs);
51
+ const now = new Date(nowMs);
37
52
  const issuedAt = now.toISOString();
38
- const expiresAt = new Date(Date.now() + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
53
+ const expiresAt = new Date(nowMs + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
39
54
  const token = generateTokenId();
40
55
  const record = {
41
56
  token,
@@ -0,0 +1,2 @@
1
+ /** Validate an HTTP Host header for a service bound to IPv4 loopback. */
2
+ export declare function isTrustedLoopbackHostHeader(value: string | string[] | undefined, expectedPort: number): boolean;
@@ -0,0 +1,16 @@
1
+ import { firstHeaderValue } from "./secretComparison.js";
2
+ const LOOPBACK_HOST_RE = /^(127\.0\.0\.1|localhost)(?::([0-9]{1,5}))?$/i;
3
+ /** Validate an HTTP Host header for a service bound to IPv4 loopback. */
4
+ export function isTrustedLoopbackHostHeader(value, expectedPort) {
5
+ const raw = firstHeaderValue(value).trim();
6
+ const match = LOOPBACK_HOST_RE.exec(raw);
7
+ if (!match)
8
+ return false;
9
+ if (match[2] === undefined)
10
+ return true;
11
+ const declaredPort = Number(match[2]);
12
+ return Number.isInteger(declaredPort)
13
+ && declaredPort >= 1
14
+ && declaredPort <= 65_535
15
+ && declaredPort === expectedPort;
16
+ }
@@ -1,5 +1,5 @@
1
1
  import { basename, dirname, isAbsolute, normalize, parse, resolve, sep, } from "node:path";
2
- import { realpathSync } from "node:fs";
2
+ import { lstatSync, readlinkSync, realpathSync } from "node:fs";
3
3
  import { PatchWardenError } from "../errors.js";
4
4
  export function guardPath(requestedPath, workspaceRoot, allowedPrefix) {
5
5
  const ws = normalize(resolve(workspaceRoot));
@@ -55,7 +55,7 @@ function realPathOrSelf(value) {
55
55
  return normalize(value);
56
56
  }
57
57
  }
58
- function realPathOrExistingPrefix(candidate) {
58
+ function realPathOrExistingPrefix(candidate, seenLinks = new Set()) {
59
59
  const normalized = normalize(candidate);
60
60
  const root = parse(normalized).root;
61
61
  const suffix = [];
@@ -64,7 +64,33 @@ function realPathOrExistingPrefix(candidate) {
64
64
  try {
65
65
  return resolve(realpathSync(current), ...suffix);
66
66
  }
67
- catch {
67
+ catch (error) {
68
+ try {
69
+ const metadata = lstatSync(current);
70
+ if (metadata.isSymbolicLink()) {
71
+ const linkKey = process.platform === "win32" ? current.toLowerCase() : current;
72
+ if (seenLinks.has(linkKey)) {
73
+ throw new PatchWardenError("workspace_path_escape", `Symbolic link cycle detected while resolving "${candidate}"`, "Remove the cyclic link and use a path inside the configured workspace.", true, { path: candidate, operation: "path_access", safe_alternative: "Use a non-link path inside workspaceRoot." });
74
+ }
75
+ const nextSeen = new Set(seenLinks);
76
+ nextSeen.add(linkKey);
77
+ const linkTarget = readlinkSync(current);
78
+ const resolvedTarget = isAbsolute(linkTarget)
79
+ ? resolve(linkTarget)
80
+ : resolve(dirname(current), linkTarget);
81
+ return resolve(realPathOrExistingPrefix(resolvedTarget, nextSeen), ...suffix);
82
+ }
83
+ }
84
+ catch (linkError) {
85
+ if (linkError instanceof PatchWardenError)
86
+ throw linkError;
87
+ const linkCode = linkError.code;
88
+ if (linkCode !== "ENOENT" && linkCode !== "ENOTDIR")
89
+ throw linkError;
90
+ }
91
+ const code = error.code;
92
+ if (code !== "ENOENT" && code !== "ENOTDIR")
93
+ throw error;
68
94
  suffix.unshift(basename(current));
69
95
  const parent = dirname(current);
70
96
  if (parent === current)
@@ -72,24 +98,13 @@ function realPathOrExistingPrefix(candidate) {
72
98
  current = parent;
73
99
  }
74
100
  }
75
- try {
76
- return resolve(realpathSync(root || current), ...suffix);
77
- }
78
- catch {
79
- return normalized;
80
- }
101
+ return resolve(realpathSync(root || current), ...suffix);
81
102
  }
82
103
  function assertInside(candidate, root, requestedPath, message) {
83
104
  const normalizedRoot = normalize(root);
84
105
  const normalizedCandidate = normalize(candidate);
85
- const checkRoot = process.platform === "win32"
86
- ? normalizedRoot.toLowerCase()
87
- : normalizedRoot;
88
- const checkCandidate = process.platform === "win32"
89
- ? normalizedCandidate.toLowerCase()
90
- : normalizedCandidate;
91
- if (checkCandidate !== checkRoot &&
92
- !checkCandidate.startsWith(checkRoot + sep)) {
106
+ if (normalizedCandidate !== normalizedRoot &&
107
+ !normalizedCandidate.startsWith(normalizedRoot + sep)) {
93
108
  if (message.startsWith("Path escapes workspace")) {
94
109
  throw new PatchWardenError("workspace_path_escape", `Path escapes workspace: "${requestedPath}" resolves to "${normalizedCandidate}" which is outside "${normalizedRoot}"`, "Use a path inside the configured workspace and allowed prefix.", true, { path: requestedPath, operation: "path_access", safe_alternative: "Read a path inside the configured workspace and allowed prefix." });
95
110
  }
@@ -1,4 +1,5 @@
1
1
  import { PatchWardenError } from "../errors.js";
2
+ import { redactSensitiveContent } from "./contentRedaction.js";
2
3
  const PLAN_RULES = [
3
4
  {
4
5
  id: "plan_secret_access",
@@ -35,7 +36,7 @@ export function guardPlanContent(title, content) {
35
36
  throw new PatchWardenError(rule.id, `save_plan blocked content in category "${rule.category}".`, rule.suggestion, true, {
36
37
  operation: "save_plan",
37
38
  matched_category: rule.category,
38
- matched_text: match[0].slice(0, 160),
39
+ matched_text: redactSensitiveContent(match[0]).content.slice(0, 160),
39
40
  });
40
41
  }
41
42
  }