patchwarden 1.5.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (591) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +158 -26
  3. package/README.md +124 -24
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
  6. package/dist/assessments/assessmentDiagnostics.js +132 -0
  7. package/dist/assessments/assessmentStore.d.ts +39 -0
  8. package/dist/assessments/assessmentStore.js +207 -45
  9. package/dist/assessments/securitySnapshot.d.ts +50 -0
  10. package/dist/assessments/securitySnapshot.js +158 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +66 -13
  13. package/dist/control/fileManager.d.ts +2 -0
  14. package/dist/control/fileManager.js +39 -0
  15. package/dist/control/middleware/auth.d.ts +6 -0
  16. package/dist/control/middleware/auth.js +14 -5
  17. package/dist/control/middleware/static.js +16 -3
  18. package/dist/control/routeTable.d.ts +24 -0
  19. package/dist/control/routeTable.js +263 -0
  20. package/dist/control/routes/audit.js +17 -7
  21. package/dist/control/routes/evidence.js +1 -1
  22. package/dist/control/routes/lineage.js +1 -1
  23. package/dist/control/routes/process.d.ts +7 -0
  24. package/dist/control/routes/process.js +329 -42
  25. package/dist/control/routes/sessions.d.ts +9 -1
  26. package/dist/control/routes/sessions.js +84 -21
  27. package/dist/control/routes/status.d.ts +60 -1
  28. package/dist/control/routes/status.js +94 -15
  29. package/dist/control/routes/taskActions.js +24 -60
  30. package/dist/control/routes/tasks.d.ts +4 -0
  31. package/dist/control/routes/tasks.js +61 -62
  32. package/dist/control/routes/workspace.js +50 -23
  33. package/dist/control/runtime.d.ts +3 -2
  34. package/dist/control/runtime.js +65 -40
  35. package/dist/control/server.js +37 -365
  36. package/dist/control/shared.d.ts +25 -1
  37. package/dist/control/shared.js +64 -11
  38. package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
  39. package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
  40. package/dist/direct/directAudit.js +234 -225
  41. package/dist/direct/directGuards.d.ts +2 -1
  42. package/dist/direct/directGuards.js +42 -29
  43. package/dist/direct/directPatch.d.ts +6 -1
  44. package/dist/direct/directPatch.js +55 -8
  45. package/dist/direct/directSessionStore.d.ts +16 -6
  46. package/dist/direct/directSessionStore.js +128 -35
  47. package/dist/direct/directVerification.js +8 -16
  48. package/dist/doctor.js +65 -57
  49. package/dist/goal/acceptanceEngine.d.ts +1 -1
  50. package/dist/goal/goalProgress.js +75 -69
  51. package/dist/goal/goalReport.js +5 -12
  52. package/dist/goal/goalStatus.d.ts +2 -0
  53. package/dist/goal/goalStatus.js +10 -3
  54. package/dist/goal/goalStore.d.ts +9 -2
  55. package/dist/goal/goalStore.js +152 -45
  56. package/dist/goal/handoffExport.js +6 -6
  57. package/dist/goal/specKitImport.js +97 -96
  58. package/dist/goal/subgoalSync.js +11 -9
  59. package/dist/goal/worktreeManager.d.ts +2 -0
  60. package/dist/goal/worktreeManager.js +209 -136
  61. package/dist/httpServer.js +111 -32
  62. package/dist/logging.js +5 -3
  63. package/dist/policy/projectPolicy.js +5 -2
  64. package/dist/release/releaseGate.d.ts +1 -1
  65. package/dist/release/releaseGate.js +78 -25
  66. package/dist/runner/agentInvocation.d.ts +14 -0
  67. package/dist/runner/agentInvocation.js +96 -4
  68. package/dist/runner/changeCapture.d.ts +13 -5
  69. package/dist/runner/changeCapture.js +243 -61
  70. package/dist/runner/postTaskCleanup.js +48 -19
  71. package/dist/runner/processSecurity.d.ts +56 -0
  72. package/dist/runner/processSecurity.js +373 -0
  73. package/dist/runner/runTask.d.ts +2 -1
  74. package/dist/runner/runTask.js +162 -93
  75. package/dist/runner/simpleProcess.d.ts +3 -0
  76. package/dist/runner/simpleProcess.js +50 -36
  77. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  78. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  79. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +3 -1
  80. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  81. package/dist/runner/taskStatusStore.d.ts +11 -0
  82. package/dist/runner/taskStatusStore.js +35 -0
  83. package/dist/runner/watch.d.ts +14 -1
  84. package/dist/runner/watch.js +320 -49
  85. package/dist/security/contentRedaction.js +2 -2
  86. package/dist/security/discoveryTokenStore.d.ts +3 -2
  87. package/dist/security/discoveryTokenStore.js +17 -2
  88. package/dist/security/loopbackHost.d.ts +2 -0
  89. package/dist/security/loopbackHost.js +16 -0
  90. package/dist/security/pathGuard.js +32 -17
  91. package/dist/security/planGuard.js +2 -1
  92. package/dist/security/runtimeGuard.js +9 -6
  93. package/dist/security/secretComparison.d.ts +3 -0
  94. package/dist/security/secretComparison.js +11 -0
  95. package/dist/security/sensitiveGuard.d.ts +5 -0
  96. package/dist/security/sensitiveGuard.js +27 -8
  97. package/dist/security/toolInvocationGuard.d.ts +2 -2
  98. package/dist/security/toolInvocationGuard.js +1 -1
  99. package/dist/security/workspaceRootGuard.d.ts +8 -0
  100. package/dist/security/workspaceRootGuard.js +47 -0
  101. package/dist/smoke-test.js +27 -26
  102. package/dist/tools/catalog/index.d.ts +4 -0
  103. package/dist/tools/catalog/index.js +4 -0
  104. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  105. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  106. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  107. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  108. package/dist/tools/definitions/toolDefs.js +1177 -0
  109. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  110. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  111. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +2 -1
  112. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
  113. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
  114. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
  115. package/dist/tools/diagnostics/index.d.ts +6 -0
  116. package/dist/tools/diagnostics/index.js +6 -0
  117. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  118. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  119. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  120. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  121. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  122. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  123. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  124. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  125. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  126. package/dist/tools/direct/index.d.ts +3 -0
  127. package/dist/tools/direct/index.js +3 -0
  128. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  129. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  130. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  131. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  132. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  133. package/dist/tools/discovery/index.d.ts +3 -0
  134. package/dist/tools/discovery/index.js +3 -0
  135. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  136. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  137. package/dist/tools/dispatch/coreDispatch.js +32 -31
  138. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  139. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  140. package/dist/tools/dispatch/directDispatch.js +11 -10
  141. package/dist/tools/dispatch/goalDispatch.js +5 -4
  142. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  143. package/dist/tools/dispatch/validation.d.ts +7 -0
  144. package/dist/tools/dispatch/validation.js +71 -0
  145. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  146. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  147. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  148. package/dist/tools/goals/index.d.ts +3 -0
  149. package/dist/tools/goals/index.js +3 -0
  150. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  151. package/dist/tools/registry.d.ts +7 -11
  152. package/dist/tools/registry.js +17 -1178
  153. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  154. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  155. package/dist/tools/release/index.d.ts +2 -0
  156. package/dist/tools/release/index.js +2 -0
  157. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  158. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  159. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  160. package/dist/tools/tasks/cancelTask.js +86 -0
  161. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  162. package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
  163. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  164. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
  165. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  166. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  167. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  168. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +11 -6
  169. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  170. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
  171. package/dist/tools/tasks/index.d.ts +18 -0
  172. package/dist/tools/tasks/index.js +18 -0
  173. package/dist/tools/tasks/killTask.d.ts +1 -0
  174. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  175. package/dist/tools/{listTasks.js → tasks/listTasks.js} +5 -4
  176. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
  177. package/dist/tools/tasks/reconcileTasks.js +496 -0
  178. package/dist/tools/tasks/retryTask.js +60 -0
  179. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  180. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +63 -41
  181. package/dist/tools/tasks/runVerification.js +38 -0
  182. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  183. package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
  184. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  185. package/dist/tools/tasks/taskStates.d.ts +6 -0
  186. package/dist/tools/tasks/taskStates.js +52 -0
  187. package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
  188. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  189. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  190. package/dist/tools/workspace/applyPatch.js +53 -0
  191. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  192. package/dist/tools/workspace/index.d.ts +10 -0
  193. package/dist/tools/workspace/index.js +10 -0
  194. package/dist/tools/workspace/listAgents.d.ts +23 -0
  195. package/dist/tools/workspace/listAgents.js +56 -0
  196. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  197. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  198. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  199. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  200. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  201. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  202. package/dist/tools/workspace/syncFile.js +134 -0
  203. package/dist/utils/atomicFile.d.ts +7 -0
  204. package/dist/utils/atomicFile.js +67 -0
  205. package/dist/utils/boundedFile.d.ts +8 -0
  206. package/dist/utils/boundedFile.js +93 -0
  207. package/dist/utils/lockedJsonFile.d.ts +15 -0
  208. package/dist/utils/lockedJsonFile.js +224 -0
  209. package/dist/utils/platform.d.ts +11 -0
  210. package/dist/utils/platform.js +26 -0
  211. package/dist/utils/stableJson.d.ts +2 -0
  212. package/dist/utils/stableJson.js +12 -0
  213. package/dist/version.d.ts +3 -2
  214. package/dist/version.js +3 -2
  215. package/dist/watcherStatus.d.ts +2 -2
  216. package/dist/watcherStatus.js +18 -0
  217. package/docs/CODE_WIKI.md +808 -143
  218. package/docs/architecture.md +15 -55
  219. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  220. package/docs/control-center/README.md +4 -4
  221. package/docs/dashboard-overview.md +4 -4
  222. package/docs/desktop-app.md +175 -0
  223. package/docs/evidence-pack-schema.md +3 -3
  224. package/docs/open-source-application.md +16 -11
  225. package/docs/release-checklist.md +1 -1
  226. package/docs/release-evidence.md +35 -39
  227. package/docs/task-safe-review-workflow.md +5 -5
  228. package/docs/threat-model.md +19 -1
  229. package/examples/config.example.json +4 -2
  230. package/package.json +27 -10
  231. package/scripts/README.md +15 -6
  232. package/scripts/build.js +11 -0
  233. package/scripts/checks/brand-check.js +3 -0
  234. package/scripts/checks/build-output-check.js +37 -0
  235. package/scripts/checks/control-center-smoke.js +102 -10
  236. package/scripts/checks/control-smoke.js +19 -8
  237. package/scripts/checks/http-mcp-smoke.js +82 -4
  238. package/scripts/checks/lifecycle-smoke.js +15 -12
  239. package/scripts/checks/mcp-manifest-check.js +1 -1
  240. package/scripts/checks/mcp-smoke.js +58 -4
  241. package/scripts/checks/package-manifest-check.js +20 -7
  242. package/scripts/checks/unit-tests.js +3 -5
  243. package/scripts/checks/watcher-supervisor-smoke.js +38 -1
  244. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  245. package/scripts/control/manage-patchwarden.ps1 +69 -4
  246. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  247. package/scripts/control/run-background-supervisor.ps1 +41 -0
  248. package/scripts/control/start-patchwarden-tunnel.ps1 +191 -33
  249. package/scripts/e2e/demo-runtime.mjs +153 -0
  250. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  251. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  252. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  253. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  254. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  255. package/scripts/lib/clean-generated-output.js +11 -0
  256. package/scripts/lib/file-discovery.js +12 -0
  257. package/scripts/release/desktop-preflight.js +211 -0
  258. package/scripts/release/pack-clean.js +59 -14
  259. package/src/assessments/agentAssessor.ts +26 -12
  260. package/src/assessments/assessmentDiagnostics.ts +172 -0
  261. package/src/assessments/assessmentStore.ts +288 -51
  262. package/src/assessments/securitySnapshot.ts +231 -0
  263. package/src/config.ts +70 -13
  264. package/src/control/fileManager.ts +40 -0
  265. package/src/control/middleware/auth.ts +22 -4
  266. package/src/control/middleware/static.ts +16 -3
  267. package/src/control/routeTable.ts +301 -0
  268. package/src/control/routes/audit.ts +14 -6
  269. package/src/control/routes/evidence.ts +1 -1
  270. package/src/control/routes/lineage.ts +1 -1
  271. package/src/control/routes/process.ts +341 -40
  272. package/src/control/routes/sessions.ts +88 -25
  273. package/src/control/routes/status.ts +134 -17
  274. package/src/control/routes/taskActions.ts +24 -58
  275. package/src/control/routes/tasks.ts +66 -58
  276. package/src/control/routes/workspace.ts +56 -25
  277. package/src/control/runtime.ts +71 -43
  278. package/src/control/server.ts +37 -342
  279. package/src/control/shared.ts +77 -11
  280. package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
  281. package/src/direct/directAudit.ts +17 -9
  282. package/src/direct/directGuards.ts +72 -44
  283. package/src/direct/directPatch.ts +95 -8
  284. package/src/direct/directSessionStore.ts +213 -55
  285. package/src/direct/directVerification.ts +9 -23
  286. package/src/doctor.ts +76 -64
  287. package/src/goal/acceptanceEngine.ts +1 -1
  288. package/src/goal/goalProgress.ts +17 -13
  289. package/src/goal/goalReport.ts +5 -13
  290. package/src/goal/goalStatus.ts +19 -3
  291. package/src/goal/goalStore.ts +241 -54
  292. package/src/goal/handoffExport.ts +6 -6
  293. package/src/goal/specKitImport.ts +12 -12
  294. package/src/goal/subgoalSync.ts +11 -8
  295. package/src/goal/worktreeManager.ts +139 -22
  296. package/src/httpServer.ts +113 -30
  297. package/src/logging.ts +10 -3
  298. package/src/policy/projectPolicy.ts +5 -2
  299. package/src/release/releaseGate.ts +74 -27
  300. package/src/runner/agentInvocation.ts +126 -4
  301. package/src/runner/changeCapture.ts +291 -57
  302. package/src/runner/postTaskCleanup.ts +41 -17
  303. package/src/runner/processSecurity.ts +433 -0
  304. package/src/runner/runTask.ts +197 -97
  305. package/src/runner/simpleProcess.ts +66 -31
  306. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  307. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
  308. package/src/runner/taskStatusStore.ts +69 -0
  309. package/src/runner/watch.ts +348 -52
  310. package/src/security/contentRedaction.ts +2 -2
  311. package/src/security/discoveryTokenStore.ts +18 -4
  312. package/src/security/loopbackHost.ts +19 -0
  313. package/src/security/pathGuard.ts +35 -16
  314. package/src/security/planGuard.ts +2 -1
  315. package/src/security/runtimeGuard.ts +9 -6
  316. package/src/security/secretComparison.ts +13 -0
  317. package/src/security/sensitiveGuard.ts +27 -8
  318. package/src/security/toolInvocationGuard.ts +2 -2
  319. package/src/security/workspaceRootGuard.ts +53 -0
  320. package/src/smoke-test.ts +27 -26
  321. package/src/tools/catalog/index.ts +4 -0
  322. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  323. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  324. package/src/tools/definitions/toolDefs.ts +1264 -0
  325. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  326. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +136 -39
  327. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
  328. package/src/tools/diagnostics/index.ts +6 -0
  329. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  330. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  331. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  332. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  333. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  334. package/src/tools/direct/index.ts +3 -0
  335. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  336. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  337. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  338. package/src/tools/discovery/index.ts +3 -0
  339. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  340. package/src/tools/dispatch/coreDispatch.ts +36 -31
  341. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  342. package/src/tools/dispatch/directDispatch.ts +11 -10
  343. package/src/tools/dispatch/goalDispatch.ts +5 -4
  344. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  345. package/src/tools/dispatch/validation.ts +78 -0
  346. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  347. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  348. package/src/tools/goals/index.ts +3 -0
  349. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  350. package/src/tools/registry.ts +17 -1261
  351. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  352. package/src/tools/release/index.ts +2 -0
  353. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  354. package/src/tools/tasks/cancelTask.ts +110 -0
  355. package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
  356. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
  357. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  358. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  359. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +13 -6
  360. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  361. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
  362. package/src/tools/tasks/index.ts +18 -0
  363. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
  364. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
  365. package/src/tools/tasks/retryTask.ts +69 -0
  366. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
  367. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  368. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  369. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  370. package/src/tools/tasks/taskStates.ts +54 -0
  371. package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
  372. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  373. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  374. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  375. package/src/tools/workspace/index.ts +10 -0
  376. package/src/tools/workspace/listAgents.ts +75 -0
  377. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  378. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  379. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  380. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  381. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  382. package/src/tools/workspace/syncFile.ts +292 -0
  383. package/src/utils/atomicFile.ts +74 -0
  384. package/src/utils/boundedFile.ts +116 -0
  385. package/src/utils/lockedJsonFile.ts +251 -0
  386. package/src/utils/platform.ts +31 -0
  387. package/src/utils/stableJson.ts +12 -0
  388. package/src/version.ts +3 -2
  389. package/src/watcherStatus.ts +19 -1
  390. package/ui/desktop-bootstrap.js +11 -0
  391. package/ui/desktop-bridge.js +36 -0
  392. package/ui/desktop.css +160 -0
  393. package/ui/getting-started.js +93 -0
  394. package/ui/i18n.js +484 -0
  395. package/ui/log-parser.js +30 -0
  396. package/ui/pages/audit.html +12 -0
  397. package/ui/pages/dashboard.html +94 -147
  398. package/ui/pages/direct-sessions.html +123 -26
  399. package/ui/pages/getting-started.html +43 -0
  400. package/ui/pages/logs.html +134 -22
  401. package/ui/pages/settings.html +101 -0
  402. package/ui/pages/task-detail.html +12 -0
  403. package/ui/pages/tasks.html +54 -8
  404. package/ui/pages/workspace.html +38 -1
  405. package/ui/settings.js +254 -0
  406. package/PatchWarden-Control.cmd +0 -6
  407. package/Restart-PatchWarden-Control.cmd +0 -6
  408. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  409. package/dist/test/unit/acceptance-engine.test.js +0 -228
  410. package/dist/test/unit/agent-router.test.d.ts +0 -1
  411. package/dist/test/unit/agent-router.test.js +0 -287
  412. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  413. package/dist/test/unit/android-doctor.test.js +0 -118
  414. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  415. package/dist/test/unit/apply-patch.test.js +0 -225
  416. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  417. package/dist/test/unit/audit-checks.test.js +0 -350
  418. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  419. package/dist/test/unit/chinese-path.test.js +0 -91
  420. package/dist/test/unit/command-guard.test.d.ts +0 -1
  421. package/dist/test/unit/command-guard.test.js +0 -160
  422. package/dist/test/unit/create-task.test.d.ts +0 -1
  423. package/dist/test/unit/create-task.test.js +0 -197
  424. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  425. package/dist/test/unit/diagnose-task.test.js +0 -457
  426. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  427. package/dist/test/unit/direct-guards.test.js +0 -328
  428. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  429. package/dist/test/unit/discovery-token-store.test.js +0 -139
  430. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  431. package/dist/test/unit/evidence-pack.test.js +0 -224
  432. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  433. package/dist/test/unit/get-task-status.test.js +0 -174
  434. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  435. package/dist/test/unit/get-task-summary.test.js +0 -146
  436. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  437. package/dist/test/unit/goal-graph.test.js +0 -298
  438. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  439. package/dist/test/unit/goal-progress.test.js +0 -381
  440. package/dist/test/unit/goal-report.test.d.ts +0 -1
  441. package/dist/test/unit/goal-report.test.js +0 -159
  442. package/dist/test/unit/goal-status.test.d.ts +0 -1
  443. package/dist/test/unit/goal-status.test.js +0 -215
  444. package/dist/test/unit/goal-store.test.d.ts +0 -1
  445. package/dist/test/unit/goal-store.test.js +0 -253
  446. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  447. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  448. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  449. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  450. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  451. package/dist/test/unit/handoff-export.test.js +0 -263
  452. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  453. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  454. package/dist/test/unit/logging.test.d.ts +0 -1
  455. package/dist/test/unit/logging.test.js +0 -397
  456. package/dist/test/unit/path-guard.test.d.ts +0 -1
  457. package/dist/test/unit/path-guard.test.js +0 -133
  458. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  459. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  460. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  461. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  462. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  463. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  464. package/dist/test/unit/release-gate.test.d.ts +0 -1
  465. package/dist/test/unit/release-gate.test.js +0 -242
  466. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  467. package/dist/test/unit/run-task-loop.test.js +0 -380
  468. package/dist/test/unit/safe-status.test.d.ts +0 -1
  469. package/dist/test/unit/safe-status.test.js +0 -165
  470. package/dist/test/unit/safe-views.test.d.ts +0 -1
  471. package/dist/test/unit/safe-views.test.js +0 -171
  472. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  473. package/dist/test/unit/schema-drift-check.test.js +0 -176
  474. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  475. package/dist/test/unit/sensitive-guard.test.js +0 -104
  476. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  477. package/dist/test/unit/spec-kit-import.test.js +0 -341
  478. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  479. package/dist/test/unit/subgoal-sync.test.js +0 -183
  480. package/dist/test/unit/sync-file.test.d.ts +0 -1
  481. package/dist/test/unit/sync-file.test.js +0 -154
  482. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  483. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  484. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  485. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  486. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  487. package/dist/test/unit/toolSearch.test.js +0 -571
  488. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  489. package/dist/test/unit/wait-for-task.test.js +0 -144
  490. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  491. package/dist/test/unit/watcher-status.test.js +0 -169
  492. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  493. package/dist/test/unit/worktree-manager.test.js +0 -176
  494. package/dist/tools/applyPatch.js +0 -41
  495. package/dist/tools/cancelTask.d.ts +0 -30
  496. package/dist/tools/cancelTask.js +0 -64
  497. package/dist/tools/finalizeDirectSession.js +0 -84
  498. package/dist/tools/goalSubgoalTask.js +0 -110
  499. package/dist/tools/killTask.d.ts +0 -15
  500. package/dist/tools/listAgents.d.ts +0 -12
  501. package/dist/tools/listAgents.js +0 -42
  502. package/dist/tools/reconcileTasks.js +0 -352
  503. package/dist/tools/retryTask.js +0 -45
  504. package/dist/tools/runVerification.js +0 -36
  505. package/dist/tools/syncFile.js +0 -65
  506. package/docs/control-center/control-center-daily-driver.md +0 -211
  507. package/docs/control-center/control-center-mvp.md +0 -205
  508. package/docs/control-center/control-center-phase2.md +0 -159
  509. package/docs/execution-plan-2026-07-09.md +0 -315
  510. package/docs/release-v0.6.0.md +0 -71
  511. package/docs/release-v0.6.1.md +0 -75
  512. package/docs/release-v0.6.4.md +0 -45
  513. package/docs/roadmap-execution-and-acceptance.md +0 -365
  514. package/src/test/unit/acceptance-engine.test.ts +0 -261
  515. package/src/test/unit/agent-router.test.ts +0 -342
  516. package/src/test/unit/android-doctor.test.ts +0 -158
  517. package/src/test/unit/apply-patch.test.ts +0 -293
  518. package/src/test/unit/audit-checks.test.ts +0 -567
  519. package/src/test/unit/chinese-path.test.ts +0 -106
  520. package/src/test/unit/command-guard.test.ts +0 -221
  521. package/src/test/unit/create-task.test.ts +0 -255
  522. package/src/test/unit/diagnose-task.test.ts +0 -544
  523. package/src/test/unit/direct-guards.test.ts +0 -467
  524. package/src/test/unit/discovery-token-store.test.ts +0 -181
  525. package/src/test/unit/evidence-pack.test.ts +0 -251
  526. package/src/test/unit/get-task-status.test.ts +0 -203
  527. package/src/test/unit/get-task-summary.test.ts +0 -173
  528. package/src/test/unit/goal-graph.test.ts +0 -347
  529. package/src/test/unit/goal-progress.test.ts +0 -538
  530. package/src/test/unit/goal-report.test.ts +0 -189
  531. package/src/test/unit/goal-status.test.ts +0 -270
  532. package/src/test/unit/goal-store.test.ts +0 -318
  533. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  534. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  535. package/src/test/unit/handoff-export.test.ts +0 -295
  536. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  537. package/src/test/unit/logging.test.ts +0 -497
  538. package/src/test/unit/path-guard.test.ts +0 -180
  539. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  540. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  541. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  542. package/src/test/unit/release-gate.test.ts +0 -314
  543. package/src/test/unit/run-task-loop.test.ts +0 -425
  544. package/src/test/unit/safe-status.test.ts +0 -187
  545. package/src/test/unit/safe-views.test.ts +0 -184
  546. package/src/test/unit/schema-drift-check.test.ts +0 -260
  547. package/src/test/unit/sensitive-guard.test.ts +0 -124
  548. package/src/test/unit/spec-kit-import.test.ts +0 -429
  549. package/src/test/unit/subgoal-sync.test.ts +0 -236
  550. package/src/test/unit/sync-file.test.ts +0 -231
  551. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  552. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  553. package/src/test/unit/toolSearch.test.ts +0 -756
  554. package/src/test/unit/wait-for-task.test.ts +0 -176
  555. package/src/test/unit/watcher-status.test.ts +0 -190
  556. package/src/test/unit/worktree-manager.test.ts +0 -247
  557. package/src/tools/cancelTask.ts +0 -70
  558. package/src/tools/listAgents.ts +0 -54
  559. package/src/tools/retryTask.ts +0 -53
  560. package/src/tools/syncFile.ts +0 -122
  561. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  562. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  563. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  564. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  565. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  566. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  567. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  568. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  569. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  570. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  571. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  572. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  573. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  574. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  575. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  576. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  577. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  578. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  579. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  580. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  581. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  582. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  583. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  584. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  585. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  586. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  587. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  588. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  589. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  590. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  591. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
@@ -6,15 +6,19 @@ import {
6
6
  readFileSync,
7
7
  readdirSync,
8
8
  statSync,
9
- writeFileSync,
10
9
  } from "node:fs";
11
10
  import { join, relative, resolve, isAbsolute } from "node:path";
12
11
  import { execFile } from "node:child_process";
12
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
13
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";
14
17
 
15
18
  const MAX_HASH_BYTES = 5 * 1024 * 1024;
16
19
  const MAX_SNAPSHOT_FILES = 5000;
17
20
  const MAX_DIFF_BYTES = 20 * 1024 * 1024;
21
+ const DIFF_TRUNCATION_MARKER = "\n[PATCHWARDEN DIFF TRUNCATED]\n";
18
22
  const SKIP_DIRECTORIES = new Set([".git", ".patchwarden", "node_modules"]);
19
23
 
20
24
  export interface FileFingerprint {
@@ -75,6 +79,8 @@ export interface ChangeArtifacts {
75
79
  diff: string;
76
80
  diff_available: boolean;
77
81
  diff_truncated: boolean;
82
+ diff_redacted?: boolean;
83
+ diff_redaction_categories?: string[];
78
84
  diff_size_bytes: number;
79
85
  additions: number;
80
86
  deletions: number;
@@ -91,25 +97,26 @@ export interface ChangeArtifacts {
91
97
  artifact_hygiene: ArtifactHygiene;
92
98
  }
93
99
 
94
- export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapshot> {
100
+ export async function captureRepoSnapshot(repoPath: string, signal?: AbortSignal): Promise<RepoSnapshot> {
101
+ throwIfAborted(signal);
95
102
  const warnings: string[] = [];
96
- const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]);
103
+ const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"], signal);
97
104
  const isGit = isGitResult.stdout.trim() === "true";
98
105
  let head: string | null = null;
99
106
  let status = "";
100
107
  let paths: string[] = [];
101
- const trackedPaths = new Set<string>();
102
- const ignoredPaths = new Set<string>();
108
+ const trackedPaths = new Map<string, Set<string>>();
109
+ const ignoredPaths = new Map<string, Set<string>>();
103
110
 
104
111
  const dirtyPaths = new Set<string>();
105
112
  if (isGit) {
106
113
  // The following five git calls are independent of each other and can run in parallel.
107
114
  const [headResult, statusResult, trackedResult, ignoredResult, listedResult] = await Promise.all([
108
- runGit(repoPath, ["rev-parse", "HEAD"]),
109
- runGit(repoPath, ["status", "--porcelain=v1", "-uall"]),
110
- runGit(repoPath, ["ls-files", "-z"]),
111
- runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]),
112
- runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]),
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),
113
120
  ]);
114
121
 
115
122
  if (headResult.status === 0) head = headResult.stdout.trim() || null;
@@ -136,25 +143,25 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
136
143
  }
137
144
  }
138
145
  if (trackedResult.status === 0) {
139
- for (const path of trackedResult.stdout.split("\0").filter(Boolean)) trackedPaths.add(normalizePath(path));
146
+ for (const path of trackedResult.stdout.split("\0").filter(Boolean)) addSnapshotPath(trackedPaths, path);
140
147
  }
141
148
  if (ignoredResult.status === 0) {
142
- for (const path of ignoredResult.stdout.split("\0").filter(Boolean)) ignoredPaths.add(normalizePath(path));
149
+ for (const path of ignoredResult.stdout.split("\0").filter(Boolean)) addSnapshotPath(ignoredPaths, path);
143
150
  } else {
144
151
  warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
145
152
  }
146
153
  if (listedResult.status === 0) {
147
154
  paths = [...new Set([
148
155
  ...listedResult.stdout.split("\0").filter(Boolean),
149
- ...walkWorkspace(repoPath),
156
+ ...walkWorkspace(repoPath, signal),
150
157
  ])];
151
158
  } else {
152
159
  warnings.push("git ls-files failed; using bounded filesystem scan");
153
- paths = walkWorkspace(repoPath);
160
+ paths = walkWorkspace(repoPath, signal);
154
161
  }
155
162
  } else {
156
163
  warnings.push("repository is not a Git worktree; diff will contain file-change evidence only");
157
- paths = walkWorkspace(repoPath);
164
+ paths = walkWorkspace(repoPath, signal);
158
165
  }
159
166
 
160
167
  if (paths.length > MAX_SNAPSHOT_FILES) {
@@ -164,6 +171,7 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
164
171
 
165
172
  const files: Record<string, FileFingerprint> = {};
166
173
  for (const inputPath of paths.sort()) {
174
+ throwIfAborted(signal);
167
175
  const normalized = normalizePath(inputPath);
168
176
  if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized)) continue;
169
177
  const absolutePath = resolve(repoPath, inputPath);
@@ -176,8 +184,8 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
176
184
  files[normalized] = {
177
185
  size: stat.size,
178
186
  sha256,
179
- tracked: trackedPaths.has(normalized),
180
- ignored: !trackedPaths.has(normalized) && ignoredPaths.has(normalized),
187
+ tracked: hasSnapshotPath(trackedPaths, normalized),
188
+ ignored: !hasSnapshotPath(trackedPaths, normalized) && hasSnapshotPath(ignoredPaths, normalized),
181
189
  };
182
190
  } catch {
183
191
  warnings.push(`could not fingerprint: ${normalized}`);
@@ -197,55 +205,96 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
197
205
  }
198
206
 
199
207
  export function writeSnapshot(taskDir: string, filename: string, snapshot: RepoSnapshot): void {
200
- writeFileSync(join(taskDir, filename), JSON.stringify(snapshot, null, 2), "utf-8");
208
+ atomicWriteJsonFileSync(join(taskDir, filename), snapshot);
201
209
  }
202
210
 
203
211
  export async function buildChangeArtifacts(
204
212
  repoPath: string,
205
213
  before: RepoSnapshot,
206
- after: RepoSnapshot
214
+ after: RepoSnapshot,
215
+ signal?: AbortSignal,
207
216
  ): Promise<ChangeArtifacts> {
217
+ throwIfAborted(signal);
208
218
  const changedFiles = compareSnapshots(before, after);
209
219
  const artifactHygiene = classifyArtifactHygiene(changedFiles);
210
220
  const sections: string[] = [];
211
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
+ };
212
254
 
213
255
  if (before.is_git && after.is_git && scopedPaths.length > 0) {
214
256
  if (before.head && after.head && before.head !== after.head) {
215
- const committed = await runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
216
- 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);
217
259
  }
218
260
 
219
- const base = after.head || "HEAD";
220
- const working = await runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
221
- 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
+ }
222
268
 
223
269
  for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
224
- const tracked = await 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);
225
276
  if (tracked.status === 0) continue;
226
- const untracked = await runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
227
- 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);
228
279
  }
229
280
  }
230
281
 
231
- const evidence = [
232
- "# PatchWarden change evidence",
233
- `# changed_files: ${changedFiles.length}`,
234
- `# workspace_dirty_before: ${before.workspace_dirty}`,
235
- `# workspace_dirty_after: ${after.workspace_dirty}`,
236
- ...changedFiles.map((file) => `# ${file.change}: ${file.path}`),
237
- ].join("\n");
238
- const body = sections.join("\n\n");
239
- 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;
240
287
  const additions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
241
288
  const deletions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
242
- const fileStats = await buildFileStats(repoPath, before, after, changedFiles);
289
+ const fileStats = await buildFileStats(repoPath, before, after, changedFiles, signal);
243
290
 
244
291
  return {
245
292
  changed_files: changedFiles,
246
293
  diff: fullDiff,
247
294
  diff_available: changedFiles.length > 0,
248
- diff_truncated: false,
295
+ diff_truncated: diffTruncated,
296
+ diff_redacted: sanitized.redacted,
297
+ diff_redaction_categories: sanitized.redaction_categories,
249
298
  diff_size_bytes: Buffer.byteLength(fullDiff, "utf-8"),
250
299
  additions,
251
300
  deletions,
@@ -262,13 +311,47 @@ export async function buildChangeArtifacts(
262
311
  };
263
312
  }
264
313
 
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
+
265
346
  async function buildFileStats(
266
347
  repoPath: string,
267
348
  before: RepoSnapshot,
268
349
  after: RepoSnapshot,
269
- changedFiles: ChangedFile[]
350
+ changedFiles: ChangedFile[],
351
+ signal?: AbortSignal,
270
352
  ): Promise<ChangeArtifacts["file_stats"]> {
271
353
  const results = await Promise.all(changedFiles.map(async (file) => {
354
+ throwIfAborted(signal);
272
355
  let additions = 0;
273
356
  let deletions = 0;
274
357
  const paths = file.old_path ? [file.old_path, file.path] : [file.path];
@@ -280,7 +363,7 @@ async function buildFileStats(
280
363
  }
281
364
  ranges.push([after.head || "HEAD"]);
282
365
  const rangeResults = await Promise.all(
283
- ranges.map((range) => runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths]))
366
+ ranges.map((range) => runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--numstat", ...range, "--", ...paths], signal))
284
367
  );
285
368
  for (const result of rangeResults) {
286
369
  for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
@@ -308,17 +391,31 @@ function countLines(content: string): number {
308
391
  return content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
309
392
  }
310
393
 
311
- export function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): ChangedFile[] {
312
- 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[] {
313
399
  const changed: ChangedFile[] = [];
314
- for (const path of paths) {
315
- const left = before.files[path];
316
- const right = after.files[path];
400
+ for (const { path, leftPath, rightPath, left, right } of pairSnapshotFiles(before.files, after.files, platform)) {
317
401
  if (!left && right) {
318
402
  changed.push(classifyChangedFile(path, "added", null, right));
319
403
  } else if (left && !right) {
320
404
  changed.push(classifyChangedFile(path, "deleted", left, null));
321
- } 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) {
322
419
  changed.push(classifyChangedFile(path, "modified", left, right));
323
420
  }
324
421
  }
@@ -386,10 +483,12 @@ export interface ExternalDirtyFile {
386
483
  export function extractExternalDirtyFiles(
387
484
  workspaceSnapshot: RepoSnapshot,
388
485
  repoPath: string,
389
- workspaceRoot: string
486
+ workspaceRoot: string,
487
+ platform: NodeJS.Platform = process.platform,
390
488
  ): ExternalDirtyFile[] {
391
489
  const dirtyFiles: ExternalDirtyFile[] = [];
392
- 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);
393
492
  for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
394
493
  const absolutePath = resolve(workspaceRoot, path);
395
494
  const rel = relative(repoPath, absolutePath);
@@ -399,7 +498,7 @@ export function extractExternalDirtyFiles(
399
498
  // 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
400
499
  // 2. It's not tracked by git (untracked file), OR
401
500
  // 3. It's explicitly ignored
402
- const isDirty = dirtyPathSet.has(path);
501
+ const isDirty = hasSnapshotPath(dirtyPathSet, path, platform);
403
502
  const isUntracked = !fingerprint.tracked;
404
503
  const isIgnored = fingerprint.ignored;
405
504
  if (isDirty || isUntracked || isIgnored) {
@@ -422,11 +521,19 @@ export function extractExternalDirtyFiles(
422
521
  */
423
522
  export function findNewExternalDirtyFiles(
424
523
  baseline: ExternalDirtyFile[],
425
- current: ExternalDirtyFile[]
524
+ current: ExternalDirtyFile[],
525
+ platform: NodeJS.Platform = process.platform,
426
526
  ): ExternalDirtyFile[] {
427
- 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
+ }
428
532
  return current.filter((f) => {
429
- 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));
430
537
  if (!baselineFile) return true; // New path — definitely new
431
538
  // Same path but content changed during task execution
432
539
  if (baselineFile.before_sha256 !== f.before_sha256) return true;
@@ -623,9 +730,98 @@ function normalizePath(value: string): string {
623
730
  return value.replace(/\\/g, "/");
624
731
  }
625
732
 
626
- 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[] {
627
822
  const result: string[] = [];
628
823
  const visit = (directory: string) => {
824
+ throwIfAborted(signal);
629
825
  if (result.length >= MAX_SNAPSHOT_FILES) return;
630
826
  let entries;
631
827
  try {
@@ -634,6 +830,7 @@ function walkWorkspace(root: string): string[] {
634
830
  return;
635
831
  }
636
832
  for (const entry of entries) {
833
+ throwIfAborted(signal);
637
834
  if (result.length >= MAX_SNAPSHOT_FILES) break;
638
835
  if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
639
836
  const absolute = join(directory, entry.name);
@@ -645,14 +842,29 @@ function walkWorkspace(root: string): string[] {
645
842
  return result;
646
843
  }
647
844
 
648
- function runGit(repoPath: string, args: string[]): Promise<{ status: number | null; stdout: string; stderr: string }> {
649
- return new Promise((resolve) => {
650
- execFile("git", args, {
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, {
651
856
  cwd: repoPath,
652
857
  encoding: "utf-8",
653
858
  timeout: 30_000,
654
859
  maxBuffer: MAX_DIFF_BYTES,
860
+ windowsHide: true,
861
+ env,
862
+ signal,
655
863
  }, (err, stdout, stderr) => {
864
+ if (signal?.aborted) {
865
+ reject(abortReason(signal));
866
+ return;
867
+ }
656
868
  let status: number | null;
657
869
  if (err) {
658
870
  // err.code is the exit code (number) when the process exited with a non-zero status;
@@ -661,11 +873,33 @@ function runGit(repoPath: string, args: string[]): Promise<{ status: number | nu
661
873
  } else {
662
874
  status = 0;
663
875
  }
664
- resolve({ status, stdout: stdout || "", stderr: stderr || "" });
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 });
665
882
  });
666
883
  });
667
884
  }
668
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
+
669
903
  function hashFileAsync(filePath: string): Promise<string> {
670
904
  return new Promise((resolve, reject) => {
671
905
  const hash = createHash("sha256");
@@ -1,29 +1,45 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
3
3
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { platform } from "node:os";
5
+ import { buildGitEnvironment, resolveTrustedExecutable } from "./processSecurity.js";
6
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
5
7
 
6
8
  const isWindows = platform() === "win32";
7
9
 
8
10
  /**
9
11
  * Remove a file or directory, working around Node.js fs.rmSync issues on Windows
10
12
  * where recursive directory deletion silently fails (Node v22+).
11
- * Falls back to `rmdir /s /q` on Windows if rmSync doesn't remove the path.
13
+ * On Windows, clears read-only attributes before one bounded native retry.
12
14
  */
13
15
  function safeRemoveSync(targetPath: string): void {
14
16
  rmSync(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
15
17
  if (isWindows && existsSync(targetPath)) {
16
- try {
17
- const stat = statSync(targetPath);
18
- if (stat.isDirectory()) {
19
- execFileSync("cmd", ["/c", "rmdir", "/s", "/q", targetPath], { windowsHide: true });
20
- } else {
21
- execFileSync("cmd", ["/c", "del", "/f", "/q", targetPath], { windowsHide: true });
22
- }
23
- } catch {
24
- // Best effort caller will verify via existsSync if needed
25
- }
18
+ makeWritableSync(targetPath);
19
+ rmSync(targetPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 150 });
20
+ }
21
+ if (existsSync(targetPath)) {
22
+ throw new Error(`Cleanup path still exists after bounded retries: ${targetPath}`);
23
+ }
24
+ }
25
+
26
+ function makeWritableSync(targetPath: string): void {
27
+ let stat: ReturnType<typeof lstatSync>;
28
+ try {
29
+ stat = lstatSync(targetPath);
30
+ } catch {
31
+ return;
32
+ }
33
+ if (stat.isSymbolicLink()) return;
34
+ try { chmodSync(targetPath, stat.isDirectory() ? 0o700 : 0o600); } catch { /* best effort */ }
35
+ if (!stat.isDirectory()) return;
36
+ let entries: string[];
37
+ try {
38
+ entries = readdirSync(targetPath);
39
+ } catch {
40
+ return;
26
41
  }
42
+ for (const entry of entries) makeWritableSync(join(targetPath, entry));
27
43
  }
28
44
 
29
45
  const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
@@ -76,7 +92,7 @@ export function runPostTaskCleanup(repoPath: string, taskDir: string): PostTaskC
76
92
  });
77
93
  }
78
94
  }
79
- writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(report, null, 2), "utf-8");
95
+ atomicWriteJsonFileSync(join(taskDir, "post-task-cleanup.json"), report);
80
96
  return report;
81
97
  }
82
98
 
@@ -117,10 +133,12 @@ function walk(dir: string, visit: (path: string, isDir: boolean) => void): void
117
133
  function hasTrackedGitContent(root: string, rel: string): boolean {
118
134
  if (!isGitWorktree(root)) return false;
119
135
  try {
120
- const output = execFileSync("git", ["ls-files", "--", rel], {
136
+ const env = buildGitEnvironment(root);
137
+ const output = execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["ls-files", "--", rel], {
121
138
  cwd: root,
122
139
  encoding: "utf-8",
123
140
  windowsHide: true,
141
+ env,
124
142
  });
125
143
  return output.trim().length > 0;
126
144
  } catch {
@@ -130,19 +148,23 @@ function hasTrackedGitContent(root: string, rel: string): boolean {
130
148
 
131
149
  function isIgnoredOrUntracked(root: string, rel: string): boolean {
132
150
  if (!isGitWorktree(root)) return true;
151
+ const env = buildGitEnvironment(root);
152
+ const git = resolveTrustedExecutable("git", root, { pathValue: env.PATH });
133
153
  try {
134
- execFileSync("git", ["check-ignore", "-q", "--", rel], {
154
+ execFileSync(git, ["check-ignore", "-q", "--", rel], {
135
155
  cwd: root,
136
156
  stdio: "ignore",
137
157
  windowsHide: true,
158
+ env,
138
159
  });
139
160
  return true;
140
161
  } catch {
141
162
  try {
142
- const output = execFileSync("git", ["ls-files", "--others", "--exclude-standard", "--", rel], {
163
+ const output = execFileSync(git, ["ls-files", "--others", "--exclude-standard", "--", rel], {
143
164
  cwd: root,
144
165
  encoding: "utf-8",
145
166
  windowsHide: true,
167
+ env,
146
168
  });
147
169
  return output.trim().length > 0 || !existsSync(resolve(root, rel));
148
170
  } catch {
@@ -153,10 +175,12 @@ function isIgnoredOrUntracked(root: string, rel: string): boolean {
153
175
 
154
176
  function isGitWorktree(root: string): boolean {
155
177
  try {
156
- return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
178
+ const env = buildGitEnvironment(root);
179
+ return execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["rev-parse", "--is-inside-work-tree"], {
157
180
  cwd: root,
158
181
  encoding: "utf-8",
159
182
  windowsHide: true,
183
+ env,
160
184
  }).trim() === "true";
161
185
  } catch {
162
186
  return false;