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