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,19 +1,24 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
+ createReadStream,
3
4
  existsSync,
4
5
  lstatSync,
5
6
  readFileSync,
6
7
  readdirSync,
7
8
  statSync,
8
- writeFileSync,
9
9
  } from "node:fs";
10
10
  import { join, relative, resolve, isAbsolute } from "node:path";
11
- import { spawnSync } from "node:child_process";
11
+ import { execFile } from "node:child_process";
12
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
12
13
  import { isSensitivePath } from "../security/sensitiveGuard.js";
14
+ import { nullDevice } from "../utils/platform.js";
15
+ import { buildGitEnvironment, resolveTrustedExecutable } from "./processSecurity.js";
16
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
13
17
 
14
18
  const MAX_HASH_BYTES = 5 * 1024 * 1024;
15
19
  const MAX_SNAPSHOT_FILES = 5000;
16
20
  const MAX_DIFF_BYTES = 20 * 1024 * 1024;
21
+ const DIFF_TRUNCATION_MARKER = "\n[PATCHWARDEN DIFF TRUNCATED]\n";
17
22
  const SKIP_DIRECTORIES = new Set([".git", ".patchwarden", "node_modules"]);
18
23
 
19
24
  export interface FileFingerprint {
@@ -74,6 +79,8 @@ export interface ChangeArtifacts {
74
79
  diff: string;
75
80
  diff_available: boolean;
76
81
  diff_truncated: boolean;
82
+ diff_redacted?: boolean;
83
+ diff_redaction_categories?: string[];
77
84
  diff_size_bytes: number;
78
85
  additions: number;
79
86
  deletions: number;
@@ -90,20 +97,30 @@ export interface ChangeArtifacts {
90
97
  artifact_hygiene: ArtifactHygiene;
91
98
  }
92
99
 
93
- export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
100
+ export async function captureRepoSnapshot(repoPath: string, signal?: AbortSignal): Promise<RepoSnapshot> {
101
+ throwIfAborted(signal);
94
102
  const warnings: string[] = [];
95
- const isGit = runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]).stdout.trim() === "true";
103
+ const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"], signal);
104
+ const isGit = isGitResult.stdout.trim() === "true";
96
105
  let head: string | null = null;
97
106
  let status = "";
98
107
  let paths: string[] = [];
99
- const trackedPaths = new Set<string>();
100
- const ignoredPaths = new Set<string>();
108
+ const trackedPaths = new Map<string, Set<string>>();
109
+ const ignoredPaths = new Map<string, Set<string>>();
101
110
 
102
111
  const dirtyPaths = new Set<string>();
103
112
  if (isGit) {
104
- const headResult = runGit(repoPath, ["rev-parse", "HEAD"]);
113
+ // The following five git calls are independent of each other and can run in parallel.
114
+ const [headResult, statusResult, trackedResult, ignoredResult, listedResult] = await Promise.all([
115
+ runGit(repoPath, ["rev-parse", "HEAD"], signal),
116
+ runGit(repoPath, ["status", "--porcelain=v1", "-uall"], signal),
117
+ runGit(repoPath, ["ls-files", "-z"], signal),
118
+ runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"], signal),
119
+ runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"], signal),
120
+ ]);
121
+
105
122
  if (headResult.status === 0) head = headResult.stdout.trim() || null;
106
- status = runGit(repoPath, ["status", "--porcelain=v1", "-uall"]).stdout.trimEnd();
123
+ status = statusResult.stdout.trimEnd();
107
124
  // Parse git status --porcelain to collect all dirty paths
108
125
  for (const line of status.split("\n")) {
109
126
  if (line.length < 4) continue;
@@ -125,29 +142,26 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
125
142
  }
126
143
  }
127
144
  }
128
- const tracked = runGit(repoPath, ["ls-files", "-z"]);
129
- if (tracked.status === 0) {
130
- for (const path of tracked.stdout.split("\0").filter(Boolean)) trackedPaths.add(normalizePath(path));
145
+ if (trackedResult.status === 0) {
146
+ for (const path of trackedResult.stdout.split("\0").filter(Boolean)) addSnapshotPath(trackedPaths, path);
131
147
  }
132
- const ignored = runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]);
133
- if (ignored.status === 0) {
134
- for (const path of ignored.stdout.split("\0").filter(Boolean)) ignoredPaths.add(normalizePath(path));
148
+ if (ignoredResult.status === 0) {
149
+ for (const path of ignoredResult.stdout.split("\0").filter(Boolean)) addSnapshotPath(ignoredPaths, path);
135
150
  } else {
136
151
  warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
137
152
  }
138
- const listed = runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]);
139
- if (listed.status === 0) {
153
+ if (listedResult.status === 0) {
140
154
  paths = [...new Set([
141
- ...listed.stdout.split("\0").filter(Boolean),
142
- ...walkWorkspace(repoPath),
155
+ ...listedResult.stdout.split("\0").filter(Boolean),
156
+ ...walkWorkspace(repoPath, signal),
143
157
  ])];
144
158
  } else {
145
159
  warnings.push("git ls-files failed; using bounded filesystem scan");
146
- paths = walkWorkspace(repoPath);
160
+ paths = walkWorkspace(repoPath, signal);
147
161
  }
148
162
  } else {
149
163
  warnings.push("repository is not a Git worktree; diff will contain file-change evidence only");
150
- paths = walkWorkspace(repoPath);
164
+ paths = walkWorkspace(repoPath, signal);
151
165
  }
152
166
 
153
167
  if (paths.length > MAX_SNAPSHOT_FILES) {
@@ -157,6 +171,7 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
157
171
 
158
172
  const files: Record<string, FileFingerprint> = {};
159
173
  for (const inputPath of paths.sort()) {
174
+ throwIfAborted(signal);
160
175
  const normalized = normalizePath(inputPath);
161
176
  if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized)) continue;
162
177
  const absolutePath = resolve(repoPath, inputPath);
@@ -169,8 +184,8 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
169
184
  files[normalized] = {
170
185
  size: stat.size,
171
186
  sha256,
172
- tracked: trackedPaths.has(normalized),
173
- ignored: !trackedPaths.has(normalized) && ignoredPaths.has(normalized),
187
+ tracked: hasSnapshotPath(trackedPaths, normalized),
188
+ ignored: !hasSnapshotPath(trackedPaths, normalized) && hasSnapshotPath(ignoredPaths, normalized),
174
189
  };
175
190
  } catch {
176
191
  warnings.push(`could not fingerprint: ${normalized}`);
@@ -190,55 +205,96 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
190
205
  }
191
206
 
192
207
  export function writeSnapshot(taskDir: string, filename: string, snapshot: RepoSnapshot): void {
193
- writeFileSync(join(taskDir, filename), JSON.stringify(snapshot, null, 2), "utf-8");
208
+ atomicWriteJsonFileSync(join(taskDir, filename), snapshot);
194
209
  }
195
210
 
196
- export function buildChangeArtifacts(
211
+ export async function buildChangeArtifacts(
197
212
  repoPath: string,
198
213
  before: RepoSnapshot,
199
- after: RepoSnapshot
200
- ): ChangeArtifacts {
214
+ after: RepoSnapshot,
215
+ signal?: AbortSignal,
216
+ ): Promise<ChangeArtifacts> {
217
+ throwIfAborted(signal);
201
218
  const changedFiles = compareSnapshots(before, after);
202
219
  const artifactHygiene = classifyArtifactHygiene(changedFiles);
203
220
  const sections: string[] = [];
204
221
  const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
222
+ const evidence = [
223
+ "# PatchWarden change evidence",
224
+ `# changed_files: ${changedFiles.length}`,
225
+ `# workspace_dirty_before: ${before.workspace_dirty}`,
226
+ `# workspace_dirty_after: ${after.workspace_dirty}`,
227
+ ...changedFiles.map((file) => `# ${file.change}: ${file.path}`),
228
+ ].join("\n");
229
+ let retainedSectionBytes = 0;
230
+ let diffTruncated = Buffer.byteLength(evidence, "utf-8") > MAX_DIFF_BYTES;
231
+ const sectionBudget = Math.max(
232
+ 0,
233
+ MAX_DIFF_BYTES
234
+ - Math.min(Buffer.byteLength(evidence, "utf-8"), MAX_DIFF_BYTES)
235
+ - Buffer.byteLength(DIFF_TRUNCATION_MARKER, "utf-8")
236
+ - 2,
237
+ );
238
+ const appendSection = (label: string, content: string, commandTruncated = false): void => {
239
+ if (!content.trim()) {
240
+ if (commandTruncated) diffTruncated = true;
241
+ return;
242
+ }
243
+ const block = `${sections.length > 0 ? "\n\n" : ""}${label}\n\n${content.trimEnd()}`;
244
+ const remaining = Math.max(0, sectionBudget - retainedSectionBytes);
245
+ const bounded = utf8Prefix(block, remaining);
246
+ if (bounded) {
247
+ sections.push(bounded);
248
+ retainedSectionBytes += Buffer.byteLength(bounded, "utf-8");
249
+ }
250
+ if (commandTruncated || Buffer.byteLength(bounded, "utf-8") < Buffer.byteLength(block, "utf-8")) {
251
+ diffTruncated = true;
252
+ }
253
+ };
205
254
 
206
255
  if (before.is_git && after.is_git && scopedPaths.length > 0) {
207
256
  if (before.head && after.head && before.head !== after.head) {
208
- const committed = runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
209
- if (committed.stdout.trim()) sections.push("# Changes committed during task\n", committed.stdout.trimEnd());
257
+ const committed = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths], signal);
258
+ appendSection("# Changes committed during task", committed.stdout, committed.truncated);
210
259
  }
211
260
 
212
- const base = after.head || "HEAD";
213
- const working = runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
214
- if (working.stdout.trim()) sections.push("# Staged and unstaged changes\n", working.stdout.trimEnd());
261
+ if (retainedSectionBytes < sectionBudget) {
262
+ const base = after.head || "HEAD";
263
+ const working = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", base, "--", ...scopedPaths], signal);
264
+ appendSection("# Staged and unstaged changes", working.stdout, working.truncated);
265
+ } else {
266
+ diffTruncated = true;
267
+ }
215
268
 
216
269
  for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
217
- const tracked = runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path]);
270
+ throwIfAborted(signal);
271
+ if (retainedSectionBytes >= sectionBudget) {
272
+ diffTruncated = true;
273
+ break;
274
+ }
275
+ const tracked = await runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path], signal);
218
276
  if (tracked.status === 0) continue;
219
- const untracked = runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
220
- if (untracked.stdout.trim()) sections.push("# Untracked file\n", untracked.stdout.trimEnd());
277
+ const untracked = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-index", "--no-color", "--binary", "--", nullDevice, file.path], signal);
278
+ appendSection("# Untracked file", untracked.stdout, untracked.truncated);
221
279
  }
222
280
  }
223
281
 
224
- const evidence = [
225
- "# PatchWarden change evidence",
226
- `# changed_files: ${changedFiles.length}`,
227
- `# workspace_dirty_before: ${before.workspace_dirty}`,
228
- `# workspace_dirty_after: ${after.workspace_dirty}`,
229
- ...changedFiles.map((file) => `# ${file.change}: ${file.path}`),
230
- ].join("\n");
231
- const body = sections.join("\n\n");
232
- const fullDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
282
+ const body = sections.join("");
283
+ const rawDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
284
+ const sanitized = sanitizeDiffEvidence(rawDiff, MAX_DIFF_BYTES);
285
+ const fullDiff = sanitized.content;
286
+ diffTruncated ||= sanitized.truncated;
233
287
  const additions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
234
288
  const deletions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
235
- const fileStats = buildFileStats(repoPath, before, after, changedFiles);
289
+ const fileStats = await buildFileStats(repoPath, before, after, changedFiles, signal);
236
290
 
237
291
  return {
238
292
  changed_files: changedFiles,
239
293
  diff: fullDiff,
240
294
  diff_available: changedFiles.length > 0,
241
- diff_truncated: false,
295
+ diff_truncated: diffTruncated,
296
+ diff_redacted: sanitized.redacted,
297
+ diff_redaction_categories: sanitized.redaction_categories,
242
298
  diff_size_bytes: Buffer.byteLength(fullDiff, "utf-8"),
243
299
  additions,
244
300
  deletions,
@@ -255,13 +311,47 @@ export function buildChangeArtifacts(
255
311
  };
256
312
  }
257
313
 
258
- function buildFileStats(
314
+ export function sanitizeDiffEvidence(
315
+ input: string,
316
+ maxBytes = MAX_DIFF_BYTES,
317
+ ): {
318
+ content: string;
319
+ truncated: boolean;
320
+ redacted: boolean;
321
+ redaction_categories: string[];
322
+ } {
323
+ if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
324
+ throw new Error("maxBytes must be a positive integer");
325
+ }
326
+ const redaction = redactSensitiveContent(input);
327
+ const encodedBytes = Buffer.byteLength(redaction.content, "utf-8");
328
+ if (encodedBytes <= maxBytes) {
329
+ return {
330
+ content: redaction.content,
331
+ truncated: false,
332
+ redacted: redaction.redacted,
333
+ redaction_categories: redaction.redaction_categories,
334
+ };
335
+ }
336
+ const marker = utf8Prefix(DIFF_TRUNCATION_MARKER, maxBytes);
337
+ const markerBytes = Buffer.byteLength(marker, "utf-8");
338
+ return {
339
+ content: utf8Prefix(redaction.content, Math.max(0, maxBytes - markerBytes)) + marker,
340
+ truncated: true,
341
+ redacted: redaction.redacted,
342
+ redaction_categories: redaction.redaction_categories,
343
+ };
344
+ }
345
+
346
+ async function buildFileStats(
259
347
  repoPath: string,
260
348
  before: RepoSnapshot,
261
349
  after: RepoSnapshot,
262
- changedFiles: ChangedFile[]
263
- ): ChangeArtifacts["file_stats"] {
264
- return changedFiles.map((file) => {
350
+ changedFiles: ChangedFile[],
351
+ signal?: AbortSignal,
352
+ ): Promise<ChangeArtifacts["file_stats"]> {
353
+ const results = await Promise.all(changedFiles.map(async (file) => {
354
+ throwIfAborted(signal);
265
355
  let additions = 0;
266
356
  let deletions = 0;
267
357
  const paths = file.old_path ? [file.old_path, file.path] : [file.path];
@@ -272,8 +362,10 @@ function buildFileStats(
272
362
  ranges.push([before.head, after.head]);
273
363
  }
274
364
  ranges.push([after.head || "HEAD"]);
275
- for (const range of ranges) {
276
- const result = runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths]);
365
+ const rangeResults = await Promise.all(
366
+ ranges.map((range) => runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--numstat", ...range, "--", ...paths], signal))
367
+ );
368
+ for (const result of rangeResults) {
277
369
  for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
278
370
  const [added, removed] = line.split(/\s+/);
279
371
  if (/^\d+$/.test(added)) additions += Number(added);
@@ -286,11 +378,12 @@ function buildFileStats(
286
378
  try {
287
379
  const content = readFileSync(resolve(repoPath, file.path), "utf-8");
288
380
  additions = countLines(content);
289
- } catch {}
381
+ } catch {} // probe failure handled by return value (additions stays 0)
290
382
  }
291
383
 
292
384
  return { path: file.path, status: file.change, additions, deletions };
293
- });
385
+ }));
386
+ return results;
294
387
  }
295
388
 
296
389
  function countLines(content: string): number {
@@ -298,17 +391,31 @@ function countLines(content: string): number {
298
391
  return content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
299
392
  }
300
393
 
301
- export function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): ChangedFile[] {
302
- const paths = [...new Set([...Object.keys(before.files), ...Object.keys(after.files)])].sort();
394
+ export function compareSnapshots(
395
+ before: RepoSnapshot,
396
+ after: RepoSnapshot,
397
+ platform: NodeJS.Platform = process.platform,
398
+ ): ChangedFile[] {
303
399
  const changed: ChangedFile[] = [];
304
- for (const path of paths) {
305
- const left = before.files[path];
306
- const right = after.files[path];
400
+ for (const { path, leftPath, rightPath, left, right } of pairSnapshotFiles(before.files, after.files, platform)) {
307
401
  if (!left && right) {
308
402
  changed.push(classifyChangedFile(path, "added", null, right));
309
403
  } else if (left && !right) {
310
404
  changed.push(classifyChangedFile(path, "deleted", left, null));
311
- } else if (left.sha256 !== right.sha256) {
405
+ } else if (left && right && left.sha256 === right.sha256 && leftPath !== rightPath) {
406
+ // Windows lookup is case-insensitive, but a case-only Git rename is still
407
+ // auditable work and must not disappear from the evidence set.
408
+ changed.push({
409
+ path: rightPath!,
410
+ old_path: leftPath!,
411
+ change: "renamed",
412
+ before_sha256: left.sha256,
413
+ after_sha256: right.sha256,
414
+ tracked: left.tracked || right.tracked,
415
+ ignored: right.ignored,
416
+ kind: classifyPathKind(rightPath!),
417
+ });
418
+ } else if (left && right && left.sha256 !== right.sha256) {
312
419
  changed.push(classifyChangedFile(path, "modified", left, right));
313
420
  }
314
421
  }
@@ -376,10 +483,12 @@ export interface ExternalDirtyFile {
376
483
  export function extractExternalDirtyFiles(
377
484
  workspaceSnapshot: RepoSnapshot,
378
485
  repoPath: string,
379
- workspaceRoot: string
486
+ workspaceRoot: string,
487
+ platform: NodeJS.Platform = process.platform,
380
488
  ): ExternalDirtyFile[] {
381
489
  const dirtyFiles: ExternalDirtyFile[] = [];
382
- const dirtyPathSet = new Set(workspaceSnapshot.dirty_paths);
490
+ const dirtyPathSet = new Map<string, Set<string>>();
491
+ for (const path of workspaceSnapshot.dirty_paths) addSnapshotPath(dirtyPathSet, path, platform);
383
492
  for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
384
493
  const absolutePath = resolve(workspaceRoot, path);
385
494
  const rel = relative(repoPath, absolutePath);
@@ -389,7 +498,7 @@ export function extractExternalDirtyFiles(
389
498
  // 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
390
499
  // 2. It's not tracked by git (untracked file), OR
391
500
  // 3. It's explicitly ignored
392
- const isDirty = dirtyPathSet.has(path);
501
+ const isDirty = hasSnapshotPath(dirtyPathSet, path, platform);
393
502
  const isUntracked = !fingerprint.tracked;
394
503
  const isIgnored = fingerprint.ignored;
395
504
  if (isDirty || isUntracked || isIgnored) {
@@ -412,11 +521,19 @@ export function extractExternalDirtyFiles(
412
521
  */
413
522
  export function findNewExternalDirtyFiles(
414
523
  baseline: ExternalDirtyFile[],
415
- current: ExternalDirtyFile[]
524
+ current: ExternalDirtyFile[],
525
+ platform: NodeJS.Platform = process.platform,
416
526
  ): ExternalDirtyFile[] {
417
- const baselineMap = new Map(baseline.map((f) => [f.path, f]));
527
+ const baselineMap = new Map<string, ExternalDirtyFile[]>();
528
+ for (const file of baseline) {
529
+ const key = comparableSnapshotPath(file.path, platform);
530
+ baselineMap.set(key, [...(baselineMap.get(key) ?? []), file]);
531
+ }
418
532
  return current.filter((f) => {
419
- const baselineFile = baselineMap.get(f.path);
533
+ const candidates = baselineMap.get(comparableSnapshotPath(f.path, platform)) ?? [];
534
+ const baselineFile = candidates.length <= 1
535
+ ? candidates[0]
536
+ : candidates.find((candidate) => normalizePath(candidate.path) === normalizePath(f.path));
420
537
  if (!baselineFile) return true; // New path — definitely new
421
538
  // Same path but content changed during task execution
422
539
  if (baselineFile.before_sha256 !== f.before_sha256) return true;
@@ -441,11 +558,11 @@ export interface ArtifactManifest {
441
558
  artifacts: ArtifactManifestEntry[];
442
559
  }
443
560
 
444
- export function buildArtifactManifest(
561
+ export async function buildArtifactManifest(
445
562
  changedFiles: ChangedFile[],
446
563
  repoPath: string,
447
564
  taskId?: string
448
- ): ArtifactManifest {
565
+ ): Promise<ArtifactManifest> {
449
566
  const entries: ArtifactManifestEntry[] = [];
450
567
  for (const file of changedFiles) {
451
568
  if (file.kind !== "build_artifact") continue;
@@ -457,7 +574,7 @@ export function buildArtifactManifest(
457
574
  if (stat.isFile()) {
458
575
  size = stat.size;
459
576
  if (size <= MAX_HASH_BYTES) {
460
- sha256 = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
577
+ sha256 = await hashFileAsync(absolutePath);
461
578
  }
462
579
  }
463
580
  } catch {
@@ -613,9 +730,98 @@ function normalizePath(value: string): string {
613
730
  return value.replace(/\\/g, "/");
614
731
  }
615
732
 
616
- function walkWorkspace(root: string): string[] {
733
+ function comparableSnapshotPath(value: string, platform: NodeJS.Platform = process.platform): string {
734
+ const normalized = normalizePath(value);
735
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
736
+ }
737
+
738
+ function addSnapshotPath(
739
+ index: Map<string, Set<string>>,
740
+ value: string,
741
+ platform: NodeJS.Platform = process.platform,
742
+ ): void {
743
+ const normalized = normalizePath(value);
744
+ const key = comparableSnapshotPath(normalized, platform);
745
+ const exactPaths = index.get(key) ?? new Set<string>();
746
+ exactPaths.add(normalized);
747
+ index.set(key, exactPaths);
748
+ }
749
+
750
+ function hasSnapshotPath(
751
+ index: Map<string, Set<string>>,
752
+ value: string,
753
+ platform: NodeJS.Platform = process.platform,
754
+ ): boolean {
755
+ const normalized = normalizePath(value);
756
+ const exactPaths = index.get(comparableSnapshotPath(normalized, platform));
757
+ if (!exactPaths) return false;
758
+ return platform !== "win32" || exactPaths.size <= 1 || exactPaths.has(normalized);
759
+ }
760
+
761
+ interface SnapshotFileEntry {
762
+ path: string;
763
+ fingerprint: FileFingerprint;
764
+ }
765
+
766
+ function pairSnapshotFiles(
767
+ before: Record<string, FileFingerprint>,
768
+ after: Record<string, FileFingerprint>,
769
+ platform: NodeJS.Platform,
770
+ ): Array<{ path: string; leftPath?: string; rightPath?: string; left?: FileFingerprint; right?: FileFingerprint }> {
771
+ const beforeGroups = groupSnapshotFiles(before, platform);
772
+ const afterGroups = groupSnapshotFiles(after, platform);
773
+ const pairs: Array<{ path: string; leftPath?: string; rightPath?: string; left?: FileFingerprint; right?: FileFingerprint }> = [];
774
+ const comparablePaths = [...new Set([...beforeGroups.keys(), ...afterGroups.keys()])].sort();
775
+
776
+ for (const comparablePath of comparablePaths) {
777
+ const beforeEntries = beforeGroups.get(comparablePath) ?? [];
778
+ const afterEntries = afterGroups.get(comparablePath) ?? [];
779
+ if (platform !== "win32" || (beforeEntries.length <= 1 && afterEntries.length <= 1)) {
780
+ pairs.push({
781
+ path: afterEntries[0]?.path ?? beforeEntries[0].path,
782
+ leftPath: beforeEntries[0]?.path,
783
+ rightPath: afterEntries[0]?.path,
784
+ left: beforeEntries[0]?.fingerprint,
785
+ right: afterEntries[0]?.fingerprint,
786
+ });
787
+ continue;
788
+ }
789
+
790
+ // NTFS can opt individual directories into case-sensitive mode. If either
791
+ // snapshot contains a case collision, preserve every exact path instead of
792
+ // silently dropping one entry from the evidence.
793
+ const beforeExact = new Map(beforeEntries.map((entry) => [normalizePath(entry.path), entry]));
794
+ const afterExact = new Map(afterEntries.map((entry) => [normalizePath(entry.path), entry]));
795
+ const exactPaths = [...new Set([...beforeExact.keys(), ...afterExact.keys()])].sort();
796
+ for (const exactPath of exactPaths) {
797
+ pairs.push({
798
+ path: afterExact.get(exactPath)?.path ?? beforeExact.get(exactPath)!.path,
799
+ leftPath: beforeExact.get(exactPath)?.path,
800
+ rightPath: afterExact.get(exactPath)?.path,
801
+ left: beforeExact.get(exactPath)?.fingerprint,
802
+ right: afterExact.get(exactPath)?.fingerprint,
803
+ });
804
+ }
805
+ }
806
+ return pairs;
807
+ }
808
+
809
+ function groupSnapshotFiles(
810
+ files: Record<string, FileFingerprint>,
811
+ platform: NodeJS.Platform,
812
+ ): Map<string, SnapshotFileEntry[]> {
813
+ const grouped = new Map<string, SnapshotFileEntry[]>();
814
+ for (const [path, fingerprint] of Object.entries(files)) {
815
+ const key = comparableSnapshotPath(path, platform);
816
+ grouped.set(key, [...(grouped.get(key) ?? []), { path, fingerprint }]);
817
+ }
818
+ return grouped;
819
+ }
820
+
821
+ function walkWorkspace(root: string, signal?: AbortSignal): string[] {
617
822
  const result: string[] = [];
618
823
  const visit = (directory: string) => {
824
+ throwIfAborted(signal);
619
825
  if (result.length >= MAX_SNAPSHOT_FILES) return;
620
826
  let entries;
621
827
  try {
@@ -624,6 +830,7 @@ function walkWorkspace(root: string): string[] {
624
830
  return;
625
831
  }
626
832
  for (const entry of entries) {
833
+ throwIfAborted(signal);
627
834
  if (result.length >= MAX_SNAPSHOT_FILES) break;
628
835
  if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
629
836
  const absolute = join(directory, entry.name);
@@ -635,16 +842,71 @@ function walkWorkspace(root: string): string[] {
635
842
  return result;
636
843
  }
637
844
 
638
- function runGit(repoPath: string, args: string[]) {
639
- const result = spawnSync("git", args, {
640
- cwd: repoPath,
641
- encoding: "utf-8",
642
- timeout: 30_000,
643
- maxBuffer: MAX_DIFF_BYTES,
845
+ function runGit(repoPath: string, args: string[], signal?: AbortSignal): Promise<{
846
+ status: number | null;
847
+ stdout: string;
848
+ stderr: string;
849
+ truncated: boolean;
850
+ }> {
851
+ throwIfAborted(signal);
852
+ return new Promise((resolve, reject) => {
853
+ const env = buildGitEnvironment(repoPath);
854
+ const git = resolveTrustedExecutable("git", repoPath, { pathValue: env.PATH });
855
+ execFile(git, args, {
856
+ cwd: repoPath,
857
+ encoding: "utf-8",
858
+ timeout: 30_000,
859
+ maxBuffer: MAX_DIFF_BYTES,
860
+ windowsHide: true,
861
+ env,
862
+ signal,
863
+ }, (err, stdout, stderr) => {
864
+ if (signal?.aborted) {
865
+ reject(abortReason(signal));
866
+ return;
867
+ }
868
+ let status: number | null;
869
+ if (err) {
870
+ // err.code is the exit code (number) when the process exited with a non-zero status;
871
+ // it is a string (e.g. "ENOENT") when spawning failed or the buffer limit was hit.
872
+ status = typeof err.code === "number" ? err.code : null;
873
+ } else {
874
+ status = 0;
875
+ }
876
+ const truncated = Boolean(
877
+ err
878
+ && typeof err.code === "string"
879
+ && err.code.includes("MAXBUFFER"),
880
+ );
881
+ resolve({ status, stdout: stdout || "", stderr: stderr || "", truncated });
882
+ });
883
+ });
884
+ }
885
+
886
+ function throwIfAborted(signal?: AbortSignal): void {
887
+ if (signal?.aborted) throw abortReason(signal);
888
+ }
889
+
890
+ function abortReason(signal: AbortSignal): Error {
891
+ return signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
892
+ }
893
+
894
+ function utf8Prefix(value: string, maxBytes: number): string {
895
+ if (maxBytes <= 0) return "";
896
+ const encoded = Buffer.from(value, "utf-8");
897
+ if (encoded.length <= maxBytes) return value;
898
+ let prefix = encoded.subarray(0, maxBytes).toString("utf-8").replace(/\uFFFD$/u, "");
899
+ while (Buffer.byteLength(prefix, "utf-8") > maxBytes) prefix = prefix.slice(0, -1);
900
+ return prefix;
901
+ }
902
+
903
+ function hashFileAsync(filePath: string): Promise<string> {
904
+ return new Promise((resolve, reject) => {
905
+ const hash = createHash("sha256");
906
+ const stream = createReadStream(filePath);
907
+ stream.on("error", reject);
908
+ hash.on("error", reject);
909
+ hash.on("finish", () => resolve(hash.digest("hex")));
910
+ stream.pipe(hash);
644
911
  });
645
- return {
646
- status: result.status,
647
- stdout: result.stdout || "",
648
- stderr: result.stderr || "",
649
- };
650
912
  }
package/src/runner/cli.ts CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { runTask } from "./runTask.js";
11
11
  import { loadConfig } from "../config.js";
12
+ import { logger } from "../logging.js";
12
13
 
13
14
  // Load config early
14
15
  loadConfig();
@@ -16,19 +17,19 @@ loadConfig();
16
17
  const taskId = process.argv[2] || process.env.PATCHWARDEN_TASK_ID;
17
18
 
18
19
  if (!taskId) {
19
- console.error("Usage: node dist/runner/cli.js <task_id>");
20
- console.error(" or: npm run runner -- <task_id>");
20
+ logger.info("Usage: node dist/runner/cli.js <task_id>");
21
+ logger.info(" or: npm run runner -- <task_id>");
21
22
  process.exit(1);
22
23
  }
23
24
 
24
- console.error(`[runner] Starting task: ${taskId}`);
25
+ logger.info(`[runner] Starting task: ${taskId}`);
25
26
 
26
27
  const result = await runTask(taskId);
27
28
 
28
- console.error(`[runner] Task ${result.task_id}: ${result.status}`);
29
+ logger.info(`[runner] Task ${result.task_id}: ${result.status}`);
29
30
  if (result.error) {
30
- console.error(`[runner] Error: ${result.error}`);
31
+ logger.fatal(`[runner] Error: ${result.error}`);
31
32
  process.exit(1);
32
33
  }
33
34
 
34
- console.error("[runner] Done.");
35
+ logger.info("[runner] Done.");