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
@@ -1,497 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { mkdtempSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs";
4
- import { join } from "node:path";
5
- import { tmpdir } from "node:os";
6
- import {
7
- Logger,
8
- logUnhandledError,
9
- installGlobalHandlers,
10
- computeArgumentsDigest,
11
- logToolInvocation,
12
- type InvocationLogEntry,
13
- } from "../../logging.js";
14
-
15
- // ── Test helpers ──────────────────────────────────────────────────
16
-
17
- /**
18
- * Capture everything written to stderr during `fn` and return it as an
19
- * array of raw chunks (one per `process.stderr.write` call).
20
- */
21
- function captureStderr(fn: () => void): string[] {
22
- const chunks: string[] = [];
23
- const original = process.stderr.write;
24
- process.stderr.write = ((chunk: string | Uint8Array) => {
25
- chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf-8"));
26
- return true;
27
- }) as typeof process.stderr.write;
28
- try {
29
- fn();
30
- } finally {
31
- process.stderr.write = original;
32
- }
33
- return chunks;
34
- }
35
-
36
- /**
37
- * Capture stderr and return the parsed JSON objects (one per log line).
38
- */
39
- function captureJson(fn: () => void): Record<string, unknown>[] {
40
- const chunks = captureStderr(fn);
41
- return chunks
42
- .flatMap((chunk) => chunk.split("\n"))
43
- .map((line) => line.trim())
44
- .filter((line) => line.length > 0)
45
- .map((line) => JSON.parse(line));
46
- }
47
-
48
- // ── Logger: stderr output ─────────────────────────────────────────
49
-
50
- describe("Logger — stderr output", () => {
51
- it("writes JSON log entries to stderr (never stdout)", () => {
52
- const log = new Logger();
53
- const entries = captureJson(() => {
54
- log.info("hello from test", { component: "unit-test" });
55
- });
56
-
57
- assert.equal(entries.length, 1);
58
- const entry = entries[0];
59
-
60
- assert.equal(entry.level, "info");
61
- assert.equal(entry.message, "hello from test");
62
- assert.equal(entry.component, "unit-test");
63
- assert.equal(typeof entry.timestamp, "string");
64
- // timestamp must be a valid ISO date
65
- assert.ok(!Number.isNaN(Date.parse(entry.timestamp as string)));
66
- });
67
-
68
- it("supports warn and error levels", () => {
69
- const log = new Logger();
70
- const entries = captureJson(() => {
71
- log.warn("careful");
72
- log.error("broken");
73
- });
74
-
75
- assert.equal(entries.length, 2);
76
- assert.equal(entries[0].level, "warn");
77
- assert.equal(entries[0].message, "careful");
78
- assert.equal(entries[1].level, "error");
79
- assert.equal(entries[1].message, "broken");
80
- });
81
-
82
- it("does not write anything to stdout", () => {
83
- const originalStdout = process.stdout.write;
84
- let stdoutWritten = false;
85
- process.stdout.write = (() => {
86
- stdoutWritten = true;
87
- return true;
88
- }) as typeof process.stdout.write;
89
-
90
- try {
91
- const log = new Logger();
92
- log.info("should not touch stdout");
93
- log.warn("nor this");
94
- log.error("nor this");
95
- } finally {
96
- process.stdout.write = originalStdout;
97
- }
98
-
99
- assert.equal(stdoutWritten, false, "Logger must never write to stdout");
100
- });
101
- });
102
-
103
- // ── Logger: audit ─────────────────────────────────────────────────
104
-
105
- describe("Logger.audit — format", () => {
106
- it("produces an audit log entry with all required fields", () => {
107
- const log = new Logger();
108
- const entries = captureJson(() => {
109
- log.audit("createTask", true, 150, undefined, "task-001");
110
- });
111
-
112
- assert.equal(entries.length, 1);
113
- const entry = entries[0];
114
-
115
- // Required core fields
116
- assert.equal(entry.level, "audit");
117
- assert.equal(typeof entry.timestamp, "string");
118
- assert.ok(!Number.isNaN(Date.parse(entry.timestamp as string)));
119
- assert.equal(typeof entry.message, "string");
120
-
121
- // Audit-specific fields
122
- assert.equal(entry.tool, "createTask");
123
- assert.equal(entry.ok, true);
124
- assert.equal(entry.duration_ms, 150);
125
- assert.equal(entry.task_id, "task-001");
126
-
127
- // error_reason should be absent when not provided
128
- assert.equal(entry.error_reason, undefined);
129
- });
130
-
131
- it("includes error_reason when the tool call failed", () => {
132
- const log = new Logger();
133
- const entries = captureJson(() => {
134
- log.audit("applyPatch", false, 3000, "command_blocked", "task-002");
135
- });
136
-
137
- assert.equal(entries.length, 1);
138
- const entry = entries[0];
139
-
140
- assert.equal(entry.ok, false);
141
- assert.equal(entry.error_reason, "command_blocked");
142
- assert.equal(entry.task_id, "task-002");
143
- });
144
-
145
- it("omits task_id when not provided", () => {
146
- const log = new Logger();
147
- const entries = captureJson(() => {
148
- log.audit("healthCheck", true, 5);
149
- });
150
-
151
- assert.equal(entries.length, 1);
152
- const entry = entries[0];
153
- assert.equal(entry.task_id, undefined);
154
- assert.equal(entry.error_reason, undefined);
155
- });
156
- });
157
-
158
- // ── Logger: verbose mode ──────────────────────────────────────────
159
-
160
- describe("Logger — verbose mode (PATCHWARDEN_VERBOSE_LOG)", () => {
161
- let savedEnv: string | undefined;
162
-
163
- beforeEach(() => {
164
- savedEnv = process.env.PATCHWARDEN_VERBOSE_LOG;
165
- });
166
-
167
- afterEach(() => {
168
- if (savedEnv === undefined) {
169
- delete process.env.PATCHWARDEN_VERBOSE_LOG;
170
- } else {
171
- process.env.PATCHWARDEN_VERBOSE_LOG = savedEnv;
172
- }
173
- });
174
-
175
- it("does not log args by default (verbose mode off)", () => {
176
- delete process.env.PATCHWARDEN_VERBOSE_LOG;
177
-
178
- const log = new Logger();
179
- const fakeToken = "sk_" + "FAKE_TOKEN_VALUE_123456";
180
- const sensitiveArgs = {
181
- prompt: "fix the bug",
182
- token: fakeToken,
183
- };
184
-
185
- const entries = captureJson(() => {
186
- log.audit("createTask", true, 100, undefined, "task-003", sensitiveArgs);
187
- });
188
-
189
- assert.equal(entries.length, 1);
190
- const entry = entries[0];
191
-
192
- // args must NOT be present when verbose mode is off
193
- assert.equal(entry.args, undefined);
194
- // The raw token must not appear anywhere in the serialized entry
195
- const raw = JSON.stringify(entry);
196
- assert.ok(!raw.includes(fakeToken), "raw token must not leak into log output");
197
- });
198
-
199
- it("logs sanitized args when PATCHWARDEN_VERBOSE_LOG=true", () => {
200
- process.env.PATCHWARDEN_VERBOSE_LOG = "true";
201
-
202
- const log = new Logger();
203
- const fakeToken = "sk_" + "FAKE_TOKEN_VALUE_123456";
204
- const sensitiveArgs = {
205
- prompt: "fix the bug",
206
- token: fakeToken,
207
- };
208
-
209
- const entries = captureJson(() => {
210
- log.audit("createTask", true, 100, undefined, "task-004", sensitiveArgs);
211
- });
212
-
213
- assert.equal(entries.length, 1);
214
- const entry = entries[0];
215
-
216
- // args must be present and redacted
217
- assert.ok(entry.args !== undefined, "args should be logged in verbose mode");
218
- const argsStr = String(entry.args);
219
- assert.ok(argsStr.includes("[REDACTED TOKEN]"), "sensitive token must be redacted");
220
- assert.ok(!argsStr.includes(fakeToken), "raw token must not appear in args");
221
- // non-sensitive content is preserved
222
- assert.ok(argsStr.includes("fix the bug"), "non-sensitive args content is preserved");
223
- });
224
-
225
- it("does not log args when PATCHWARDEN_VERBOSE_LOG is set but not 'true'", () => {
226
- process.env.PATCHWARDEN_VERBOSE_LOG = "false";
227
-
228
- const log = new Logger();
229
- const entries = captureJson(() => {
230
- log.audit("createTask", true, 100, undefined, "task-005", { prompt: "hello" });
231
- });
232
-
233
- assert.equal(entries.length, 1);
234
- assert.equal(entries[0].args, undefined);
235
- });
236
- });
237
-
238
- // ── logUnhandledError ─────────────────────────────────────────────
239
-
240
- describe("logUnhandledError", () => {
241
- it("produces structured JSON output for Error instances", () => {
242
- const error = new Error("something went wrong");
243
- error.name = "CustomError";
244
-
245
- const entries = captureJson(() => {
246
- logUnhandledError(error);
247
- });
248
-
249
- assert.equal(entries.length, 1);
250
- const entry = entries[0];
251
-
252
- assert.equal(entry.level, "error");
253
- assert.equal(entry.message, "unhandled_error");
254
- assert.equal(entry.error, "something went wrong");
255
- assert.equal(entry.error_name, "CustomError");
256
- assert.equal(typeof entry.timestamp, "string");
257
- assert.ok(!Number.isNaN(Date.parse(entry.timestamp as string)));
258
- assert.equal(typeof entry.stack, "string");
259
- assert.ok((entry.stack as string).includes("something went wrong"));
260
- });
261
-
262
- it("handles non-Error values (string)", () => {
263
- const entries = captureJson(() => {
264
- logUnhandledError("a plain string rejection");
265
- });
266
-
267
- assert.equal(entries.length, 1);
268
- const entry = entries[0];
269
-
270
- assert.equal(entry.level, "error");
271
- assert.equal(entry.error, "a plain string rejection");
272
- assert.equal(entry.error_name, "string");
273
- assert.equal(entry.stack, undefined);
274
- });
275
-
276
- it("handles non-Error values (object)", () => {
277
- const entries = captureJson(() => {
278
- logUnhandledError({ code: 42, detail: "weird" });
279
- });
280
-
281
- assert.equal(entries.length, 1);
282
- const entry = entries[0];
283
-
284
- assert.equal(entry.level, "error");
285
- assert.ok(String(entry.error).includes("42"));
286
- assert.equal(entry.error_name, "object");
287
- });
288
- });
289
-
290
- // ── installGlobalHandlers ─────────────────────────────────────────
291
-
292
- describe("installGlobalHandlers", () => {
293
- it("does not throw when called", () => {
294
- // Save existing listeners so we can restore them afterwards and avoid
295
- // the fatal `uncaughtException` handler interfering with the test
296
- // runner.
297
- const origUnhandled = process.listeners("unhandledRejection");
298
- const origUncaught = process.listeners("uncaughtException");
299
-
300
- process.removeAllListeners("unhandledRejection");
301
- process.removeAllListeners("uncaughtException");
302
-
303
- try {
304
- assert.doesNotThrow(() => installGlobalHandlers());
305
-
306
- // Verify the handlers were actually registered
307
- assert.ok(process.listenerCount("unhandledRejection") >= 1);
308
- assert.ok(process.listenerCount("uncaughtException") >= 1);
309
- } finally {
310
- // Restore original listeners
311
- process.removeAllListeners("unhandledRejection");
312
- process.removeAllListeners("uncaughtException");
313
- for (const fn of origUnhandled) process.on("unhandledRejection", fn);
314
- for (const fn of origUncaught) process.on("uncaughtException", fn);
315
- }
316
- });
317
-
318
- it("can be called multiple times without throwing", () => {
319
- const origUnhandled = process.listeners("unhandledRejection");
320
- const origUncaught = process.listeners("uncaughtException");
321
-
322
- process.removeAllListeners("unhandledRejection");
323
- process.removeAllListeners("uncaughtException");
324
-
325
- try {
326
- assert.doesNotThrow(() => {
327
- installGlobalHandlers();
328
- installGlobalHandlers();
329
- });
330
- } finally {
331
- process.removeAllListeners("unhandledRejection");
332
- process.removeAllListeners("uncaughtException");
333
- for (const fn of origUnhandled) process.on("unhandledRejection", fn);
334
- for (const fn of origUncaught) process.on("uncaughtException", fn);
335
- }
336
- });
337
- });
338
-
339
- // ── computeArgumentsDigest ───────────────────────────────────────
340
-
341
- describe("computeArgumentsDigest", () => {
342
- it("returns the same digest for identical arguments (stability)", () => {
343
- const args = { b: 2, a: 1, nested: { y: "yes", x: [1, 2] } };
344
- const d1 = computeArgumentsDigest(args);
345
- const d2 = computeArgumentsDigest(args);
346
- assert.equal(d1, d2);
347
- });
348
-
349
- it("returns the same digest regardless of key order (stable serialization)", () => {
350
- const d1 = computeArgumentsDigest({ a: 1, b: 2 });
351
- const d2 = computeArgumentsDigest({ b: 2, a: 1 });
352
- assert.equal(d1, d2);
353
- });
354
-
355
- it("returns different digests for different arguments", () => {
356
- const d1 = computeArgumentsDigest({ a: 1 });
357
- const d2 = computeArgumentsDigest({ a: 2 });
358
- assert.notEqual(d1, d2);
359
- });
360
-
361
- it("returns a 'sha256:' prefixed digest", () => {
362
- const digest = computeArgumentsDigest({ foo: "bar" });
363
- assert.ok(
364
- digest.startsWith("sha256:"),
365
- `digest should start with "sha256:", got: ${digest}`,
366
- );
367
- // full format: sha256: + 64 lowercase hex chars
368
- assert.match(digest, /^sha256:[a-f0-9]{64}$/);
369
- });
370
-
371
- it("does not leak raw argument values into the digest string", () => {
372
- const fakeToken = "tok-value";
373
- const sensitiveArgs = { token: fakeToken, prompt: "hello-world" };
374
- const digest = computeArgumentsDigest(sensitiveArgs);
375
- assert.ok(
376
- !digest.includes(fakeToken),
377
- "digest must not leak raw token value",
378
- );
379
- assert.ok(
380
- !digest.includes("hello-world"),
381
- "digest must not leak raw prompt value",
382
- );
383
- });
384
- });
385
-
386
- // ── logToolInvocation ────────────────────────────────────────────
387
-
388
- describe("logToolInvocation", () => {
389
- let tempDir: string;
390
-
391
- beforeEach(() => {
392
- tempDir = mkdtempSync(join(tmpdir(), "pw-invlog-"));
393
- });
394
-
395
- afterEach(() => {
396
- rmSync(tempDir, { recursive: true, force: true });
397
- });
398
-
399
- function sampleEntry(overrides: Partial<InvocationLogEntry> = {}): InvocationLogEntry {
400
- return {
401
- timestamp: new Date().toISOString(),
402
- toolName: "read_workspace_file",
403
- discoveryToken: "tok",
404
- risk: "workspace_read_sensitive",
405
- profile: "full",
406
- arguments_digest: "sha256:" + "0".repeat(64),
407
- result: "ok",
408
- duration_ms: 42,
409
- ...overrides,
410
- };
411
- }
412
-
413
- it("writes an entry to invocation.log inside the provided logsDir", () => {
414
- const logsDir = join(tempDir, ".patchwarden", "logs");
415
- const entry = sampleEntry();
416
- logToolInvocation(entry, { logsDir });
417
-
418
- const logPath = join(logsDir, "invocation.log");
419
- assert.ok(existsSync(logPath), "invocation.log should be created");
420
-
421
- const content = readFileSync(logPath, "utf-8");
422
- const lines = content.trim().split("\n");
423
- assert.equal(lines.length, 1, "exactly one JSON line should be written");
424
-
425
- const parsed = JSON.parse(lines[0]);
426
- assert.equal(parsed.toolName, "read_workspace_file");
427
- assert.equal(parsed.discoveryToken, "tok");
428
- assert.equal(parsed.risk, "workspace_read_sensitive");
429
- assert.equal(parsed.profile, "full");
430
- assert.equal(parsed.result, "ok");
431
- assert.equal(parsed.duration_ms, 42);
432
- assert.match(parsed.arguments_digest, /^sha256:[a-f0-9]{64}$/);
433
- assert.ok(!Number.isNaN(Date.parse(parsed.timestamp)));
434
- });
435
-
436
- it("appends multiple entries as JSON Lines", () => {
437
- const logsDir = join(tempDir, "logs");
438
- logToolInvocation(sampleEntry({ duration_ms: 1 }), { logsDir });
439
- logToolInvocation(
440
- sampleEntry({ duration_ms: 2, result: "error", error_code: "blocked" }),
441
- { logsDir },
442
- );
443
-
444
- const content = readFileSync(join(logsDir, "invocation.log"), "utf-8");
445
- const lines = content.trim().split("\n");
446
- assert.equal(lines.length, 2);
447
-
448
- const first = JSON.parse(lines[0]);
449
- const second = JSON.parse(lines[1]);
450
- assert.equal(first.duration_ms, 1);
451
- assert.equal(first.result, "ok");
452
- assert.equal(second.duration_ms, 2);
453
- assert.equal(second.result, "error");
454
- assert.equal(second.error_code, "blocked");
455
- });
456
-
457
- it("includes allowedScope and error_code when provided", () => {
458
- const logsDir = join(tempDir, "logs");
459
- logToolInvocation(
460
- sampleEntry({
461
- allowedScope: ["src/", "docs/"],
462
- result: "error",
463
- error_code: "scope_violation",
464
- }),
465
- { logsDir },
466
- );
467
-
468
- const parsed = JSON.parse(
469
- readFileSync(join(logsDir, "invocation.log"), "utf-8").trim(),
470
- );
471
- assert.deepEqual(parsed.allowedScope, ["src/", "docs/"]);
472
- assert.equal(parsed.error_code, "scope_violation");
473
- });
474
-
475
- it("omits error_code and allowedScope when not provided", () => {
476
- const logsDir = join(tempDir, "logs");
477
- logToolInvocation(sampleEntry(), { logsDir });
478
-
479
- const parsed = JSON.parse(
480
- readFileSync(join(logsDir, "invocation.log"), "utf-8").trim(),
481
- );
482
- assert.equal(parsed.error_code, undefined);
483
- assert.equal(parsed.allowedScope, undefined);
484
- });
485
-
486
- it("does not throw when the log directory cannot be created", () => {
487
- // Create a file, then attempt to use a path beneath it as logsDir.
488
- // mkdirSync will fail with ENOTDIR because the parent is a file.
489
- const blockingFile = join(tempDir, "blocker");
490
- writeFileSync(blockingFile, "x", "utf-8");
491
- const impossibleLogsDir = join(blockingFile, "subdir");
492
-
493
- assert.doesNotThrow(() => {
494
- logToolInvocation(sampleEntry(), { logsDir: impossibleLogsDir });
495
- });
496
- });
497
- });
@@ -1,180 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { mkdtempSync, rmSync, mkdirSync, writeFileSync, symlinkSync } from "node:fs";
4
- import { join, resolve, sep } from "node:path";
5
- import { tmpdir } from "node:os";
6
- import { guardPath, guardWorkspacePath } from "../../security/pathGuard.js";
7
- import { PatchWardenError } from "../../errors.js";
8
-
9
- describe("guardPath", () => {
10
- let tempDir: string;
11
-
12
- beforeEach(() => {
13
- tempDir = mkdtempSync(join(tmpdir(), "pw-pathguard-"));
14
- });
15
-
16
- afterEach(() => {
17
- rmSync(tempDir, { recursive: true, force: true });
18
- });
19
-
20
- it("allows paths inside workspace", () => {
21
- const result = guardPath("src/main.ts", tempDir);
22
- assert.ok(result.startsWith(tempDir));
23
- assert.ok(result.includes("src"));
24
- assert.ok(result.includes("main.ts"));
25
- });
26
-
27
- it("allows root workspace path", () => {
28
- const result = guardPath(".", tempDir);
29
- assert.equal(result, resolve(tempDir));
30
- });
31
-
32
- it("allows empty path as workspace root", () => {
33
- const result = guardPath("", tempDir);
34
- assert.equal(result, resolve(tempDir));
35
- });
36
-
37
- it("rejects path traversal with ..", () => {
38
- assert.throws(
39
- () => guardPath("../../../etc/passwd", tempDir),
40
- PatchWardenError
41
- );
42
- });
43
-
44
- it("rejects path traversal nested with ..", () => {
45
- assert.throws(
46
- () => guardPath("src/../../../etc/passwd", tempDir),
47
- PatchWardenError
48
- );
49
- });
50
-
51
- it("rejects absolute path outside workspace", () => {
52
- const outside = process.platform === "win32" ? "C:\\Windows\\System32" : "/etc";
53
- assert.throws(
54
- () => guardPath(outside, tempDir),
55
- PatchWardenError
56
- );
57
- });
58
-
59
- it("allows absolute path inside workspace", () => {
60
- const insidePath = join(tempDir, "src", "main.ts");
61
- mkdirSync(join(tempDir, "src"), { recursive: true });
62
- writeFileSync(insidePath, "test", "utf-8");
63
- const result = guardPath(insidePath, tempDir);
64
- assert.equal(result, resolve(insidePath));
65
- });
66
-
67
- it("enforces allowedPrefix", () => {
68
- mkdirSync(join(tempDir, "allowed"), { recursive: true });
69
- mkdirSync(join(tempDir, "forbidden"), { recursive: true });
70
- // Path inside allowed prefix works
71
- const okResult = guardPath("allowed/file.ts", tempDir, "allowed");
72
- assert.ok(okResult.includes("allowed"));
73
- // Path outside allowed prefix fails
74
- assert.throws(
75
- () => guardPath("forbidden/file.ts", tempDir, "allowed"),
76
- PatchWardenError
77
- );
78
- });
79
-
80
- it("handles Windows mixed separators", () => {
81
- const mixedPath = "src\\subdir/file.ts";
82
- const result = guardPath(mixedPath, tempDir);
83
- assert.ok(result.startsWith(tempDir));
84
- });
85
-
86
- it("rejects symlink escape", { skip: process.platform === "win32" ? "Windows symlink permissions unstable" : undefined }, () => {
87
- const target = mkdtempSync(join(tmpdir(), "pw-symlink-target-"));
88
- try {
89
- const linkPath = join(tempDir, "escape-link");
90
- symlinkSync(target, linkPath);
91
- assert.throws(
92
- () => guardPath("escape-link/secret.txt", tempDir),
93
- PatchWardenError
94
- );
95
- } finally {
96
- rmSync(target, { recursive: true, force: true });
97
- }
98
- });
99
-
100
- it("rejects Windows junction escape", { skip: process.platform !== "win32" ? "Windows-only test" : undefined }, () => {
101
- const target = mkdtempSync(join(tmpdir(), "pw-junction-target-"));
102
- try {
103
- const linkPath = join(tempDir, "escape-junction");
104
- symlinkSync(target, linkPath, "junction");
105
- assert.throws(
106
- () => guardPath("escape-junction/secret.txt", tempDir),
107
- PatchWardenError
108
- );
109
- } finally {
110
- rmSync(target, { recursive: true, force: true });
111
- }
112
- });
113
-
114
- it("rejects intermediate symlink escape (TOCTOU defense)", { skip: process.platform === "win32" ? "Windows symlink permissions unstable" : undefined }, () => {
115
- const target = mkdtempSync(join(tmpdir(), "pw-toctou-target-"));
116
- try {
117
- const linkPath = join(tempDir, "escape-link");
118
- symlinkSync(target, linkPath);
119
- mkdirSync(join(target, "subdir"), { recursive: true });
120
- writeFileSync(join(target, "subdir", "secret.txt"), "secret", "utf-8");
121
- assert.throws(
122
- () => guardPath("escape-link/subdir/secret.txt", tempDir),
123
- PatchWardenError
124
- );
125
- } finally {
126
- rmSync(target, { recursive: true, force: true });
127
- }
128
- });
129
- });
130
-
131
- describe("guardWorkspacePath", () => {
132
- let tempDir: string;
133
-
134
- beforeEach(() => {
135
- tempDir = mkdtempSync(join(tmpdir(), "pw-wspath-"));
136
- });
137
-
138
- afterEach(() => {
139
- rmSync(tempDir, { recursive: true, force: true });
140
- });
141
-
142
- it("allows relative path inside workspace", () => {
143
- const result = guardWorkspacePath("my-repo", tempDir);
144
- assert.equal(result, resolve(tempDir, "my-repo"));
145
- });
146
-
147
- it("allows absolute path inside workspace", () => {
148
- const absPath = join(tempDir, "my-repo");
149
- const result = guardWorkspacePath(absPath, tempDir);
150
- assert.equal(result, resolve(absPath));
151
- });
152
-
153
- it("rejects path outside workspace", () => {
154
- assert.throws(
155
- () => guardWorkspacePath("../../../etc", tempDir),
156
- PatchWardenError
157
- );
158
- });
159
-
160
- it("rejects different drive letter on Windows", { skip: process.platform !== "win32" ? "Windows-only test" : undefined }, () => {
161
- // If workspace is on C:, a D: path should be rejected
162
- const wsDrive = tempDir.match(/^([A-Za-z]):/);
163
- if (!wsDrive) return;
164
- const otherDrive = wsDrive[1].toLowerCase() === "c" ? "D" : "C";
165
- assert.throws(
166
- () => guardWorkspacePath(`${otherDrive}:\\some\\repo`, tempDir),
167
- (err: unknown) => err instanceof PatchWardenError && err.reason === "workspace_path_escape"
168
- );
169
- });
170
-
171
- it("defaults to workspace root for empty input", () => {
172
- const result = guardWorkspacePath("", tempDir);
173
- assert.equal(result, resolve(tempDir));
174
- });
175
-
176
- it("defaults to workspace root for dot input", () => {
177
- const result = guardWorkspacePath(".", tempDir);
178
- assert.equal(result, resolve(tempDir));
179
- });
180
- });