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
@@ -17,14 +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";
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.
28
44
 
29
45
  // ── Bootstrap ─────────────────────────────────────────────────────
30
46
 
@@ -35,37 +51,175 @@ const wsRoot = resolveWorkspaceRoot(config);
35
51
 
36
52
  const POLL_INTERVAL_MS = 4000;
37
53
  const WATCHER_HEARTBEAT_FILE = join(dirname(tasksDir), "watcher-heartbeat.json");
54
+ const WATCHER_LOCK_FILE = join(dirname(tasksDir), "watcher.lock");
38
55
  const WATCHER_STARTED_AT = new Date().toISOString();
39
56
  const WATCHER_INSTANCE_ID = process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || `standalone-${process.pid}-${Date.now()}`;
40
57
  const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID))
41
58
  ? Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID)
42
59
  : null;
43
- mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
44
-
45
- console.error(`[watcher] Workspace: ${wsRoot}`);
46
- console.error(`[watcher] Tasks: ${tasksDir}`);
47
- console.error(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
48
- console.error(`[watcher] Press Ctrl+C to stop`);
49
-
50
60
  // Track executed tasks to prevent re-execution
51
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
+ }
52
211
 
53
212
  // ── Main loop ─────────────────────────────────────────────────────
54
213
 
55
214
  async function tick() {
56
215
  try {
57
- const heartbeatTemporary = `${WATCHER_HEARTBEAT_FILE}.${WATCHER_INSTANCE_ID}.tmp`;
58
- writeFileSync(heartbeatTemporary, JSON.stringify({
59
- status: "running",
60
- pid: process.pid,
61
- started_at: WATCHER_STARTED_AT,
62
- last_heartbeat_at: new Date().toISOString(),
63
- instance_id: WATCHER_INSTANCE_ID,
64
- launcher_pid: WATCHER_LAUNCHER_PID,
65
- }, null, 2), "utf-8");
66
- renameSync(heartbeatTemporary, WATCHER_HEARTBEAT_FILE);
216
+ writeWatcherHeartbeat("running");
67
217
  // Ensure tasks directory exists
68
- if (!existsSync(tasksDir)) return;
218
+ if (!existsSync(tasksDir)) {
219
+ consecutiveTickFailures = 0;
220
+ writeWatcherHeartbeat("running");
221
+ return;
222
+ }
69
223
 
70
224
  const entries = readdirSync(tasksDir, { withFileTypes: true });
71
225
  const taskDirs = entries.filter((e) => e.isDirectory());
@@ -81,11 +235,19 @@ async function tick() {
81
235
 
82
236
  if (!existsSync(statusFile)) continue;
83
237
 
84
- let statusData: any;
238
+ let statusData: Record<string, unknown>;
85
239
  try {
86
- statusData = JSON.parse(readFileSync(statusFile, "utf-8"));
87
- } catch {
88
- 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;
89
251
  }
90
252
 
91
253
  if (statusData.status !== "pending") continue;
@@ -93,13 +255,18 @@ async function tick() {
93
255
  // ── Pre-flight safety checks ──
94
256
  try {
95
257
  // Check repo_path
96
- 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);
97
264
 
98
265
  // Check agent
99
- guardAgentCommand(statusData.agent, config);
266
+ guardAgentCommand(typeof statusData.agent === "string" ? statusData.agent : "", config);
100
267
 
101
268
  // Check test_command
102
- if (statusData.test_command) {
269
+ if (typeof statusData.test_command === "string" && statusData.test_command) {
103
270
  guardTestCommand(statusData.test_command, config, resolvedRepoPath);
104
271
  }
105
272
  if (Array.isArray(statusData.verify_commands)) {
@@ -109,60 +276,126 @@ async function tick() {
109
276
  }
110
277
 
111
278
  // Assessment freshness revalidation
112
- if (statusData.assessment_id) {
113
- const preExecSnapshot = captureRepoSnapshot(resolvedRepoPath);
114
- const validation = validateAssessmentFreshness(String(statusData.assessment_id), preExecSnapshot);
279
+ if (typeof statusData.assessment_id === "string" && statusData.assessment_id) {
280
+ const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
281
+ const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
115
282
  if (!validation.valid) {
116
283
  throw new Error(`assessment validation failed: ${validation.failure_reason}`);
117
284
  }
118
285
  }
119
286
  } catch (err) {
120
- const errMsg = `[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`;
121
- console.error(errMsg);
287
+ const errMsg = redactSensitiveContent(
288
+ `[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`,
289
+ ).content;
290
+ logger.error(errMsg);
122
291
 
123
292
  // Write error and mark as failed so it doesn't get re-picked
124
293
  try {
125
- writeFileSync(join(taskDir, "error.log"), errMsg, "utf-8");
126
- const data = JSON.parse(readFileSync(statusFile, "utf-8"));
127
- data.status = "failed";
128
- data.error = errMsg;
129
- data.updated_at = new Date().toISOString();
130
- writeFileSync(statusFile, JSON.stringify(data, null, 2), "utf-8");
131
- } catch {}
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
+ });
305
+ } catch (err) {
306
+ logger.warn("watcher error-log write failed", { error: err instanceof Error ? err.message : String(err) });
307
+ }
132
308
  executedTasks.add(taskId);
133
309
  continue;
134
310
  }
135
311
 
136
312
  // ── Execute ──
137
- console.error(`[watcher] Executing: ${taskId}`);
313
+ logger.info(`[watcher] Executing: ${taskId}`);
138
314
  executedTasks.add(taskId);
139
315
 
140
316
  try {
317
+ const { runTask } = await import("./runTask.js");
141
318
  const result = await runTask(taskId);
142
- console.error(`[watcher] ${taskId} → ${result.status}`);
319
+ logger.info(`[watcher] ${taskId} → ${result.status}`);
143
320
  } catch (err) {
144
- console.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
321
+ logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
145
322
  }
146
323
  }
324
+ consecutiveTickFailures = 0;
325
+ writeWatcherHeartbeat("running");
147
326
  } catch (err) {
148
- console.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
+ }
149
337
  }
150
338
  }
151
339
 
152
- // ── 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`);
153
369
 
154
- console.error("[watcher] Started");
155
- 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
+ }
156
379
 
157
- // Run first tick immediately
158
- 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);
159
385
 
160
- // Graceful shutdown
161
- process.on("SIGINT", () => {
162
- console.error("[watcher] Stopped");
163
- process.exit(0);
164
- });
165
- process.on("SIGTERM", () => {
166
- console.error("[watcher] Stopped");
167
- process.exit(0);
168
- });
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
+ }
@@ -12,6 +12,7 @@ export interface StructuredRedactionResult<T> {
12
12
 
13
13
  interface RedactionRule {
14
14
  category: string;
15
+ reason: string;
15
16
  pattern: RegExp;
16
17
  replace: string;
17
18
  }
@@ -19,31 +20,59 @@ interface RedactionRule {
19
20
  const RULES: RedactionRule[] = [
20
21
  {
21
22
  category: "private_key",
23
+ reason: "matched private key block",
22
24
  pattern: /-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi,
23
25
  replace: "[REDACTED PRIVATE KEY]",
24
26
  },
25
27
  {
26
28
  category: "bearer_token",
29
+ reason: "matched bearer token",
27
30
  pattern: /\bBearer\s+[A-Za-z0-9._~+\/-]{12,}={0,2}/gi,
28
31
  replace: "Bearer [REDACTED]",
29
32
  },
30
33
  {
31
34
  category: "npm_token",
35
+ reason: "matched npm auth token",
32
36
  pattern: /(\b_?authToken\s*=\s*)[^\s\r\n]+/gi,
33
37
  replace: "$1[REDACTED]",
34
38
  },
35
39
  {
36
40
  category: "credential_assignment",
37
- pattern: /\b((?:access[_ -]?token|api[_ -]?key|secret|password|credential|token)\s*[:=]\s*)(["']?)([^\s,"'\]}]{8,})(["']?)/gi,
41
+ reason: "matched credential assignment",
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,
38
43
  replace: "$1$2[REDACTED]$4",
39
44
  },
40
45
  {
41
46
  category: "known_token_format",
42
- pattern: /\b(?:sk|ghp|github_pat)_[A-Za-z0-9_-]{16,}\b/g,
47
+ reason: "matched known token format",
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,
43
49
  replace: "[REDACTED TOKEN]",
44
50
  },
45
51
  ];
46
52
 
53
+ export interface RedactionCategoryCount {
54
+ category: string;
55
+ reason: string;
56
+ count: number;
57
+ }
58
+
59
+ export function countRedactionsByCategory(input: string): RedactionCategoryCount[] {
60
+ const counts = new Map<string, number>();
61
+ for (const rule of RULES) {
62
+ rule.pattern.lastIndex = 0;
63
+ const matches = input.match(rule.pattern);
64
+ if (matches && matches.length > 0) {
65
+ counts.set(rule.category, (counts.get(rule.category) || 0) + matches.length);
66
+ }
67
+ }
68
+ const result: RedactionCategoryCount[] = [];
69
+ for (const rule of RULES) {
70
+ const count = counts.get(rule.category);
71
+ if (count) result.push({ category: rule.category, reason: rule.reason, count });
72
+ }
73
+ return result;
74
+ }
75
+
47
76
  export function redactSensitiveContent(input: string): RedactionResult {
48
77
  let content = input;
49
78
  const categories: string[] = [];
@@ -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
  }