patchwarden 1.5.1 → 1.6.2

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