patchwarden 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (597) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +194 -26
  3. package/README.md +160 -22
  4. package/dist/assessments/agentAssessor.d.ts +1 -1
  5. package/dist/assessments/agentAssessor.js +24 -15
  6. package/dist/assessments/assessmentStore.d.ts +1 -1
  7. package/dist/assessments/assessmentStore.js +44 -36
  8. package/dist/assessments/confirmCli.js +5 -4
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +98 -11
  11. package/dist/control/fileManager.d.ts +2 -0
  12. package/dist/control/fileManager.js +39 -0
  13. package/dist/control/middleware/auth.d.ts +16 -0
  14. package/dist/control/middleware/auth.js +17 -0
  15. package/dist/control/middleware/static.d.ts +3 -0
  16. package/dist/control/middleware/static.js +78 -0
  17. package/dist/control/routeTable.d.ts +24 -0
  18. package/dist/control/routeTable.js +263 -0
  19. package/dist/control/routes/audit.d.ts +15 -0
  20. package/dist/control/routes/audit.js +287 -0
  21. package/dist/control/routes/evidence.d.ts +4 -0
  22. package/dist/control/routes/evidence.js +96 -0
  23. package/dist/control/routes/lineage.d.ts +3 -0
  24. package/dist/control/routes/lineage.js +71 -0
  25. package/dist/control/routes/policy.d.ts +3 -0
  26. package/dist/control/routes/policy.js +81 -0
  27. package/dist/control/routes/process.d.ts +12 -0
  28. package/dist/control/routes/process.js +487 -0
  29. package/dist/control/routes/sessions.d.ts +30 -0
  30. package/dist/control/routes/sessions.js +287 -0
  31. package/dist/control/routes/status.d.ts +51 -0
  32. package/dist/control/routes/status.js +307 -0
  33. package/dist/control/routes/taskActions.d.ts +21 -0
  34. package/dist/control/routes/taskActions.js +197 -0
  35. package/dist/control/routes/tasks.d.ts +24 -0
  36. package/dist/control/routes/tasks.js +309 -0
  37. package/dist/control/routes/workspace.d.ts +15 -0
  38. package/dist/control/routes/workspace.js +220 -0
  39. package/dist/control/runtime.d.ts +92 -0
  40. package/dist/control/runtime.js +425 -0
  41. package/dist/control/server.d.ts +13 -0
  42. package/dist/control/server.js +150 -0
  43. package/dist/control/shared.d.ts +59 -0
  44. package/dist/control/shared.js +341 -0
  45. package/dist/controlCenter.d.ts +6 -0
  46. package/dist/controlCenter.js +7 -2197
  47. package/dist/direct/directAudit.js +234 -225
  48. package/dist/direct/directGuards.d.ts +2 -1
  49. package/dist/direct/directGuards.js +72 -37
  50. package/dist/direct/directPatch.d.ts +6 -1
  51. package/dist/direct/directPatch.js +55 -8
  52. package/dist/direct/directSessionStore.d.ts +16 -6
  53. package/dist/direct/directSessionStore.js +127 -35
  54. package/dist/direct/directVerification.js +8 -16
  55. package/dist/doctor.d.ts +18 -1
  56. package/dist/doctor.js +614 -373
  57. package/dist/goal/acceptanceEngine.d.ts +1 -1
  58. package/dist/goal/goalProgress.js +75 -69
  59. package/dist/goal/goalReport.d.ts +54 -0
  60. package/dist/goal/goalReport.js +197 -0
  61. package/dist/goal/goalStatus.d.ts +8 -0
  62. package/dist/goal/goalStatus.js +10 -3
  63. package/dist/goal/goalStore.d.ts +9 -2
  64. package/dist/goal/goalStore.js +152 -45
  65. package/dist/goal/handoffExport.js +6 -6
  66. package/dist/goal/specKitImport.d.ts +63 -0
  67. package/dist/goal/specKitImport.js +221 -0
  68. package/dist/goal/subgoalSync.js +13 -10
  69. package/dist/goal/worktreeManager.d.ts +2 -0
  70. package/dist/goal/worktreeManager.js +209 -136
  71. package/dist/httpServer.js +126 -44
  72. package/dist/index.js +7 -4
  73. package/dist/logging.d.ts +7 -1
  74. package/dist/logging.js +13 -3
  75. package/dist/policy/projectPolicy.js +5 -2
  76. package/dist/release/releaseGate.d.ts +1 -1
  77. package/dist/release/releaseGate.js +78 -25
  78. package/dist/runner/agentInvocation.d.ts +14 -0
  79. package/dist/runner/agentInvocation.js +96 -4
  80. package/dist/runner/changeCapture.d.ts +14 -6
  81. package/dist/runner/changeCapture.js +287 -81
  82. package/dist/runner/cli.js +7 -6
  83. package/dist/runner/postTaskCleanup.js +60 -7
  84. package/dist/runner/processSecurity.d.ts +56 -0
  85. package/dist/runner/processSecurity.js +373 -0
  86. package/dist/runner/runTask.d.ts +1 -1
  87. package/dist/runner/runTask.js +339 -287
  88. package/dist/runner/simpleProcess.d.ts +3 -0
  89. package/dist/runner/simpleProcess.js +52 -38
  90. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  91. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  92. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  93. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  94. package/dist/runner/taskStatusStore.d.ts +11 -0
  95. package/dist/runner/taskStatusStore.js +24 -0
  96. package/dist/runner/watch.d.ts +14 -1
  97. package/dist/runner/watch.js +265 -54
  98. package/dist/security/contentRedaction.d.ts +6 -0
  99. package/dist/security/contentRedaction.js +24 -2
  100. package/dist/security/discoveryTokenStore.d.ts +3 -2
  101. package/dist/security/discoveryTokenStore.js +17 -2
  102. package/dist/security/loopbackHost.d.ts +2 -0
  103. package/dist/security/loopbackHost.js +16 -0
  104. package/dist/security/pathGuard.js +32 -17
  105. package/dist/security/planGuard.js +2 -1
  106. package/dist/security/runtimeGuard.js +9 -6
  107. package/dist/security/secretComparison.d.ts +3 -0
  108. package/dist/security/secretComparison.js +11 -0
  109. package/dist/security/sensitiveGuard.d.ts +5 -0
  110. package/dist/security/sensitiveGuard.js +27 -8
  111. package/dist/security/toolInvocationGuard.d.ts +2 -2
  112. package/dist/security/toolInvocationGuard.js +1 -1
  113. package/dist/security/workspaceRootGuard.d.ts +8 -0
  114. package/dist/security/workspaceRootGuard.js +47 -0
  115. package/dist/smoke-test.js +280 -273
  116. package/dist/tools/catalog/index.d.ts +4 -0
  117. package/dist/tools/catalog/index.js +4 -0
  118. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  119. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  120. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
  121. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  122. package/dist/tools/definitions/toolDefs.js +1177 -0
  123. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  124. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  125. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
  126. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
  127. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  128. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
  129. package/dist/tools/diagnostics/index.d.ts +6 -0
  130. package/dist/tools/diagnostics/index.js +6 -0
  131. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  132. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  133. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
  134. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
  135. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  136. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  137. package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
  138. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
  139. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
  140. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  141. package/dist/tools/direct/index.d.ts +3 -0
  142. package/dist/tools/direct/index.js +3 -0
  143. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  144. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  145. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  146. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  147. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  148. package/dist/tools/discovery/index.d.ts +3 -0
  149. package/dist/tools/discovery/index.js +3 -0
  150. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  151. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  152. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  153. package/dist/tools/dispatch/coreDispatch.js +283 -0
  154. package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
  155. package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
  156. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  157. package/dist/tools/dispatch/directDispatch.js +116 -0
  158. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  159. package/dist/tools/dispatch/goalDispatch.js +92 -0
  160. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  161. package/dist/tools/dispatch/releaseDispatch.js +46 -0
  162. package/dist/tools/dispatch/types.d.ts +23 -0
  163. package/dist/tools/dispatch/types.js +15 -0
  164. package/dist/tools/dispatch/validation.d.ts +7 -0
  165. package/dist/tools/dispatch/validation.js +71 -0
  166. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  167. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
  168. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  169. package/dist/tools/goals/index.d.ts +3 -0
  170. package/dist/tools/goals/index.js +3 -0
  171. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  172. package/dist/tools/registry.d.ts +10 -14
  173. package/dist/tools/registry.js +47 -1651
  174. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  175. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  176. package/dist/tools/release/index.d.ts +2 -0
  177. package/dist/tools/release/index.js +2 -0
  178. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  179. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  180. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  181. package/dist/tools/tasks/cancelTask.js +85 -0
  182. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  183. package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
  184. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  185. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  186. package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
  187. package/dist/tools/tasks/evidencePack.js +387 -0
  188. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  189. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  190. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  191. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  192. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  193. package/dist/tools/tasks/index.d.ts +18 -0
  194. package/dist/tools/tasks/index.js +18 -0
  195. package/dist/tools/tasks/killTask.d.ts +1 -0
  196. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  197. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  198. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  199. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  200. package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
  201. package/dist/tools/tasks/retryTask.js +60 -0
  202. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  203. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
  204. package/dist/tools/tasks/runVerification.js +38 -0
  205. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  206. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  207. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  208. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  209. package/dist/tools/workspace/applyPatch.js +53 -0
  210. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  211. package/dist/tools/workspace/index.d.ts +10 -0
  212. package/dist/tools/workspace/index.js +10 -0
  213. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  214. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  215. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  216. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  217. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  218. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  219. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  220. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  221. package/dist/tools/workspace/syncFile.js +134 -0
  222. package/dist/utils/atomicFile.d.ts +7 -0
  223. package/dist/utils/atomicFile.js +48 -0
  224. package/dist/utils/boundedFile.d.ts +8 -0
  225. package/dist/utils/boundedFile.js +93 -0
  226. package/dist/utils/lockedJsonFile.d.ts +15 -0
  227. package/dist/utils/lockedJsonFile.js +224 -0
  228. package/dist/utils/platform.d.ts +11 -0
  229. package/dist/utils/platform.js +26 -0
  230. package/dist/utils/stableJson.d.ts +2 -0
  231. package/dist/utils/stableJson.js +12 -0
  232. package/dist/version.d.ts +3 -2
  233. package/dist/version.js +3 -2
  234. package/dist/watcherStatus.d.ts +2 -2
  235. package/dist/watcherStatus.js +18 -0
  236. package/docs/CODE_WIKI.md +1648 -0
  237. package/docs/agentseal-integration.md +150 -0
  238. package/docs/architecture.md +23 -0
  239. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  240. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  241. package/docs/control-center/README.md +4 -4
  242. package/docs/dashboard-overview.md +86 -0
  243. package/docs/demo.md +8 -0
  244. package/docs/desktop-app.md +175 -0
  245. package/docs/direct-session-workflow.md +98 -0
  246. package/docs/evidence-pack-schema.md +215 -0
  247. package/docs/lineage-evidence-pack-workflow.md +127 -0
  248. package/docs/mcp-inspector-testing.md +200 -0
  249. package/docs/open-source-application.md +168 -0
  250. package/docs/opencode-worker.md +151 -0
  251. package/docs/openhands-worker.md +181 -0
  252. package/docs/release-checklist.md +1 -1
  253. package/docs/release-evidence.md +82 -0
  254. package/docs/spec-kit-integration.md +131 -0
  255. package/docs/task-safe-review-workflow.md +98 -0
  256. package/docs/threat-model.md +97 -0
  257. package/docs/user-feedback.md +40 -0
  258. package/docs/why-patchwarden.md +110 -0
  259. package/examples/config.example.json +4 -2
  260. package/package.json +28 -11
  261. package/scripts/README.md +15 -6
  262. package/scripts/build.js +11 -0
  263. package/scripts/checks/brand-check.js +3 -0
  264. package/scripts/checks/build-output-check.js +37 -0
  265. package/scripts/checks/control-center-smoke.js +447 -6
  266. package/scripts/checks/control-smoke.js +19 -8
  267. package/scripts/checks/http-mcp-smoke.js +82 -4
  268. package/scripts/checks/lifecycle-smoke.js +39 -33
  269. package/scripts/checks/mcp-manifest-check.js +1 -1
  270. package/scripts/checks/mcp-smoke.js +30 -4
  271. package/scripts/checks/package-manifest-check.js +20 -7
  272. package/scripts/checks/unit-tests.js +3 -5
  273. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  274. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  275. package/scripts/control/manage-patchwarden.ps1 +69 -4
  276. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  277. package/scripts/control/run-background-supervisor.ps1 +41 -0
  278. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  279. package/scripts/generate-demo-gif.py +320 -0
  280. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  281. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  282. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  283. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  284. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  285. package/scripts/lib/clean-generated-output.js +11 -0
  286. package/scripts/lib/file-discovery.js +12 -0
  287. package/scripts/release/desktop-preflight.js +211 -0
  288. package/scripts/release/pack-clean.js +59 -14
  289. package/src/assessments/agentAssessor.ts +29 -15
  290. package/src/assessments/assessmentStore.ts +55 -43
  291. package/src/assessments/confirmCli.ts +5 -4
  292. package/src/config.ts +105 -11
  293. package/src/control/fileManager.ts +40 -0
  294. package/src/control/middleware/auth.ts +35 -0
  295. package/src/control/middleware/static.ts +84 -0
  296. package/src/control/routeTable.ts +301 -0
  297. package/src/control/routes/audit.ts +329 -0
  298. package/src/control/routes/evidence.ts +107 -0
  299. package/src/control/routes/lineage.ts +92 -0
  300. package/src/control/routes/policy.ts +81 -0
  301. package/src/control/routes/process.ts +505 -0
  302. package/src/control/routes/sessions.ts +314 -0
  303. package/src/control/routes/status.ts +405 -0
  304. package/src/control/routes/taskActions.ts +214 -0
  305. package/src/control/routes/tasks.ts +331 -0
  306. package/src/control/routes/workspace.ts +234 -0
  307. package/src/control/runtime.ts +508 -0
  308. package/src/control/server.ts +165 -0
  309. package/src/control/shared.ts +360 -0
  310. package/src/controlCenter.ts +7 -2347
  311. package/src/direct/directAudit.ts +17 -9
  312. package/src/direct/directGuards.ts +100 -51
  313. package/src/direct/directPatch.ts +95 -8
  314. package/src/direct/directSessionStore.ts +211 -55
  315. package/src/direct/directVerification.ts +9 -23
  316. package/src/doctor.ts +766 -492
  317. package/src/goal/acceptanceEngine.ts +1 -1
  318. package/src/goal/goalProgress.ts +17 -13
  319. package/src/goal/goalReport.ts +263 -0
  320. package/src/goal/goalStatus.ts +25 -3
  321. package/src/goal/goalStore.ts +241 -54
  322. package/src/goal/handoffExport.ts +6 -6
  323. package/src/goal/specKitImport.ts +355 -0
  324. package/src/goal/subgoalSync.ts +15 -10
  325. package/src/goal/worktreeManager.ts +139 -22
  326. package/src/httpServer.ts +130 -44
  327. package/src/index.ts +7 -4
  328. package/src/logging.ts +20 -4
  329. package/src/policy/projectPolicy.ts +5 -2
  330. package/src/release/releaseGate.ts +74 -27
  331. package/src/runner/agentInvocation.ts +126 -4
  332. package/src/runner/changeCapture.ts +342 -80
  333. package/src/runner/cli.ts +7 -6
  334. package/src/runner/postTaskCleanup.ts +55 -7
  335. package/src/runner/processSecurity.ts +433 -0
  336. package/src/runner/runTask.ts +441 -292
  337. package/src/runner/simpleProcess.ts +68 -33
  338. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  339. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  340. package/src/runner/taskStatusStore.ts +54 -0
  341. package/src/runner/watch.ts +290 -57
  342. package/src/security/contentRedaction.ts +31 -2
  343. package/src/security/discoveryTokenStore.ts +18 -4
  344. package/src/security/loopbackHost.ts +19 -0
  345. package/src/security/pathGuard.ts +35 -16
  346. package/src/security/planGuard.ts +2 -1
  347. package/src/security/runtimeGuard.ts +9 -6
  348. package/src/security/secretComparison.ts +13 -0
  349. package/src/security/sensitiveGuard.ts +27 -8
  350. package/src/security/toolInvocationGuard.ts +2 -2
  351. package/src/security/workspaceRootGuard.ts +53 -0
  352. package/src/smoke-test.ts +276 -273
  353. package/src/tools/catalog/index.ts +4 -0
  354. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  355. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
  356. package/src/tools/definitions/toolDefs.ts +1264 -0
  357. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  358. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
  359. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
  360. package/src/tools/diagnostics/index.ts +6 -0
  361. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  362. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
  363. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  364. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
  365. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
  366. package/src/tools/direct/index.ts +3 -0
  367. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  368. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  369. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  370. package/src/tools/discovery/index.ts +3 -0
  371. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  372. package/src/tools/dispatch/coreDispatch.ts +379 -0
  373. package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
  374. package/src/tools/dispatch/directDispatch.ts +168 -0
  375. package/src/tools/dispatch/goalDispatch.ts +128 -0
  376. package/src/tools/dispatch/releaseDispatch.ts +66 -0
  377. package/src/tools/dispatch/types.ts +24 -0
  378. package/src/tools/dispatch/validation.ts +78 -0
  379. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  380. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
  381. package/src/tools/goals/index.ts +3 -0
  382. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  383. package/src/tools/registry.ts +54 -1858
  384. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  385. package/src/tools/release/index.ts +2 -0
  386. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  387. package/src/tools/tasks/cancelTask.ts +109 -0
  388. package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
  389. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  390. package/src/tools/tasks/evidencePack.ts +504 -0
  391. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  392. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  393. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  394. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  395. package/src/tools/tasks/index.ts +18 -0
  396. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  397. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  398. package/src/tools/tasks/retryTask.ts +69 -0
  399. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
  400. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  401. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  402. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  403. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  404. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  405. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  406. package/src/tools/workspace/index.ts +10 -0
  407. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  408. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  409. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  410. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  411. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  412. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  413. package/src/tools/workspace/syncFile.ts +292 -0
  414. package/src/utils/atomicFile.ts +58 -0
  415. package/src/utils/boundedFile.ts +116 -0
  416. package/src/utils/lockedJsonFile.ts +251 -0
  417. package/src/utils/platform.ts +31 -0
  418. package/src/utils/stableJson.ts +12 -0
  419. package/src/version.ts +3 -2
  420. package/src/watcherStatus.ts +19 -1
  421. package/ui/desktop-bootstrap.js +11 -0
  422. package/ui/desktop-bridge.js +36 -0
  423. package/ui/desktop.css +160 -0
  424. package/ui/getting-started.js +93 -0
  425. package/ui/i18n.js +484 -0
  426. package/ui/log-parser.js +30 -0
  427. package/ui/pages/audit.html +204 -3
  428. package/ui/pages/dashboard.html +937 -79
  429. package/ui/pages/direct-sessions.html +609 -60
  430. package/ui/pages/getting-started.html +43 -0
  431. package/ui/pages/logs.html +105 -22
  432. package/ui/pages/settings.html +101 -0
  433. package/ui/pages/task-detail.html +468 -438
  434. package/ui/pages/tasks.html +648 -65
  435. package/ui/pages/workspace.html +38 -1
  436. package/ui/settings.js +245 -0
  437. package/PatchWarden-Control.cmd +0 -6
  438. package/Restart-PatchWarden-Control.cmd +0 -6
  439. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  440. package/dist/test/unit/acceptance-engine.test.js +0 -228
  441. package/dist/test/unit/agent-router.test.d.ts +0 -1
  442. package/dist/test/unit/agent-router.test.js +0 -287
  443. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  444. package/dist/test/unit/android-doctor.test.js +0 -118
  445. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  446. package/dist/test/unit/audit-checks.test.js +0 -350
  447. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  448. package/dist/test/unit/chinese-path.test.js +0 -91
  449. package/dist/test/unit/command-guard.test.d.ts +0 -1
  450. package/dist/test/unit/command-guard.test.js +0 -160
  451. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  452. package/dist/test/unit/diagnose-task.test.js +0 -457
  453. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  454. package/dist/test/unit/direct-guards.test.js +0 -213
  455. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  456. package/dist/test/unit/discovery-token-store.test.js +0 -139
  457. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  458. package/dist/test/unit/evidence-pack.test.js +0 -130
  459. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  460. package/dist/test/unit/goal-graph.test.js +0 -298
  461. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  462. package/dist/test/unit/goal-progress.test.js +0 -381
  463. package/dist/test/unit/goal-status.test.d.ts +0 -1
  464. package/dist/test/unit/goal-status.test.js +0 -215
  465. package/dist/test/unit/goal-store.test.d.ts +0 -1
  466. package/dist/test/unit/goal-store.test.js +0 -253
  467. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  468. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  469. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  470. package/dist/test/unit/goal-tools-registry.test.js +0 -190
  471. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  472. package/dist/test/unit/handoff-export.test.js +0 -263
  473. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  474. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  475. package/dist/test/unit/logging.test.d.ts +0 -1
  476. package/dist/test/unit/logging.test.js +0 -397
  477. package/dist/test/unit/path-guard.test.d.ts +0 -1
  478. package/dist/test/unit/path-guard.test.js +0 -109
  479. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  480. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  481. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  482. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  483. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  484. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  485. package/dist/test/unit/release-gate.test.d.ts +0 -1
  486. package/dist/test/unit/release-gate.test.js +0 -242
  487. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  488. package/dist/test/unit/run-task-loop.test.js +0 -380
  489. package/dist/test/unit/safe-status.test.d.ts +0 -1
  490. package/dist/test/unit/safe-status.test.js +0 -165
  491. package/dist/test/unit/safe-views.test.d.ts +0 -1
  492. package/dist/test/unit/safe-views.test.js +0 -171
  493. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  494. package/dist/test/unit/schema-drift-check.test.js +0 -176
  495. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  496. package/dist/test/unit/sensitive-guard.test.js +0 -104
  497. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  498. package/dist/test/unit/subgoal-sync.test.js +0 -183
  499. package/dist/test/unit/sync-file.test.d.ts +0 -1
  500. package/dist/test/unit/sync-file.test.js +0 -154
  501. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  502. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  503. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  504. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  505. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  506. package/dist/test/unit/toolSearch.test.js +0 -571
  507. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  508. package/dist/test/unit/watcher-status.test.js +0 -169
  509. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  510. package/dist/test/unit/worktree-manager.test.js +0 -176
  511. package/dist/tools/applyPatch.js +0 -41
  512. package/dist/tools/cancelTask.d.ts +0 -30
  513. package/dist/tools/cancelTask.js +0 -64
  514. package/dist/tools/evidencePack.js +0 -168
  515. package/dist/tools/finalizeDirectSession.js +0 -84
  516. package/dist/tools/goalSubgoalTask.js +0 -110
  517. package/dist/tools/killTask.d.ts +0 -15
  518. package/dist/tools/retryTask.js +0 -45
  519. package/dist/tools/runVerification.js +0 -36
  520. package/dist/tools/syncFile.js +0 -65
  521. package/docs/control-center/control-center-daily-driver.md +0 -211
  522. package/docs/control-center/control-center-mvp.md +0 -205
  523. package/docs/control-center/control-center-phase2.md +0 -159
  524. package/docs/release-v0.6.0.md +0 -71
  525. package/docs/release-v0.6.1.md +0 -75
  526. package/docs/release-v0.6.4.md +0 -45
  527. package/src/test/unit/acceptance-engine.test.ts +0 -261
  528. package/src/test/unit/agent-router.test.ts +0 -342
  529. package/src/test/unit/android-doctor.test.ts +0 -158
  530. package/src/test/unit/audit-checks.test.ts +0 -567
  531. package/src/test/unit/chinese-path.test.ts +0 -106
  532. package/src/test/unit/command-guard.test.ts +0 -221
  533. package/src/test/unit/diagnose-task.test.ts +0 -544
  534. package/src/test/unit/direct-guards.test.ts +0 -297
  535. package/src/test/unit/discovery-token-store.test.ts +0 -181
  536. package/src/test/unit/evidence-pack.test.ts +0 -142
  537. package/src/test/unit/goal-graph.test.ts +0 -347
  538. package/src/test/unit/goal-progress.test.ts +0 -538
  539. package/src/test/unit/goal-status.test.ts +0 -270
  540. package/src/test/unit/goal-store.test.ts +0 -318
  541. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  542. package/src/test/unit/goal-tools-registry.test.ts +0 -243
  543. package/src/test/unit/handoff-export.test.ts +0 -295
  544. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  545. package/src/test/unit/logging.test.ts +0 -497
  546. package/src/test/unit/path-guard.test.ts +0 -150
  547. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  548. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  549. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  550. package/src/test/unit/release-gate.test.ts +0 -314
  551. package/src/test/unit/run-task-loop.test.ts +0 -425
  552. package/src/test/unit/safe-status.test.ts +0 -187
  553. package/src/test/unit/safe-views.test.ts +0 -184
  554. package/src/test/unit/schema-drift-check.test.ts +0 -260
  555. package/src/test/unit/sensitive-guard.test.ts +0 -124
  556. package/src/test/unit/subgoal-sync.test.ts +0 -236
  557. package/src/test/unit/sync-file.test.ts +0 -231
  558. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  559. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  560. package/src/test/unit/toolSearch.test.ts +0 -756
  561. package/src/test/unit/watcher-status.test.ts +0 -190
  562. package/src/test/unit/worktree-manager.test.ts +0 -247
  563. package/src/tools/cancelTask.ts +0 -70
  564. package/src/tools/evidencePack.ts +0 -205
  565. package/src/tools/retryTask.ts +0 -53
  566. package/src/tools/syncFile.ts +0 -122
  567. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  568. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  569. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  570. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  571. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  572. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  573. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  574. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  575. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  576. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  577. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  578. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  579. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  580. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  581. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  582. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  583. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  584. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  585. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  586. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  587. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  588. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  589. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  590. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  591. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  592. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  593. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  594. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  595. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  596. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  597. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -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 ───────────────────────────────────────────────
@@ -1,9 +1,12 @@
1
- import { resolve, relative, isAbsolute, sep } from "node:path";
1
+ import { relative } from "node:path";
2
2
  import { existsSync, statSync, openSync, readSync, closeSync } from "node:fs";
3
3
  import { PatchWardenError } from "../errors.js";
4
- import { isSensitivePath } from "../security/sensitiveGuard.js";
5
- import { guardWorkspacePath } from "../security/pathGuard.js";
6
- import { getConfig } from "../config.js";
4
+ import {
5
+ hasWindowsAlternateDataStream,
6
+ isSensitivePath,
7
+ } from "../security/sensitiveGuard.js";
8
+ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
9
+ import { getConfig, type PatchWardenConfig } from "../config.js";
7
10
  import type { DirectSessionRecord } from "./directSessionStore.js";
8
11
 
9
12
  // ── Session state guards ───────────────────────────────────────────
@@ -55,36 +58,34 @@ export function guardDirectPath(
55
58
  resolvedRepoPath: string,
56
59
  workspaceRoot: string
57
60
  ): string {
58
- const config = getConfig();
59
- const resolved = resolve(resolvedRepoPath, filePath);
60
- const normalizedRepo = resolve(resolvedRepoPath);
61
- const normalizedWs = resolve(workspaceRoot);
62
-
63
- // Check path is inside workspace root
64
- const relToWs = relative(normalizedWs, resolved);
65
- if (isAbsolute(relToWs) || relToWs.startsWith("..")) {
61
+ if (hasWindowsAlternateDataStream(filePath)) {
66
62
  throw new PatchWardenError(
67
- "path_outside_repo",
68
- `Path "${filePath}" resolves outside the session repo.`,
69
- "Use a relative path inside the session's repo_path.",
63
+ "windows_ads_path_blocked",
64
+ `Path "${filePath}" contains a Windows alternate data stream suffix.`,
65
+ "Use a normal relative file path without a colon or NTFS stream name.",
70
66
  true,
71
67
  { path: filePath, operation: "direct_path_access" }
72
68
  );
73
69
  }
74
70
 
75
- // Check path is inside repo_path
76
- const relToRepo = relative(normalizedRepo, resolved);
77
- if (isAbsolute(relToRepo) || relToRepo.startsWith("..")) {
78
- throw new PatchWardenError(
79
- "path_outside_repo",
80
- `Path "${filePath}" is outside the session repo_path "${resolvedRepoPath}".`,
81
- "Use a relative path inside the session's repo_path.",
82
- true,
83
- { path: filePath, operation: "direct_path_access" }
84
- );
71
+ // Resolve the repo first so a repo-level symlink/junction cannot escape the
72
+ // workspace. guardPath then resolves the candidate or its nearest existing
73
+ // parent, closing intermediate symlink/junction escapes for new files too.
74
+ try {
75
+ const guardedRepo = guardWorkspacePath(resolvedRepoPath, workspaceRoot);
76
+ return guardPath(filePath, guardedRepo);
77
+ } catch (error) {
78
+ if (error instanceof PatchWardenError && error.reason === "workspace_path_escape") {
79
+ throw new PatchWardenError(
80
+ "path_outside_repo",
81
+ `Path "${filePath}" resolves outside the session repo.`,
82
+ "Use a relative path inside the session's repo_path.",
83
+ true,
84
+ { path: filePath, operation: "direct_path_access" }
85
+ );
86
+ }
87
+ throw error;
85
88
  }
86
-
87
- return resolved;
88
89
  }
89
90
 
90
91
  /**
@@ -96,13 +97,15 @@ export function guardDirectReadPath(
96
97
  workspaceRoot: string
97
98
  ): string {
98
99
  const resolved = guardDirectPath(filePath, resolvedRepoPath, workspaceRoot);
99
- const normalized = filePath.replace(/\\/g, "/");
100
+ const policyPaths = directPolicyPaths(
101
+ filePath,
102
+ resolved,
103
+ resolvedRepoPath,
104
+ workspaceRoot
105
+ );
100
106
 
101
107
  // Block internal PatchWarden paths (sessions, tasks, plans, assessments)
102
- if (
103
- normalized.startsWith(".patchwarden/") ||
104
- normalized.includes("/.patchwarden/")
105
- ) {
108
+ if (policyPaths.some((path) => hasPathSegment(path, ".patchwarden"))) {
106
109
  throw new PatchWardenError(
107
110
  "internal_patchwarden_path_blocked",
108
111
  `Access denied: "${filePath}" is inside the internal .patchwarden directory.`,
@@ -112,7 +115,7 @@ export function guardDirectReadPath(
112
115
  );
113
116
  }
114
117
 
115
- if (isSensitivePath(filePath)) {
118
+ if (policyPaths.some((path) => isSensitivePath(path))) {
116
119
  throw new PatchWardenError(
117
120
  "sensitive_path_blocked",
118
121
  `Access denied: "${filePath}" matches a sensitive file pattern.`,
@@ -145,13 +148,15 @@ export function guardDirectWritePath(
145
148
  workspaceRoot: string
146
149
  ): string {
147
150
  const resolved = guardDirectPath(filePath, resolvedRepoPath, workspaceRoot);
148
- const normalized = filePath.replace(/\\/g, "/");
151
+ const policyPaths = directPolicyPaths(
152
+ filePath,
153
+ resolved,
154
+ resolvedRepoPath,
155
+ workspaceRoot
156
+ );
149
157
 
150
158
  // Block internal PatchWarden paths
151
- if (
152
- normalized.startsWith(".patchwarden/") ||
153
- normalized.includes("/.patchwarden/")
154
- ) {
159
+ if (policyPaths.some((path) => hasPathSegment(path, ".patchwarden"))) {
155
160
  throw new PatchWardenError(
156
161
  "internal_patchwarden_path_blocked",
157
162
  `Modification denied: "${filePath}" is inside the internal .patchwarden directory.`,
@@ -161,7 +166,7 @@ export function guardDirectWritePath(
161
166
  );
162
167
  }
163
168
 
164
- if (isSensitivePath(normalized)) {
169
+ if (policyPaths.some((path) => isSensitivePath(path))) {
165
170
  throw new PatchWardenError(
166
171
  "sensitive_path_blocked",
167
172
  `Modification denied: "${filePath}" matches a sensitive file pattern.`,
@@ -172,7 +177,7 @@ export function guardDirectWritePath(
172
177
  }
173
178
 
174
179
  // Block node_modules
175
- if (normalized.startsWith("node_modules/") || normalized.includes("/node_modules/")) {
180
+ if (policyPaths.some((path) => hasPathSegment(path, "node_modules"))) {
176
181
  throw new PatchWardenError(
177
182
  "blocked_artifact_path",
178
183
  `Modification denied: "${filePath}" is inside node_modules.`,
@@ -183,7 +188,7 @@ export function guardDirectWritePath(
183
188
  }
184
189
 
185
190
  // Block release/
186
- if (normalized.startsWith("release/") || normalized.includes("/release/")) {
191
+ if (policyPaths.some((path) => hasPathSegment(path, "release"))) {
187
192
  throw new PatchWardenError(
188
193
  "blocked_artifact_path",
189
194
  `Modification denied: "${filePath}" is inside the release directory.`,
@@ -194,7 +199,7 @@ export function guardDirectWritePath(
194
199
  }
195
200
 
196
201
  // Block dist/
197
- if (normalized.startsWith("dist/") || normalized.includes("/dist/")) {
202
+ if (policyPaths.some((path) => hasPathSegment(path, "dist"))) {
198
203
  throw new PatchWardenError(
199
204
  "blocked_artifact_path",
200
205
  `Modification denied: "${filePath}" is inside the dist directory.`,
@@ -217,6 +222,27 @@ export function guardDirectWritePath(
217
222
  return resolved;
218
223
  }
219
224
 
225
+ function directPolicyPaths(
226
+ requestedPath: string,
227
+ resolvedPath: string,
228
+ resolvedRepoPath: string,
229
+ workspaceRoot: string
230
+ ): string[] {
231
+ const guardedRepo = guardWorkspacePath(resolvedRepoPath, workspaceRoot);
232
+ return [
233
+ requestedPath.replace(/\\/g, "/"),
234
+ relative(guardedRepo, resolvedPath).replace(/\\/g, "/"),
235
+ ];
236
+ }
237
+
238
+ function hasPathSegment(filePath: string, expectedSegment: string): boolean {
239
+ const expected = expectedSegment.toLowerCase();
240
+ return filePath
241
+ .split("/")
242
+ .filter(Boolean)
243
+ .some((segment) => segment.toLowerCase() === expected);
244
+ }
245
+
220
246
  // ── Size guards ────────────────────────────────────────────────────
221
247
 
222
248
  export function guardDirectPatchSize(patchBytes: number): void {
@@ -232,8 +258,10 @@ export function guardDirectPatchSize(patchBytes: number): void {
232
258
  }
233
259
  }
234
260
 
235
- export function guardDirectFileSize(fileBytes: number): void {
236
- const config = getConfig();
261
+ export function guardDirectFileSize(
262
+ fileBytes: number,
263
+ config: PatchWardenConfig = getConfig(),
264
+ ): void {
237
265
  if (fileBytes > config.directMaxFileBytes) {
238
266
  throw new PatchWardenError(
239
267
  "file_too_large",
@@ -257,22 +285,43 @@ const BINARY_EXTENSIONS = new Set([
257
285
  ".wasm", ".node",
258
286
  ]);
259
287
 
288
+ /**
289
+ * Maximum bytes to scan for null-byte detection when the extension is not
290
+ * a known binary type. 1 MB catches binary content embedded deep in files
291
+ * (e.g., null bytes at offset 8200) without reading excessively large files.
292
+ */
293
+ const BINARY_SCAN_LIMIT = 1_048_576; // 1 MB
294
+ const BINARY_SCAN_CHUNK = 65536; // 64 KB per read
295
+
260
296
  export function isBinaryFile(filePath: string): boolean {
261
297
  const normalized = filePath.replace(/\\/g, "/").toLowerCase();
262
298
  for (const ext of BINARY_EXTENSIONS) {
263
299
  if (normalized.endsWith(ext)) return true;
264
300
  }
265
301
 
266
- // Check file content for null bytes (simple heuristic)
302
+ // Scan file content for null bytes in chunks up to BINARY_SCAN_LIMIT.
303
+ // Previously only the first 8 KB were checked, allowing null bytes at
304
+ // offset 8193+ to bypass detection.
267
305
  try {
306
+ const stat = statSync(filePath);
307
+ const scanSize = Math.min(stat.size, BINARY_SCAN_LIMIT);
268
308
  const fd = openSync(filePath, "r");
269
- const buffer = Buffer.alloc(8192);
270
- const bytesRead = readSync(fd, buffer, 0, 8192, 0);
271
- closeSync(fd);
272
- for (let i = 0; i < bytesRead; i++) {
273
- if (buffer[i] === 0) return true;
309
+ try {
310
+ const chunk = Buffer.alloc(BINARY_SCAN_CHUNK);
311
+ let scanned = 0;
312
+ while (scanned < scanSize) {
313
+ const toRead = Math.min(BINARY_SCAN_CHUNK, scanSize - scanned);
314
+ const bytesRead = readSync(fd, chunk, 0, toRead, scanned);
315
+ if (bytesRead === 0) break;
316
+ for (let i = 0; i < bytesRead; i++) {
317
+ if (chunk[i] === 0) return true;
318
+ }
319
+ scanned += bytesRead;
320
+ }
321
+ return false;
322
+ } finally {
323
+ closeSync(fd);
274
324
  }
275
- return false;
276
325
  } catch {
277
326
  return false;
278
327
  }
@@ -1,7 +1,8 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
1
+ import { closeSync, existsSync, fstatSync, openSync, readFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
- import { dirname, resolve } from "node:path";
4
3
  import { PatchWardenError } from "../errors.js";
4
+ import { atomicWriteFileSync } from "../utils/atomicFile.js";
5
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
5
6
 
6
7
  // ── Types ──────────────────────────────────────────────────────────
7
8
 
@@ -36,6 +37,12 @@ export function computeContentSha256(content: string): string {
36
37
  return createHash("sha256").update(content, "utf-8").digest("hex");
37
38
  }
38
39
 
40
+ export interface ApplyPatchOptions {
41
+ expectedSha256?: string;
42
+ maxFileBytes?: number;
43
+ revalidatePath?: () => string;
44
+ }
45
+
39
46
  export function validateExpectedSha256(
40
47
  filePath: string,
41
48
  expectedSha256: string
@@ -68,7 +75,8 @@ export function validateExpectedSha256(
68
75
 
69
76
  export function applyPatchOperations(
70
77
  filePath: string,
71
- operations: PatchOperation[]
78
+ operations: PatchOperation[],
79
+ options: ApplyPatchOptions = {},
72
80
  ): ApplyPatchResult {
73
81
  if (!existsSync(filePath)) {
74
82
  throw new PatchWardenError(
@@ -80,8 +88,40 @@ export function applyPatchOperations(
80
88
  );
81
89
  }
82
90
 
83
- const beforeContent = readFileSync(filePath, "utf-8");
84
- const beforeSha256 = computeContentSha256(beforeContent);
91
+ let descriptor: number | null = null;
92
+ let beforeBytes: Buffer;
93
+ let existingMode: number;
94
+ try {
95
+ descriptor = openSync(filePath, "r");
96
+ const metadata = fstatSync(descriptor);
97
+ if (!metadata.isFile()) {
98
+ throw new PatchWardenError(
99
+ "direct_target_not_file",
100
+ `Patch target is not a regular file: "${filePath}".`,
101
+ "Choose an existing text file inside the Direct session repository.",
102
+ );
103
+ }
104
+ guardFileByteLimit(metadata.size, options.maxFileBytes);
105
+ existingMode = metadata.mode;
106
+ beforeBytes = readFileSync(descriptor);
107
+ } finally {
108
+ if (descriptor !== null) closeSync(descriptor);
109
+ }
110
+ guardFileByteLimit(beforeBytes.length, options.maxFileBytes);
111
+ const beforeSha256 = createHash("sha256").update(beforeBytes).digest("hex");
112
+ if (options.expectedSha256 && beforeSha256 !== options.expectedSha256) {
113
+ throw fileHashMismatch(options.expectedSha256, beforeSha256);
114
+ }
115
+ const beforeContent = beforeBytes.toString("utf-8");
116
+ if (!Buffer.from(beforeContent, "utf-8").equals(beforeBytes)) {
117
+ throw new PatchWardenError(
118
+ "unsupported_text_encoding",
119
+ `File "${filePath}" is not valid UTF-8 text.`,
120
+ "Use a normal task or convert the file to UTF-8 before Direct editing.",
121
+ true,
122
+ { path: filePath },
123
+ );
124
+ }
85
125
  let content = beforeContent;
86
126
  let operationsApplied = 0;
87
127
 
@@ -91,13 +131,33 @@ export function applyPatchOperations(
91
131
  }
92
132
 
93
133
  const afterSha256 = computeContentSha256(content);
134
+ guardFileByteLimit(Buffer.byteLength(content, "utf-8"), options.maxFileBytes);
135
+ const sensitive = redactSensitiveContent(content);
136
+ if (sensitive.redacted) {
137
+ throw new PatchWardenError(
138
+ "sensitive_content_blocked",
139
+ `Patch result contains credential-like content (${sensitive.redaction_categories.join(", ")}).`,
140
+ "Remove the sensitive value and retry with placeholders or environment-variable references.",
141
+ true,
142
+ { path: filePath, redaction_categories: sensitive.redaction_categories },
143
+ );
144
+ }
94
145
  const bytesChanged = Math.abs(
95
146
  Buffer.byteLength(content, "utf-8") - Buffer.byteLength(beforeContent, "utf-8")
96
147
  );
97
148
 
98
- // Ensure parent directory exists (should already exist for existing files)
99
- mkdirSync(dirname(filePath), { recursive: true });
100
- writeFileSync(filePath, content, "utf-8");
149
+ if (options.revalidatePath && !samePath(options.revalidatePath(), filePath)) {
150
+ throw new PatchWardenError(
151
+ "direct_path_changed_during_write",
152
+ `Patch target path changed while preparing "${filePath}".`,
153
+ "Retry only after concurrent filesystem changes stop.",
154
+ true,
155
+ { path: filePath, operation: "apply_patch" },
156
+ );
157
+ }
158
+ const currentSha256 = computeFileSha256(filePath);
159
+ if (currentSha256 !== beforeSha256) throw fileHashMismatch(beforeSha256, currentSha256);
160
+ atomicWriteFileSync(filePath, content, { mode: existingMode });
101
161
 
102
162
  return {
103
163
  before_sha256: beforeSha256,
@@ -107,6 +167,33 @@ export function applyPatchOperations(
107
167
  };
108
168
  }
109
169
 
170
+ function guardFileByteLimit(fileBytes: number, maxFileBytes: number | undefined): void {
171
+ if (maxFileBytes === undefined || fileBytes <= maxFileBytes) return;
172
+ throw new PatchWardenError(
173
+ "file_too_large",
174
+ `File size ${fileBytes} bytes exceeds maximum ${maxFileBytes} bytes.`,
175
+ "Use a normal task for larger files.",
176
+ true,
177
+ { file_bytes: fileBytes, max_bytes: maxFileBytes },
178
+ );
179
+ }
180
+
181
+ function fileHashMismatch(expectedSha256: string, actualSha256: string): PatchWardenError {
182
+ return new PatchWardenError(
183
+ "file_hash_mismatch",
184
+ `File hash mismatch. Expected "${expectedSha256}" but got "${actualSha256}".`,
185
+ "Re-read the file to get the current sha256, then retry the patch.",
186
+ true,
187
+ { expected_sha256: expectedSha256, actual_sha256: actualSha256 },
188
+ );
189
+ }
190
+
191
+ function samePath(left: string, right: string): boolean {
192
+ return process.platform === "win32"
193
+ ? left.toLowerCase() === right.toLowerCase()
194
+ : left === right;
195
+ }
196
+
110
197
  function applySingleOperation(
111
198
  content: string,
112
199
  op: PatchOperation