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
@@ -20,31 +20,32 @@ import {
20
20
  PAGE_ALIASES,
21
21
  port,
22
22
  readBody,
23
- resolveTailParam,
24
23
  sendJson,
25
24
  } from "./shared.js";
26
25
  import { recordEvent, removeStatusFile, writeStatusFile } from "./runtime.js";
27
- import { checkControlToken } from "./middleware/auth.js";
26
+ import { checkControlToken, isTrustedControlHost } from "./middleware/auth.js";
28
27
  import { serveFavicon, serveStatic } from "./middleware/static.js";
29
- import { handleTasks, handleStaleTasks, handleTaskDetail, handleTaskSafeResult, handleTaskSafeAudit, handleTaskSafeTestSummary, handleTaskSafeDiffSummary } from "./routes/tasks.js";
30
- import { handleReconcile, handleTaskAudit, handleOpenTaskFolder, handleHideStale } from "./routes/taskActions.js";
31
- import { handleDirectSessions, handleDirectSessionDetail, handleDirectSessionSafeSummary, handleDirectSessionFinalize, handleDirectSessionAudit, handleDirectSessionHide } from "./routes/sessions.js";
32
- import { handleLineages, handleLineageDetail } from "./routes/lineage.js";
33
- import { handleEvidencePacks, handleEvidencePackDetail, handleEvidencePackExport } from "./routes/evidence.js";
34
- import { handleProjectPolicy, handleReleaseStatus } from "./routes/policy.js";
35
- import { handleWorkspace, handleWorkspaceRepos, handleWorkspaceRepoStatus } from "./routes/workspace.js";
36
- import { handleManageAction, handleOpenLogsFolder } from "./routes/process.js";
37
- import { handleStatus, handleControlCenterStatus, handleEvents, handleTunnelUiUrl, handleDiagnostics } from "./routes/status.js";
38
- import { handleLogs, handleAudit, handleWarnings, type LogCategory } from "./routes/audit.js";
28
+ import { buildRoutes } from "./routeTable.js";
39
29
 
40
30
  // ── Request router ────────────────────────────────────────────────
41
31
 
42
32
  async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
43
- const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || host}`);
33
+ res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
34
+ res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
35
+ res.setHeader("Referrer-Policy", "no-referrer");
36
+ res.setHeader("X-Content-Type-Options", "nosniff");
37
+ res.setHeader("X-Frame-Options", "DENY");
38
+
39
+ if (!isTrustedControlHost(req, port)) {
40
+ sendJson(res, 421, { error: "Untrusted Host header" });
41
+ return;
42
+ }
43
+
44
+ const parsedUrl = new URL(req.url || "/", `http://${host}:${port}`);
44
45
  const pathname = parsedUrl.pathname;
45
46
  const method = (req.method || "GET").toUpperCase();
46
47
 
47
- // Static routes
48
+ // ── Static resource priority branches (not in routeTable.ts) ──
48
49
  if (method === "GET" && pathname === "/") {
49
50
  serveStatic(res, "pages/dashboard.html");
50
51
  return;
@@ -65,6 +66,12 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
65
66
  if (
66
67
  method === "GET" &&
67
68
  (pathname === "/colors_and_type.css" ||
69
+ pathname === "/desktop.css" ||
70
+ pathname === "/desktop-bridge.js" ||
71
+ pathname === "/log-parser.js" ||
72
+ pathname === "/i18n.js" ||
73
+ pathname === "/getting-started.js" ||
74
+ pathname === "/settings.js" ||
68
75
  pathname.startsWith("/pages/") ||
69
76
  pathname.startsWith("/partials/") ||
70
77
  pathname.startsWith("/vendor/"))
@@ -73,339 +80,27 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
73
80
  return;
74
81
  }
75
82
 
76
- // GET API routes
77
- if (method === "GET" && pathname === "/api/status") {
78
- await handleStatus(res);
79
- return;
80
- }
81
- if (method === "GET" && pathname === "/api/tasks") {
82
- handleTasks(res, {
83
- repo_path: parsedUrl.searchParams.get("repo_path") || undefined,
84
- status: parsedUrl.searchParams.get("status") || undefined,
85
- acceptance_status: parsedUrl.searchParams.get("acceptance_status") || undefined,
86
- agent: parsedUrl.searchParams.get("agent") || undefined,
87
- warning_type: parsedUrl.searchParams.get("warning_type") || undefined,
88
- });
89
- return;
90
- }
91
- if (method === "GET" && pathname === "/api/tasks/stale") {
92
- handleStaleTasks(res);
93
- return;
94
- }
95
- if (method === "GET" && pathname === "/api/lineages") {
96
- handleLineages(res);
97
- return;
98
- }
99
- const lineageMatch = pathname.match(/^\/api\/lineages\/([^/]+)$/);
100
- if (method === "GET" && lineageMatch) {
101
- let lineageId: string;
102
- try {
103
- lineageId = decodeURIComponent(lineageMatch[1]);
104
- } catch {
105
- lineageId = lineageMatch[1];
106
- }
107
- handleLineageDetail(res, lineageId);
108
- return;
109
- }
110
- if (method === "GET" && pathname === "/api/project-policy") {
111
- handleProjectPolicy(res, parsedUrl.searchParams.get("repo_path") || ".");
112
- return;
113
- }
114
- if (method === "GET" && pathname === "/api/release/status") {
115
- handleReleaseStatus(res, parsedUrl.searchParams.get("repo_path") || ".");
116
- return;
117
- }
118
- if (method === "GET" && pathname === "/api/evidence-packs") {
119
- handleEvidencePacks(res);
120
- return;
121
- }
122
- const evidencePackMatch = pathname.match(/^\/api\/evidence-packs\/([^/]+)$/);
123
- if (method === "GET" && evidencePackMatch) {
124
- let lineageId: string;
125
- try {
126
- lineageId = decodeURIComponent(evidencePackMatch[1]);
127
- } catch {
128
- lineageId = evidencePackMatch[1];
129
- }
130
- handleEvidencePackDetail(res, lineageId);
131
- return;
132
- }
133
- const taskMatch = pathname.match(/^\/api\/tasks\/([^/]+)$/);
134
- if (method === "GET" && taskMatch) {
135
- let taskId: string;
136
- try {
137
- taskId = decodeURIComponent(taskMatch[1]);
138
- } catch {
139
- taskId = taskMatch[1];
140
- }
141
- if (
142
- taskId === "." ||
143
- taskId === ".." ||
144
- taskId.includes("/") ||
145
- taskId.includes("\\") ||
146
- taskId.includes("\0")
147
- ) {
148
- sendJson(res, 400, { error: "Invalid task id" });
149
- return;
150
- }
151
- handleTaskDetail(res, taskId);
152
- return;
153
- }
154
- // Safe, bounded views for task artifacts (no full stdout/stderr/diff).
155
- const taskSafeResultMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-result$/);
156
- if (method === "GET" && taskSafeResultMatch) {
157
- let taskId: string;
158
- try {
159
- taskId = decodeURIComponent(taskSafeResultMatch[1]);
160
- } catch {
161
- taskId = taskSafeResultMatch[1];
162
- }
163
- handleTaskSafeResult(res, taskId);
164
- return;
165
- }
166
- const taskSafeAuditMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-audit$/);
167
- if (method === "GET" && taskSafeAuditMatch) {
168
- let taskId: string;
169
- try {
170
- taskId = decodeURIComponent(taskSafeAuditMatch[1]);
171
- } catch {
172
- taskId = taskSafeAuditMatch[1];
173
- }
174
- handleTaskSafeAudit(res, taskId);
175
- return;
176
- }
177
- const taskSafeTestSummaryMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-test-summary$/);
178
- if (method === "GET" && taskSafeTestSummaryMatch) {
179
- let taskId: string;
180
- try {
181
- taskId = decodeURIComponent(taskSafeTestSummaryMatch[1]);
182
- } catch {
183
- taskId = taskSafeTestSummaryMatch[1];
184
- }
185
- handleTaskSafeTestSummary(res, taskId);
186
- return;
187
- }
188
- const taskSafeDiffSummaryMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/safe-diff-summary$/);
189
- if (method === "GET" && taskSafeDiffSummaryMatch) {
190
- let taskId: string;
191
- try {
192
- taskId = decodeURIComponent(taskSafeDiffSummaryMatch[1]);
193
- } catch {
194
- taskId = taskSafeDiffSummaryMatch[1];
195
- }
196
- handleTaskSafeDiffSummary(res, taskId);
197
- return;
198
- }
199
- // Logs: /api/logs/<category>?tail=<100|300|1000>
200
- const logsMatch = pathname.match(/^\/api\/logs\/([a-z-]+)$/);
201
- if (method === "GET" && logsMatch) {
202
- const rawCat = logsMatch[1];
203
- const category = rawCat === "core" || rawCat === "direct" || rawCat === "watcher" || rawCat === "control-center"
204
- ? (rawCat as LogCategory)
205
- : null;
206
- if (!category) {
207
- sendJson(res, 404, { error: "Unknown log category" });
208
- return;
209
- }
210
- const tail = resolveTailParam(parsedUrl.searchParams.get("tail"));
211
- handleLogs(res, category, tail);
212
- return;
213
- }
214
- if (method === "GET" && pathname === "/api/workspace") {
215
- handleWorkspace(res);
216
- return;
217
- }
218
- if (method === "GET" && pathname === "/api/workspace/repos") {
219
- handleWorkspaceRepos(res);
220
- return;
221
- }
222
- // On-demand git status for a single repo (path-traversal safe).
223
- // The repo segment is URL-decoded; traversal is rejected by guardWorkspacePath.
224
- const workspaceRepoMatch = pathname.match(/^\/api\/workspace\/([^/]+(?:\/[^/]+)*)\/status$/);
225
- if (method === "GET" && workspaceRepoMatch) {
226
- let repoParam: string;
227
- try {
228
- repoParam = decodeURIComponent(workspaceRepoMatch[1]);
229
- } catch {
230
- sendJson(res, 400, { error: "Invalid repo path encoding" });
231
- return;
232
- }
233
- handleWorkspaceRepoStatus(res, repoParam);
234
- return;
235
- }
236
- if (method === "GET" && pathname === "/api/direct-sessions") {
237
- handleDirectSessions(res);
238
- return;
239
- }
240
- const directSessionSummaryMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/summary$/);
241
- if (method === "GET" && directSessionSummaryMatch) {
242
- let sessionId: string;
243
- try {
244
- sessionId = decodeURIComponent(directSessionSummaryMatch[1]);
245
- } catch {
246
- sessionId = directSessionSummaryMatch[1];
247
- }
248
- handleDirectSessionSafeSummary(res, sessionId);
249
- return;
250
- }
251
- const directSessionMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)$/);
252
- if (method === "GET" && directSessionMatch) {
253
- let sessionId: string;
254
- try {
255
- sessionId = decodeURIComponent(directSessionMatch[1]);
256
- } catch {
257
- sessionId = directSessionMatch[1];
258
- }
259
- handleDirectSessionDetail(res, sessionId);
260
- return;
261
- }
262
- if (method === "GET" && pathname === "/api/audit") {
263
- handleAudit(res);
264
- return;
265
- }
266
- if (method === "GET" && pathname === "/api/warnings") {
267
- handleWarnings(res);
268
- return;
269
- }
270
- if (method === "GET" && pathname === "/api/diagnostics") {
271
- handleDiagnostics(res);
272
- return;
273
- }
274
- if (method === "GET" && pathname === "/api/tunnel-ui-url") {
275
- handleTunnelUiUrl(res);
276
- return;
277
- }
278
- if (method === "GET" && pathname === "/api/events") {
279
- const limitParam = parsedUrl.searchParams.get("limit");
280
- let limit = 100;
281
- if (limitParam !== null) {
282
- const n = parseInt(limitParam, 10);
283
- if (Number.isFinite(n) && n > 0 && n <= 1000) limit = n;
284
- }
285
- handleEvents(res, limit);
286
- return;
287
- }
288
- if (method === "GET" && pathname === "/api/control-center-status") {
289
- handleControlCenterStatus(res);
290
- return;
291
- }
292
-
293
- // POST API routes (all require control token)
83
+ // ── POST routes: drain body + verify control token before dispatch ──
84
+ // All POST routes in the table declare `requiresToken: true`; the body is
85
+ // drained up-front so HTTP keep-alive connections stay clean even when the
86
+ // route is unknown (preserving the original handleRequest behavior).
294
87
  if (method === "POST") {
295
- await readBody(req); // drain optional body
88
+ await readBody(req);
296
89
  if (!checkControlToken(req)) {
297
90
  sendJson(res, 403, { error: "Missing or invalid control token" });
298
91
  return;
299
92
  }
300
- if (pathname === "/api/start-all") return handleManageAction(res, "start", "all");
301
- if (pathname === "/api/stop-all") return handleManageAction(res, "stop", "all");
302
- if (pathname === "/api/restart-all") return handleManageAction(res, "restart", "all");
303
- if (pathname === "/api/core/start") return handleManageAction(res, "start", "core");
304
- if (pathname === "/api/core/stop") return handleManageAction(res, "stop", "core");
305
- if (pathname === "/api/direct/start") return handleManageAction(res, "start", "direct");
306
- if (pathname === "/api/direct/stop") return handleManageAction(res, "stop", "direct");
307
- if (pathname === "/api/open-logs-folder") {
308
- handleOpenLogsFolder(res);
309
- return;
310
- }
311
- // POST /api/direct-sessions/:sessionId/finalize — finalize a direct session
312
- // (must be matched BEFORE any generic /api/direct-sessions/:sessionId pattern)
313
- const finalizeDirectMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/finalize$/);
314
- if (finalizeDirectMatch) {
315
- let sessionId: string;
316
- try {
317
- sessionId = decodeURIComponent(finalizeDirectMatch[1]);
318
- } catch {
319
- sessionId = finalizeDirectMatch[1];
320
- }
321
- await handleDirectSessionFinalize(res, sessionId);
322
- return;
323
- }
324
- // POST /api/direct-sessions/:sessionId/audit — audit a direct session
325
- const auditDirectMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/audit$/);
326
- if (auditDirectMatch) {
327
- let sessionId: string;
328
- try {
329
- sessionId = decodeURIComponent(auditDirectMatch[1]);
330
- } catch {
331
- sessionId = auditDirectMatch[1];
332
- }
333
- handleDirectSessionAudit(res, sessionId);
334
- return;
335
- }
336
- // POST /api/direct-sessions/:sessionId/hide — hide a direct session from the list
337
- const hideDirectMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/hide$/);
338
- if (hideDirectMatch) {
339
- let sessionId: string;
340
- try {
341
- sessionId = decodeURIComponent(hideDirectMatch[1]);
342
- } catch {
343
- sessionId = hideDirectMatch[1];
344
- }
345
- handleDirectSessionHide(res, sessionId);
346
- return;
347
- }
348
- // POST /api/tasks/:taskId/reconcile (token already validated above)
349
- const reconcileMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/reconcile$/);
350
- if (reconcileMatch) {
351
- let taskId: string;
352
- try {
353
- taskId = decodeURIComponent(reconcileMatch[1]);
354
- } catch {
355
- taskId = reconcileMatch[1];
356
- }
357
- handleReconcile(res, taskId);
358
- return;
359
- }
360
- // POST /api/tasks/:taskId/audit — run audit_task safely
361
- const auditMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/audit$/);
362
- if (auditMatch) {
363
- let taskId: string;
364
- try {
365
- taskId = decodeURIComponent(auditMatch[1]);
366
- } catch {
367
- taskId = auditMatch[1];
368
- }
369
- handleTaskAudit(res, taskId);
370
- return;
371
- }
372
- // POST /api/tasks/:taskId/open-folder — open task folder in file explorer
373
- const openFolderMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/open-folder$/);
374
- if (openFolderMatch) {
375
- let taskId: string;
376
- try {
377
- taskId = decodeURIComponent(openFolderMatch[1]);
378
- } catch {
379
- taskId = openFolderMatch[1];
380
- }
381
- handleOpenTaskFolder(res, taskId);
382
- return;
383
- }
384
- // POST /api/tasks/:taskId/hide-stale — hide a stale task from the dashboard
385
- const hideStaleMatch = pathname.match(/^\/api\/tasks\/([^/]+)\/hide-stale$/);
386
- if (hideStaleMatch) {
387
- let taskId: string;
388
- try {
389
- taskId = decodeURIComponent(hideStaleMatch[1]);
390
- } catch {
391
- taskId = hideStaleMatch[1];
392
- }
393
- handleHideStale(res, taskId);
394
- return;
395
- }
396
- // POST /api/evidence-packs/:lineageId/export — export an evidence pack for a lineage
397
- const exportPackMatch = pathname.match(/^\/api\/evidence-packs\/([^/]+)\/export$/);
398
- if (exportPackMatch) {
399
- let lineageId: string;
400
- try {
401
- lineageId = decodeURIComponent(exportPackMatch[1]);
402
- } catch {
403
- lineageId = exportPackMatch[1];
404
- }
405
- handleEvidencePackExport(res, lineageId);
406
- return;
407
- }
408
- sendJson(res, 404, { error: "Not found" });
93
+ }
94
+
95
+ // ── Declarative route table dispatch ──
96
+ // Filter by method match pattern (in declaration order, so specific
97
+ // patterns win over generic `:id`) call handler with capture-group slice.
98
+ const routes = buildRoutes(parsedUrl);
99
+ for (const route of routes) {
100
+ if (route.method !== method) continue;
101
+ const match = pathname.match(route.pattern);
102
+ if (!match) continue;
103
+ await route.handler(res, match.slice(1));
409
104
  return;
410
105
  }
411
106
 
@@ -10,11 +10,14 @@
10
10
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
11
11
  import { delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
- import { randomUUID } from "node:crypto";
13
+ import { createHash, randomUUID } from "node:crypto";
14
14
  import { homedir } from "node:os";
15
15
  import { type IncomingMessage, type ServerResponse } from "node:http";
16
16
  import { loadConfig, type PatchWardenConfig } from "../config.js";
17
17
  import { logger } from "../logging.js";
18
+ import { readTextFilePrefixSync, readTextFileTailLinesSync } from "../utils/boundedFile.js";
19
+ import { redactSensitiveContent, redactSensitiveValue } from "../security/contentRedaction.js";
20
+ import { guardPath } from "../security/pathGuard.js";
18
21
 
19
22
  // ── Paths ─────────────────────────────────────────────────────────
20
23
 
@@ -22,7 +25,22 @@ export const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."
22
25
  export const uiRoot = join(projectRoot, "ui");
23
26
  export const manageScriptPath = join(projectRoot, "scripts", "control", "manage-patchwarden.ps1");
24
27
 
28
+ function resolveActiveConfigPath(): string {
29
+ if (process.env.PATCHWARDEN_CONFIG) return resolve(process.env.PATCHWARDEN_CONFIG);
30
+ for (const name of ["patchwarden.config.json", ".patchwarden.json"]) {
31
+ const candidate = resolve(process.cwd(), name);
32
+ if (existsSync(candidate)) return candidate;
33
+ }
34
+ return resolve(process.cwd(), "patchwarden.config.json");
35
+ }
36
+
37
+ export const activeConfigPath = resolveActiveConfigPath();
38
+ export const configIdentitySha256 = createHash("sha256")
39
+ .update(process.platform === "win32" ? activeConfigPath.toLowerCase() : activeConfigPath)
40
+ .digest("hex");
41
+
25
42
  export const PAGE_ALIASES: Record<string, string> = {
43
+ "/getting-started.html": "pages/getting-started.html",
26
44
  "/dashboard.html": "pages/dashboard.html",
27
45
  "/tasks.html": "pages/tasks.html",
28
46
  "/workspace.html": "pages/workspace.html",
@@ -30,13 +48,17 @@ export const PAGE_ALIASES: Record<string, string> = {
30
48
  "/task-detail.html": "pages/task-detail.html",
31
49
  "/direct-sessions.html": "pages/direct-sessions.html",
32
50
  "/logs.html": "pages/logs.html",
51
+ "/settings.html": "pages/settings.html",
33
52
  };
34
53
 
35
54
  // ── Config (fault-tolerant bootstrap) ─────────────────────────────
36
55
 
37
56
  export function createFallbackConfig(): PatchWardenConfig {
38
57
  return {
39
- workspaceRoot: process.cwd(),
58
+ // Fault-tolerant dashboard mode must not widen a rejected workspace to the
59
+ // caller's cwd (which could be a home or drive root). The package root is
60
+ // bounded and all execution capabilities remain disabled below.
61
+ workspaceRoot: projectRoot,
40
62
  plansDir: ".patchwarden/plans",
41
63
  tasksDir: ".patchwarden/tasks",
42
64
  assessmentsDir: ".patchwarden/assessments",
@@ -99,8 +121,6 @@ export const host = "127.0.0.1";
99
121
  // not depend on the real 8080/8081 ports being free on the host.
100
122
  export const CORE_BASE_URL = process.env.PATCHWARDEN_CORE_URL || "http://127.0.0.1:8080";
101
123
  export const DIRECT_BASE_URL = process.env.PATCHWARDEN_DIRECT_URL || "http://127.0.0.1:8081";
102
- export const DEFAULT_TUNNEL_CLIENT_EXE = "D:\\ai_agent\\tunnel-client-v0.0.9--context-conduit-topaz-windows-amd64\\tunnel-client.exe";
103
-
104
124
  export const CONTROL_CENTER_FAVICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
105
125
  <rect width="64" height="64" rx="14" fill="#0a0e14"/>
106
126
  <path d="M32 52s17-8 17-27V14L32 8 15 14v11c0 19 17 27 17 27z" fill="#111820" stroke="#2dd4a8" stroke-width="4" stroke-linejoin="round"/>
@@ -219,8 +239,11 @@ export function readBody(req: IncomingMessage): Promise<unknown | null> {
219
239
  export function readJsonFileSafe<T = unknown>(filePath: string): T | null {
220
240
  if (!existsSync(filePath)) return null;
221
241
  try {
242
+ const size = statSync(filePath).size;
243
+ if (size > Math.max(1, config.maxReadFileBytes)) return null;
222
244
  const raw = readFileSync(filePath, "utf-8");
223
- return JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw) as T;
245
+ const parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
246
+ return redactSensitiveValue(parsed).value as T;
224
247
  } catch {
225
248
  return null;
226
249
  }
@@ -248,18 +271,28 @@ export function readJsonFileSafeUnder<T = unknown>(root: string, relPath: string
248
271
  export function readTextFileSafe(filePath: string): string | null {
249
272
  if (!existsSync(filePath)) return null;
250
273
  try {
251
- return readFileSync(filePath, "utf-8");
274
+ const bounded = readTextFilePrefixSync(filePath, Math.max(1, config.maxReadFileBytes));
275
+ return redactSensitiveContent(bounded.content).content;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ /** Resolve a Control Center artifact path and reject symlink/junction escapes. */
282
+ export function guardControlPath(path: string, allowedPrefix: string): string | null {
283
+ try {
284
+ return guardPath(path, config.workspaceRoot, allowedPrefix);
252
285
  } catch {
253
286
  return null;
254
287
  }
255
288
  }
256
289
 
257
290
  export function readFileTail(filePath: string, lines = 100): string {
258
- if (!existsSync(filePath)) return "";
259
- const content = readFileSync(filePath, "utf-8");
260
- const allLines = content.split(/\r?\n/);
261
- if (allLines.length > 0 && allLines[allLines.length - 1] === "") allLines.pop();
262
- return allLines.slice(-lines).join("\n");
291
+ try {
292
+ return readTextFileTailLinesSync(filePath, lines);
293
+ } catch {
294
+ return "";
295
+ }
263
296
  }
264
297
 
265
298
  export function findLatestLog(dir: string, pattern: RegExp): string | null {
@@ -288,6 +321,39 @@ export function findLatestLog(dir: string, pattern: RegExp): string | null {
288
321
  return join(dir, latestName);
289
322
  }
290
323
 
324
+ // ── Routing ──────────────────────────────────────────────────────
325
+
326
+ /**
327
+ * Declarative route descriptor consumed by `routeTable.ts` and dispatched by
328
+ * `server.ts`'s `handleRequest`. The handler receives the slice of path
329
+ * capture groups `[match[1], match[2], ...]` so wrappers can decode them via
330
+ * `decodeParam`. POST routes default to `requiresToken: true`.
331
+ */
332
+ export interface Route {
333
+ method: "GET" | "POST";
334
+ pattern: RegExp;
335
+ handler: (res: ServerResponse, params: string[]) => Promise<void> | void;
336
+ requiresToken?: boolean; // POST defaults to true
337
+ }
338
+
339
+ /**
340
+ * Safely decode a path capture group, falling back to the raw value if the
341
+ * percent-encoding is malformed. Accepts the raw `RegExpMatchArray` or the
342
+ * sliced `string[]` params passed to a Route handler — both are readable by
343
+ * numeric index. Note: under `lib: es2024+` `RegExpMatchArray` is a tuple
344
+ * `[string, ...string[]]`, so we widen the parameter to `readonly string[]` to
345
+ * accept both shapes without forcing callers to cast.
346
+ */
347
+ export function decodeParam(match: readonly string[] | RegExpMatchArray | null, index: number): string {
348
+ if (!match) return "";
349
+ const raw = match[index] || "";
350
+ try {
351
+ return decodeURIComponent(raw);
352
+ } catch {
353
+ return raw;
354
+ }
355
+ }
356
+
291
357
  // Re-export widely-used node primitives so route modules have a single import
292
358
  // surface for path/delimiter helpers that are also needed alongside the shared
293
359
  // helpers above.
@@ -0,0 +1,12 @@
1
+ const DANGEROUS_ALLOWED_TEST_COMMAND_PATTERNS = [
2
+ /(^|\s)rm\s+-rf(?:\s|$)/i,
3
+ /(^|\s)del\s+\/s(?:\s|$)/i,
4
+ /(^|\s)format(?:\.exe)?(?:\s|$)/i,
5
+ /(^|\s)shutdown(?:\.exe)?(?:\s|$)/i,
6
+ /curl\s+[^\r\n|]*\|/i,
7
+ /wget\s+[^\r\n|]*\|/i,
8
+ ];
9
+
10
+ export function isDangerousAllowedTestCommand(command: string): boolean {
11
+ return DANGEROUS_ALLOWED_TEST_COMMAND_PATTERNS.some((pattern) => pattern.test(command));
12
+ }
@@ -1,15 +1,16 @@
1
- import { writeFileSync } from "node:fs";
2
1
  import { join, resolve, relative, isAbsolute } from "node:path";
3
- import { getConfig } from "../config.js";
4
2
  import { isSensitivePath } from "../security/sensitiveGuard.js";
5
3
  import { PatchWardenError } from "../errors.js";
6
4
  import {
7
5
  readDirectSession,
8
6
  getDirectSessionDir,
7
+ updateDirectSession,
8
+ withDirectSessionMutationLock,
9
9
  type DirectSessionRecord,
10
10
  type DirectSessionVerificationRun,
11
11
  } from "./directSessionStore.js";
12
12
  import type { ChangedFile, ChangeArtifacts } from "../runner/changeCapture.js";
13
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
13
14
 
14
15
  // ── Types ──────────────────────────────────────────────────────────
15
16
 
@@ -39,7 +40,7 @@ export interface DirectSessionAuditOutput {
39
40
  // ── Main audit function ────────────────────────────────────────────
40
41
 
41
42
  export function auditDirectSession(sessionId: string): DirectSessionAuditOutput {
42
- const config = getConfig();
43
+ return withDirectSessionMutationLock(sessionId, () => {
43
44
  const session = readDirectSession(sessionId);
44
45
  const sessionDir = getDirectSessionDir(sessionId);
45
46
 
@@ -98,6 +99,15 @@ export function auditDirectSession(sessionId: string): DirectSessionAuditOutput
98
99
  reason_code: sensitiveFiles.length > 0 ? "sensitive_file_modified" : undefined,
99
100
  });
100
101
 
102
+ checks.push({
103
+ name: "sensitive_content",
104
+ result: artifacts.diff_redacted ? "fail" : "pass",
105
+ detail: artifacts.diff_redacted
106
+ ? `Credential-like content was redacted from diff evidence (${(artifacts.diff_redaction_categories ?? []).join(", ") || "sensitive_content"}).`
107
+ : "No credential-like content was detected in diff evidence.",
108
+ reason_code: artifacts.diff_redacted ? "sensitive_content_detected" : undefined,
109
+ });
110
+
101
111
  // Check 5: node_modules modification
102
112
  const nodeModulesChanges = findPathChanges(artifacts, "node_modules");
103
113
  checks.push({
@@ -304,14 +314,12 @@ export function auditDirectSession(sessionId: string): DirectSessionAuditOutput
304
314
  };
305
315
 
306
316
  // Write audit.json and audit.md
307
- writeFileSync(
308
- join(sessionDir, "audit.json"),
309
- JSON.stringify(output, null, 2),
310
- "utf-8"
311
- );
312
- writeFileSync(auditPath, formatAuditMd(output, checks, session), "utf-8");
317
+ atomicWriteJsonFileSync(join(sessionDir, "audit.json"), output);
318
+ atomicWriteFileSync(auditPath, formatAuditMd(output, checks, session));
319
+ updateDirectSession(sessionId, { audited: true });
313
320
 
314
321
  return output;
322
+ });
315
323
  }
316
324
 
317
325
  // ── Helper functions ───────────────────────────────────────────────