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,26 +1,40 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstatSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
2
+ import { createReadStream, lstatSync, readFileSync, readdirSync, } from "node:fs";
3
3
  import { join, relative, resolve, isAbsolute } from "node:path";
4
- import { spawnSync } from "node:child_process";
4
+ import { execFile } from "node:child_process";
5
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
5
6
  import { isSensitivePath } from "../security/sensitiveGuard.js";
7
+ import { nullDevice } from "../utils/platform.js";
8
+ import { buildGitEnvironment, resolveTrustedExecutable } from "./processSecurity.js";
9
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
6
10
  const MAX_HASH_BYTES = 5 * 1024 * 1024;
7
11
  const MAX_SNAPSHOT_FILES = 5000;
8
12
  const MAX_DIFF_BYTES = 20 * 1024 * 1024;
13
+ const DIFF_TRUNCATION_MARKER = "\n[PATCHWARDEN DIFF TRUNCATED]\n";
9
14
  const SKIP_DIRECTORIES = new Set([".git", ".patchwarden", "node_modules"]);
10
- export function captureRepoSnapshot(repoPath) {
15
+ export async function captureRepoSnapshot(repoPath, signal) {
16
+ throwIfAborted(signal);
11
17
  const warnings = [];
12
- const isGit = runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]).stdout.trim() === "true";
18
+ const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"], signal);
19
+ const isGit = isGitResult.stdout.trim() === "true";
13
20
  let head = null;
14
21
  let status = "";
15
22
  let paths = [];
16
- const trackedPaths = new Set();
17
- const ignoredPaths = new Set();
23
+ const trackedPaths = new Map();
24
+ const ignoredPaths = new Map();
18
25
  const dirtyPaths = new Set();
19
26
  if (isGit) {
20
- const headResult = runGit(repoPath, ["rev-parse", "HEAD"]);
27
+ // The following five git calls are independent of each other and can run in parallel.
28
+ const [headResult, statusResult, trackedResult, ignoredResult, listedResult] = await Promise.all([
29
+ runGit(repoPath, ["rev-parse", "HEAD"], signal),
30
+ runGit(repoPath, ["status", "--porcelain=v1", "-uall"], signal),
31
+ runGit(repoPath, ["ls-files", "-z"], signal),
32
+ runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"], signal),
33
+ runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"], signal),
34
+ ]);
21
35
  if (headResult.status === 0)
22
36
  head = headResult.stdout.trim() || null;
23
- status = runGit(repoPath, ["status", "--porcelain=v1", "-uall"]).stdout.trimEnd();
37
+ status = statusResult.stdout.trimEnd();
24
38
  // Parse git status --porcelain to collect all dirty paths
25
39
  for (const line of status.split("\n")) {
26
40
  if (line.length < 4)
@@ -45,34 +59,31 @@ export function captureRepoSnapshot(repoPath) {
45
59
  }
46
60
  }
47
61
  }
48
- const tracked = runGit(repoPath, ["ls-files", "-z"]);
49
- if (tracked.status === 0) {
50
- for (const path of tracked.stdout.split("\0").filter(Boolean))
51
- trackedPaths.add(normalizePath(path));
62
+ if (trackedResult.status === 0) {
63
+ for (const path of trackedResult.stdout.split("\0").filter(Boolean))
64
+ addSnapshotPath(trackedPaths, path);
52
65
  }
53
- const ignored = runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]);
54
- if (ignored.status === 0) {
55
- for (const path of ignored.stdout.split("\0").filter(Boolean))
56
- ignoredPaths.add(normalizePath(path));
66
+ if (ignoredResult.status === 0) {
67
+ for (const path of ignoredResult.stdout.split("\0").filter(Boolean))
68
+ addSnapshotPath(ignoredPaths, path);
57
69
  }
58
70
  else {
59
71
  warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
60
72
  }
61
- const listed = runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]);
62
- if (listed.status === 0) {
73
+ if (listedResult.status === 0) {
63
74
  paths = [...new Set([
64
- ...listed.stdout.split("\0").filter(Boolean),
65
- ...walkWorkspace(repoPath),
75
+ ...listedResult.stdout.split("\0").filter(Boolean),
76
+ ...walkWorkspace(repoPath, signal),
66
77
  ])];
67
78
  }
68
79
  else {
69
80
  warnings.push("git ls-files failed; using bounded filesystem scan");
70
- paths = walkWorkspace(repoPath);
81
+ paths = walkWorkspace(repoPath, signal);
71
82
  }
72
83
  }
73
84
  else {
74
85
  warnings.push("repository is not a Git worktree; diff will contain file-change evidence only");
75
- paths = walkWorkspace(repoPath);
86
+ paths = walkWorkspace(repoPath, signal);
76
87
  }
77
88
  if (paths.length > MAX_SNAPSHOT_FILES) {
78
89
  warnings.push(`snapshot limited to ${MAX_SNAPSHOT_FILES} files`);
@@ -80,6 +91,7 @@ export function captureRepoSnapshot(repoPath) {
80
91
  }
81
92
  const files = {};
82
93
  for (const inputPath of paths.sort()) {
94
+ throwIfAborted(signal);
83
95
  const normalized = normalizePath(inputPath);
84
96
  if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized))
85
97
  continue;
@@ -94,8 +106,8 @@ export function captureRepoSnapshot(repoPath) {
94
106
  files[normalized] = {
95
107
  size: stat.size,
96
108
  sha256,
97
- tracked: trackedPaths.has(normalized),
98
- ignored: !trackedPaths.has(normalized) && ignoredPaths.has(normalized),
109
+ tracked: hasSnapshotPath(trackedPaths, normalized),
110
+ ignored: !hasSnapshotPath(trackedPaths, normalized) && hasSnapshotPath(ignoredPaths, normalized),
99
111
  };
100
112
  }
101
113
  catch {
@@ -114,32 +126,14 @@ export function captureRepoSnapshot(repoPath) {
114
126
  };
115
127
  }
116
128
  export function writeSnapshot(taskDir, filename, snapshot) {
117
- writeFileSync(join(taskDir, filename), JSON.stringify(snapshot, null, 2), "utf-8");
129
+ atomicWriteJsonFileSync(join(taskDir, filename), snapshot);
118
130
  }
119
- export function buildChangeArtifacts(repoPath, before, after) {
131
+ export async function buildChangeArtifacts(repoPath, before, after, signal) {
132
+ throwIfAborted(signal);
120
133
  const changedFiles = compareSnapshots(before, after);
121
134
  const artifactHygiene = classifyArtifactHygiene(changedFiles);
122
135
  const sections = [];
123
136
  const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
124
- if (before.is_git && after.is_git && scopedPaths.length > 0) {
125
- if (before.head && after.head && before.head !== after.head) {
126
- const committed = runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
127
- if (committed.stdout.trim())
128
- sections.push("# Changes committed during task\n", committed.stdout.trimEnd());
129
- }
130
- const base = after.head || "HEAD";
131
- const working = runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
132
- if (working.stdout.trim())
133
- sections.push("# Staged and unstaged changes\n", working.stdout.trimEnd());
134
- for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
135
- const tracked = runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path]);
136
- if (tracked.status === 0)
137
- continue;
138
- const untracked = runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
139
- if (untracked.stdout.trim())
140
- sections.push("# Untracked file\n", untracked.stdout.trimEnd());
141
- }
142
- }
143
137
  const evidence = [
144
138
  "# PatchWarden change evidence",
145
139
  `# changed_files: ${changedFiles.length}`,
@@ -147,16 +141,70 @@ export function buildChangeArtifacts(repoPath, before, after) {
147
141
  `# workspace_dirty_after: ${after.workspace_dirty}`,
148
142
  ...changedFiles.map((file) => `# ${file.change}: ${file.path}`),
149
143
  ].join("\n");
150
- const body = sections.join("\n\n");
151
- const fullDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
144
+ let retainedSectionBytes = 0;
145
+ let diffTruncated = Buffer.byteLength(evidence, "utf-8") > MAX_DIFF_BYTES;
146
+ const sectionBudget = Math.max(0, MAX_DIFF_BYTES
147
+ - Math.min(Buffer.byteLength(evidence, "utf-8"), MAX_DIFF_BYTES)
148
+ - Buffer.byteLength(DIFF_TRUNCATION_MARKER, "utf-8")
149
+ - 2);
150
+ const appendSection = (label, content, commandTruncated = false) => {
151
+ if (!content.trim()) {
152
+ if (commandTruncated)
153
+ diffTruncated = true;
154
+ return;
155
+ }
156
+ const block = `${sections.length > 0 ? "\n\n" : ""}${label}\n\n${content.trimEnd()}`;
157
+ const remaining = Math.max(0, sectionBudget - retainedSectionBytes);
158
+ const bounded = utf8Prefix(block, remaining);
159
+ if (bounded) {
160
+ sections.push(bounded);
161
+ retainedSectionBytes += Buffer.byteLength(bounded, "utf-8");
162
+ }
163
+ if (commandTruncated || Buffer.byteLength(bounded, "utf-8") < Buffer.byteLength(block, "utf-8")) {
164
+ diffTruncated = true;
165
+ }
166
+ };
167
+ if (before.is_git && after.is_git && scopedPaths.length > 0) {
168
+ if (before.head && after.head && before.head !== after.head) {
169
+ const committed = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths], signal);
170
+ appendSection("# Changes committed during task", committed.stdout, committed.truncated);
171
+ }
172
+ if (retainedSectionBytes < sectionBudget) {
173
+ const base = after.head || "HEAD";
174
+ const working = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", base, "--", ...scopedPaths], signal);
175
+ appendSection("# Staged and unstaged changes", working.stdout, working.truncated);
176
+ }
177
+ else {
178
+ diffTruncated = true;
179
+ }
180
+ for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
181
+ throwIfAborted(signal);
182
+ if (retainedSectionBytes >= sectionBudget) {
183
+ diffTruncated = true;
184
+ break;
185
+ }
186
+ const tracked = await runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path], signal);
187
+ if (tracked.status === 0)
188
+ continue;
189
+ const untracked = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-index", "--no-color", "--binary", "--", nullDevice, file.path], signal);
190
+ appendSection("# Untracked file", untracked.stdout, untracked.truncated);
191
+ }
192
+ }
193
+ const body = sections.join("");
194
+ const rawDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
195
+ const sanitized = sanitizeDiffEvidence(rawDiff, MAX_DIFF_BYTES);
196
+ const fullDiff = sanitized.content;
197
+ diffTruncated ||= sanitized.truncated;
152
198
  const additions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
153
199
  const deletions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
154
- const fileStats = buildFileStats(repoPath, before, after, changedFiles);
200
+ const fileStats = await buildFileStats(repoPath, before, after, changedFiles, signal);
155
201
  return {
156
202
  changed_files: changedFiles,
157
203
  diff: fullDiff,
158
204
  diff_available: changedFiles.length > 0,
159
- diff_truncated: false,
205
+ diff_truncated: diffTruncated,
206
+ diff_redacted: sanitized.redacted,
207
+ diff_redaction_categories: sanitized.redaction_categories,
160
208
  diff_size_bytes: Buffer.byteLength(fullDiff, "utf-8"),
161
209
  additions,
162
210
  deletions,
@@ -172,8 +220,32 @@ export function buildChangeArtifacts(repoPath, before, after) {
172
220
  artifact_hygiene: artifactHygiene,
173
221
  };
174
222
  }
175
- function buildFileStats(repoPath, before, after, changedFiles) {
176
- return changedFiles.map((file) => {
223
+ export function sanitizeDiffEvidence(input, maxBytes = MAX_DIFF_BYTES) {
224
+ if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
225
+ throw new Error("maxBytes must be a positive integer");
226
+ }
227
+ const redaction = redactSensitiveContent(input);
228
+ const encodedBytes = Buffer.byteLength(redaction.content, "utf-8");
229
+ if (encodedBytes <= maxBytes) {
230
+ return {
231
+ content: redaction.content,
232
+ truncated: false,
233
+ redacted: redaction.redacted,
234
+ redaction_categories: redaction.redaction_categories,
235
+ };
236
+ }
237
+ const marker = utf8Prefix(DIFF_TRUNCATION_MARKER, maxBytes);
238
+ const markerBytes = Buffer.byteLength(marker, "utf-8");
239
+ return {
240
+ content: utf8Prefix(redaction.content, Math.max(0, maxBytes - markerBytes)) + marker,
241
+ truncated: true,
242
+ redacted: redaction.redacted,
243
+ redaction_categories: redaction.redaction_categories,
244
+ };
245
+ }
246
+ async function buildFileStats(repoPath, before, after, changedFiles, signal) {
247
+ const results = await Promise.all(changedFiles.map(async (file) => {
248
+ throwIfAborted(signal);
177
249
  let additions = 0;
178
250
  let deletions = 0;
179
251
  const paths = file.old_path ? [file.old_path, file.path] : [file.path];
@@ -183,8 +255,8 @@ function buildFileStats(repoPath, before, after, changedFiles) {
183
255
  ranges.push([before.head, after.head]);
184
256
  }
185
257
  ranges.push([after.head || "HEAD"]);
186
- for (const range of ranges) {
187
- const result = runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths]);
258
+ const rangeResults = await Promise.all(ranges.map((range) => runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--numstat", ...range, "--", ...paths], signal)));
259
+ for (const result of rangeResults) {
188
260
  for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
189
261
  const [added, removed] = line.split(/\s+/);
190
262
  if (/^\d+$/.test(added))
@@ -199,29 +271,41 @@ function buildFileStats(repoPath, before, after, changedFiles) {
199
271
  const content = readFileSync(resolve(repoPath, file.path), "utf-8");
200
272
  additions = countLines(content);
201
273
  }
202
- catch { }
274
+ catch { } // probe failure handled by return value (additions stays 0)
203
275
  }
204
276
  return { path: file.path, status: file.change, additions, deletions };
205
- });
277
+ }));
278
+ return results;
206
279
  }
207
280
  function countLines(content) {
208
281
  if (content.length === 0)
209
282
  return 0;
210
283
  return content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
211
284
  }
212
- export function compareSnapshots(before, after) {
213
- const paths = [...new Set([...Object.keys(before.files), ...Object.keys(after.files)])].sort();
285
+ export function compareSnapshots(before, after, platform = process.platform) {
214
286
  const changed = [];
215
- for (const path of paths) {
216
- const left = before.files[path];
217
- const right = after.files[path];
287
+ for (const { path, leftPath, rightPath, left, right } of pairSnapshotFiles(before.files, after.files, platform)) {
218
288
  if (!left && right) {
219
289
  changed.push(classifyChangedFile(path, "added", null, right));
220
290
  }
221
291
  else if (left && !right) {
222
292
  changed.push(classifyChangedFile(path, "deleted", left, null));
223
293
  }
224
- else if (left.sha256 !== right.sha256) {
294
+ else if (left && right && left.sha256 === right.sha256 && leftPath !== rightPath) {
295
+ // Windows lookup is case-insensitive, but a case-only Git rename is still
296
+ // auditable work and must not disappear from the evidence set.
297
+ changed.push({
298
+ path: rightPath,
299
+ old_path: leftPath,
300
+ change: "renamed",
301
+ before_sha256: left.sha256,
302
+ after_sha256: right.sha256,
303
+ tracked: left.tracked || right.tracked,
304
+ ignored: right.ignored,
305
+ kind: classifyPathKind(rightPath),
306
+ });
307
+ }
308
+ else if (left && right && left.sha256 !== right.sha256) {
225
309
  changed.push(classifyChangedFile(path, "modified", left, right));
226
310
  }
227
311
  }
@@ -274,9 +358,11 @@ export function emptyArtifactHygiene() {
274
358
  * Extract files that are dirty in the workspace but outside the target repo.
275
359
  * Used to establish a baseline before task execution.
276
360
  */
277
- export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspaceRoot) {
361
+ export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspaceRoot, platform = process.platform) {
278
362
  const dirtyFiles = [];
279
- const dirtyPathSet = new Set(workspaceSnapshot.dirty_paths);
363
+ const dirtyPathSet = new Map();
364
+ for (const path of workspaceSnapshot.dirty_paths)
365
+ addSnapshotPath(dirtyPathSet, path, platform);
280
366
  for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
281
367
  const absolutePath = resolve(workspaceRoot, path);
282
368
  const rel = relative(repoPath, absolutePath);
@@ -286,7 +372,7 @@ export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspace
286
372
  // 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
287
373
  // 2. It's not tracked by git (untracked file), OR
288
374
  // 3. It's explicitly ignored
289
- const isDirty = dirtyPathSet.has(path);
375
+ const isDirty = hasSnapshotPath(dirtyPathSet, path, platform);
290
376
  const isUntracked = !fingerprint.tracked;
291
377
  const isIgnored = fingerprint.ignored;
292
378
  if (isDirty || isUntracked || isIgnored) {
@@ -306,10 +392,17 @@ export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspace
306
392
  * Returns files that are NEW (not present in baseline) or CHANGED
307
393
  * (same path but different sha256, meaning the task modified them).
308
394
  */
309
- export function findNewExternalDirtyFiles(baseline, current) {
310
- const baselineMap = new Map(baseline.map((f) => [f.path, f]));
395
+ export function findNewExternalDirtyFiles(baseline, current, platform = process.platform) {
396
+ const baselineMap = new Map();
397
+ for (const file of baseline) {
398
+ const key = comparableSnapshotPath(file.path, platform);
399
+ baselineMap.set(key, [...(baselineMap.get(key) ?? []), file]);
400
+ }
311
401
  return current.filter((f) => {
312
- const baselineFile = baselineMap.get(f.path);
402
+ const candidates = baselineMap.get(comparableSnapshotPath(f.path, platform)) ?? [];
403
+ const baselineFile = candidates.length <= 1
404
+ ? candidates[0]
405
+ : candidates.find((candidate) => normalizePath(candidate.path) === normalizePath(f.path));
313
406
  if (!baselineFile)
314
407
  return true; // New path — definitely new
315
408
  // Same path but content changed during task execution
@@ -318,7 +411,7 @@ export function findNewExternalDirtyFiles(baseline, current) {
318
411
  return false;
319
412
  });
320
413
  }
321
- export function buildArtifactManifest(changedFiles, repoPath, taskId) {
414
+ export async function buildArtifactManifest(changedFiles, repoPath, taskId) {
322
415
  const entries = [];
323
416
  for (const file of changedFiles) {
324
417
  if (file.kind !== "build_artifact")
@@ -331,7 +424,7 @@ export function buildArtifactManifest(changedFiles, repoPath, taskId) {
331
424
  if (stat.isFile()) {
332
425
  size = stat.size;
333
426
  if (size <= MAX_HASH_BYTES) {
334
- sha256 = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
427
+ sha256 = await hashFileAsync(absolutePath);
335
428
  }
336
429
  }
337
430
  }
@@ -475,9 +568,72 @@ function classificationReason(change) {
475
568
  function normalizePath(value) {
476
569
  return value.replace(/\\/g, "/");
477
570
  }
478
- function walkWorkspace(root) {
571
+ function comparableSnapshotPath(value, platform = process.platform) {
572
+ const normalized = normalizePath(value);
573
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
574
+ }
575
+ function addSnapshotPath(index, value, platform = process.platform) {
576
+ const normalized = normalizePath(value);
577
+ const key = comparableSnapshotPath(normalized, platform);
578
+ const exactPaths = index.get(key) ?? new Set();
579
+ exactPaths.add(normalized);
580
+ index.set(key, exactPaths);
581
+ }
582
+ function hasSnapshotPath(index, value, platform = process.platform) {
583
+ const normalized = normalizePath(value);
584
+ const exactPaths = index.get(comparableSnapshotPath(normalized, platform));
585
+ if (!exactPaths)
586
+ return false;
587
+ return platform !== "win32" || exactPaths.size <= 1 || exactPaths.has(normalized);
588
+ }
589
+ function pairSnapshotFiles(before, after, platform) {
590
+ const beforeGroups = groupSnapshotFiles(before, platform);
591
+ const afterGroups = groupSnapshotFiles(after, platform);
592
+ const pairs = [];
593
+ const comparablePaths = [...new Set([...beforeGroups.keys(), ...afterGroups.keys()])].sort();
594
+ for (const comparablePath of comparablePaths) {
595
+ const beforeEntries = beforeGroups.get(comparablePath) ?? [];
596
+ const afterEntries = afterGroups.get(comparablePath) ?? [];
597
+ if (platform !== "win32" || (beforeEntries.length <= 1 && afterEntries.length <= 1)) {
598
+ pairs.push({
599
+ path: afterEntries[0]?.path ?? beforeEntries[0].path,
600
+ leftPath: beforeEntries[0]?.path,
601
+ rightPath: afterEntries[0]?.path,
602
+ left: beforeEntries[0]?.fingerprint,
603
+ right: afterEntries[0]?.fingerprint,
604
+ });
605
+ continue;
606
+ }
607
+ // NTFS can opt individual directories into case-sensitive mode. If either
608
+ // snapshot contains a case collision, preserve every exact path instead of
609
+ // silently dropping one entry from the evidence.
610
+ const beforeExact = new Map(beforeEntries.map((entry) => [normalizePath(entry.path), entry]));
611
+ const afterExact = new Map(afterEntries.map((entry) => [normalizePath(entry.path), entry]));
612
+ const exactPaths = [...new Set([...beforeExact.keys(), ...afterExact.keys()])].sort();
613
+ for (const exactPath of exactPaths) {
614
+ pairs.push({
615
+ path: afterExact.get(exactPath)?.path ?? beforeExact.get(exactPath).path,
616
+ leftPath: beforeExact.get(exactPath)?.path,
617
+ rightPath: afterExact.get(exactPath)?.path,
618
+ left: beforeExact.get(exactPath)?.fingerprint,
619
+ right: afterExact.get(exactPath)?.fingerprint,
620
+ });
621
+ }
622
+ }
623
+ return pairs;
624
+ }
625
+ function groupSnapshotFiles(files, platform) {
626
+ const grouped = new Map();
627
+ for (const [path, fingerprint] of Object.entries(files)) {
628
+ const key = comparableSnapshotPath(path, platform);
629
+ grouped.set(key, [...(grouped.get(key) ?? []), { path, fingerprint }]);
630
+ }
631
+ return grouped;
632
+ }
633
+ function walkWorkspace(root, signal) {
479
634
  const result = [];
480
635
  const visit = (directory) => {
636
+ throwIfAborted(signal);
481
637
  if (result.length >= MAX_SNAPSHOT_FILES)
482
638
  return;
483
639
  let entries;
@@ -488,6 +644,7 @@ function walkWorkspace(root) {
488
644
  return;
489
645
  }
490
646
  for (const entry of entries) {
647
+ throwIfAborted(signal);
491
648
  if (result.length >= MAX_SNAPSHOT_FILES)
492
649
  break;
493
650
  if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name))
@@ -502,16 +659,65 @@ function walkWorkspace(root) {
502
659
  visit(root);
503
660
  return result;
504
661
  }
505
- function runGit(repoPath, args) {
506
- const result = spawnSync("git", args, {
507
- cwd: repoPath,
508
- encoding: "utf-8",
509
- timeout: 30_000,
510
- maxBuffer: MAX_DIFF_BYTES,
662
+ function runGit(repoPath, args, signal) {
663
+ throwIfAborted(signal);
664
+ return new Promise((resolve, reject) => {
665
+ const env = buildGitEnvironment(repoPath);
666
+ const git = resolveTrustedExecutable("git", repoPath, { pathValue: env.PATH });
667
+ execFile(git, args, {
668
+ cwd: repoPath,
669
+ encoding: "utf-8",
670
+ timeout: 30_000,
671
+ maxBuffer: MAX_DIFF_BYTES,
672
+ windowsHide: true,
673
+ env,
674
+ signal,
675
+ }, (err, stdout, stderr) => {
676
+ if (signal?.aborted) {
677
+ reject(abortReason(signal));
678
+ return;
679
+ }
680
+ let status;
681
+ if (err) {
682
+ // err.code is the exit code (number) when the process exited with a non-zero status;
683
+ // it is a string (e.g. "ENOENT") when spawning failed or the buffer limit was hit.
684
+ status = typeof err.code === "number" ? err.code : null;
685
+ }
686
+ else {
687
+ status = 0;
688
+ }
689
+ const truncated = Boolean(err
690
+ && typeof err.code === "string"
691
+ && err.code.includes("MAXBUFFER"));
692
+ resolve({ status, stdout: stdout || "", stderr: stderr || "", truncated });
693
+ });
694
+ });
695
+ }
696
+ function throwIfAborted(signal) {
697
+ if (signal?.aborted)
698
+ throw abortReason(signal);
699
+ }
700
+ function abortReason(signal) {
701
+ return signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
702
+ }
703
+ function utf8Prefix(value, maxBytes) {
704
+ if (maxBytes <= 0)
705
+ return "";
706
+ const encoded = Buffer.from(value, "utf-8");
707
+ if (encoded.length <= maxBytes)
708
+ return value;
709
+ let prefix = encoded.subarray(0, maxBytes).toString("utf-8").replace(/\uFFFD$/u, "");
710
+ while (Buffer.byteLength(prefix, "utf-8") > maxBytes)
711
+ prefix = prefix.slice(0, -1);
712
+ return prefix;
713
+ }
714
+ function hashFileAsync(filePath) {
715
+ return new Promise((resolve, reject) => {
716
+ const hash = createHash("sha256");
717
+ const stream = createReadStream(filePath);
718
+ stream.on("error", reject);
719
+ hash.on("error", reject);
720
+ hash.on("finish", () => resolve(hash.digest("hex")));
721
+ stream.pipe(hash);
511
722
  });
512
- return {
513
- status: result.status,
514
- stdout: result.stdout || "",
515
- stderr: result.stderr || "",
516
- };
517
723
  }
@@ -8,19 +8,20 @@
8
8
  */
9
9
  import { runTask } from "./runTask.js";
10
10
  import { loadConfig } from "../config.js";
11
+ import { logger } from "../logging.js";
11
12
  // Load config early
12
13
  loadConfig();
13
14
  const taskId = process.argv[2] || process.env.PATCHWARDEN_TASK_ID;
14
15
  if (!taskId) {
15
- console.error("Usage: node dist/runner/cli.js <task_id>");
16
- console.error(" or: npm run runner -- <task_id>");
16
+ logger.info("Usage: node dist/runner/cli.js <task_id>");
17
+ logger.info(" or: npm run runner -- <task_id>");
17
18
  process.exit(1);
18
19
  }
19
- console.error(`[runner] Starting task: ${taskId}`);
20
+ logger.info(`[runner] Starting task: ${taskId}`);
20
21
  const result = await runTask(taskId);
21
- console.error(`[runner] Task ${result.task_id}: ${result.status}`);
22
+ logger.info(`[runner] Task ${result.task_id}: ${result.status}`);
22
23
  if (result.error) {
23
- console.error(`[runner] Error: ${result.error}`);
24
+ logger.fatal(`[runner] Error: ${result.error}`);
24
25
  process.exit(1);
25
26
  }
26
- console.error("[runner] Done.");
27
+ logger.info("[runner] Done.");