patchwarden 1.5.1 → 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 (573) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +152 -25
  3. package/README.md +120 -23
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentStore.js +42 -34
  6. package/dist/config.d.ts +15 -0
  7. package/dist/config.js +66 -13
  8. package/dist/control/fileManager.d.ts +2 -0
  9. package/dist/control/fileManager.js +39 -0
  10. package/dist/control/middleware/auth.d.ts +6 -0
  11. package/dist/control/middleware/auth.js +14 -5
  12. package/dist/control/middleware/static.js +16 -3
  13. package/dist/control/routeTable.d.ts +24 -0
  14. package/dist/control/routeTable.js +263 -0
  15. package/dist/control/routes/audit.js +17 -7
  16. package/dist/control/routes/evidence.js +1 -1
  17. package/dist/control/routes/lineage.js +1 -1
  18. package/dist/control/routes/process.d.ts +7 -0
  19. package/dist/control/routes/process.js +329 -42
  20. package/dist/control/routes/sessions.d.ts +9 -1
  21. package/dist/control/routes/sessions.js +84 -21
  22. package/dist/control/routes/status.d.ts +46 -1
  23. package/dist/control/routes/status.js +72 -15
  24. package/dist/control/routes/taskActions.js +24 -60
  25. package/dist/control/routes/tasks.d.ts +4 -0
  26. package/dist/control/routes/tasks.js +61 -62
  27. package/dist/control/routes/workspace.js +50 -23
  28. package/dist/control/runtime.d.ts +3 -2
  29. package/dist/control/runtime.js +63 -30
  30. package/dist/control/server.js +36 -365
  31. package/dist/control/shared.d.ts +25 -1
  32. package/dist/control/shared.js +64 -11
  33. package/dist/direct/directAudit.js +234 -225
  34. package/dist/direct/directGuards.d.ts +2 -1
  35. package/dist/direct/directGuards.js +42 -29
  36. package/dist/direct/directPatch.d.ts +6 -1
  37. package/dist/direct/directPatch.js +55 -8
  38. package/dist/direct/directSessionStore.d.ts +16 -6
  39. package/dist/direct/directSessionStore.js +127 -35
  40. package/dist/direct/directVerification.js +8 -16
  41. package/dist/doctor.js +62 -52
  42. package/dist/goal/acceptanceEngine.d.ts +1 -1
  43. package/dist/goal/goalProgress.js +75 -69
  44. package/dist/goal/goalReport.js +5 -12
  45. package/dist/goal/goalStatus.d.ts +2 -0
  46. package/dist/goal/goalStatus.js +10 -3
  47. package/dist/goal/goalStore.d.ts +9 -2
  48. package/dist/goal/goalStore.js +152 -45
  49. package/dist/goal/handoffExport.js +6 -6
  50. package/dist/goal/specKitImport.js +97 -96
  51. package/dist/goal/subgoalSync.js +11 -9
  52. package/dist/goal/worktreeManager.d.ts +2 -0
  53. package/dist/goal/worktreeManager.js +209 -136
  54. package/dist/httpServer.js +111 -32
  55. package/dist/logging.js +5 -3
  56. package/dist/policy/projectPolicy.js +5 -2
  57. package/dist/release/releaseGate.d.ts +1 -1
  58. package/dist/release/releaseGate.js +78 -25
  59. package/dist/runner/agentInvocation.d.ts +14 -0
  60. package/dist/runner/agentInvocation.js +96 -4
  61. package/dist/runner/changeCapture.d.ts +13 -5
  62. package/dist/runner/changeCapture.js +243 -61
  63. package/dist/runner/postTaskCleanup.js +48 -19
  64. package/dist/runner/processSecurity.d.ts +56 -0
  65. package/dist/runner/processSecurity.js +373 -0
  66. package/dist/runner/runTask.d.ts +1 -1
  67. package/dist/runner/runTask.js +115 -87
  68. package/dist/runner/simpleProcess.d.ts +3 -0
  69. package/dist/runner/simpleProcess.js +50 -36
  70. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  71. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  72. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  73. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  74. package/dist/runner/taskStatusStore.d.ts +11 -0
  75. package/dist/runner/taskStatusStore.js +24 -0
  76. package/dist/runner/watch.d.ts +14 -1
  77. package/dist/runner/watch.js +256 -48
  78. package/dist/security/contentRedaction.js +2 -2
  79. package/dist/security/discoveryTokenStore.d.ts +3 -2
  80. package/dist/security/discoveryTokenStore.js +17 -2
  81. package/dist/security/loopbackHost.d.ts +2 -0
  82. package/dist/security/loopbackHost.js +16 -0
  83. package/dist/security/pathGuard.js +32 -17
  84. package/dist/security/planGuard.js +2 -1
  85. package/dist/security/runtimeGuard.js +9 -6
  86. package/dist/security/secretComparison.d.ts +3 -0
  87. package/dist/security/secretComparison.js +11 -0
  88. package/dist/security/sensitiveGuard.d.ts +5 -0
  89. package/dist/security/sensitiveGuard.js +27 -8
  90. package/dist/security/toolInvocationGuard.d.ts +2 -2
  91. package/dist/security/toolInvocationGuard.js +1 -1
  92. package/dist/security/workspaceRootGuard.d.ts +8 -0
  93. package/dist/security/workspaceRootGuard.js +47 -0
  94. package/dist/smoke-test.js +24 -23
  95. package/dist/tools/catalog/index.d.ts +4 -0
  96. package/dist/tools/catalog/index.js +4 -0
  97. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  98. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  99. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  100. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  101. package/dist/tools/definitions/toolDefs.js +1177 -0
  102. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  103. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  104. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
  105. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
  106. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  107. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
  108. package/dist/tools/diagnostics/index.d.ts +6 -0
  109. package/dist/tools/diagnostics/index.js +6 -0
  110. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  111. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  112. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  113. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  114. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  115. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  116. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  117. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  118. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  119. package/dist/tools/direct/index.d.ts +3 -0
  120. package/dist/tools/direct/index.js +3 -0
  121. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  122. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  123. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  124. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  125. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  126. package/dist/tools/discovery/index.d.ts +3 -0
  127. package/dist/tools/discovery/index.js +3 -0
  128. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  129. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  130. package/dist/tools/dispatch/coreDispatch.js +32 -31
  131. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  132. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  133. package/dist/tools/dispatch/directDispatch.js +11 -10
  134. package/dist/tools/dispatch/goalDispatch.js +5 -4
  135. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  136. package/dist/tools/dispatch/validation.d.ts +7 -0
  137. package/dist/tools/dispatch/validation.js +71 -0
  138. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  139. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  140. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  141. package/dist/tools/goals/index.d.ts +3 -0
  142. package/dist/tools/goals/index.js +3 -0
  143. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  144. package/dist/tools/registry.d.ts +7 -11
  145. package/dist/tools/registry.js +17 -1178
  146. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  147. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  148. package/dist/tools/release/index.d.ts +2 -0
  149. package/dist/tools/release/index.js +2 -0
  150. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  151. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  152. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  153. package/dist/tools/tasks/cancelTask.js +85 -0
  154. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
  155. package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
  156. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  157. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  158. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  159. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  160. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  161. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  162. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  163. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  164. package/dist/tools/tasks/index.d.ts +18 -0
  165. package/dist/tools/tasks/index.js +18 -0
  166. package/dist/tools/tasks/killTask.d.ts +1 -0
  167. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  168. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  169. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  170. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  171. package/dist/tools/tasks/retryTask.js +60 -0
  172. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  173. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
  174. package/dist/tools/tasks/runVerification.js +38 -0
  175. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  176. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  177. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  178. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  179. package/dist/tools/workspace/applyPatch.js +53 -0
  180. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  181. package/dist/tools/workspace/index.d.ts +10 -0
  182. package/dist/tools/workspace/index.js +10 -0
  183. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  184. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  185. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  186. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  187. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  188. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  189. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  190. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  191. package/dist/tools/workspace/syncFile.js +134 -0
  192. package/dist/utils/atomicFile.d.ts +7 -0
  193. package/dist/utils/atomicFile.js +48 -0
  194. package/dist/utils/boundedFile.d.ts +8 -0
  195. package/dist/utils/boundedFile.js +93 -0
  196. package/dist/utils/lockedJsonFile.d.ts +15 -0
  197. package/dist/utils/lockedJsonFile.js +224 -0
  198. package/dist/utils/platform.d.ts +11 -0
  199. package/dist/utils/platform.js +26 -0
  200. package/dist/utils/stableJson.d.ts +2 -0
  201. package/dist/utils/stableJson.js +12 -0
  202. package/dist/version.d.ts +3 -2
  203. package/dist/version.js +3 -2
  204. package/dist/watcherStatus.d.ts +2 -2
  205. package/dist/watcherStatus.js +18 -0
  206. package/docs/CODE_WIKI.md +808 -143
  207. package/docs/architecture.md +15 -55
  208. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  209. package/docs/control-center/README.md +4 -4
  210. package/docs/dashboard-overview.md +4 -4
  211. package/docs/desktop-app.md +175 -0
  212. package/docs/evidence-pack-schema.md +3 -3
  213. package/docs/open-source-application.md +17 -11
  214. package/docs/release-checklist.md +1 -1
  215. package/docs/release-evidence.md +39 -29
  216. package/docs/task-safe-review-workflow.md +5 -5
  217. package/docs/threat-model.md +19 -1
  218. package/examples/config.example.json +4 -2
  219. package/package.json +27 -10
  220. package/scripts/README.md +15 -6
  221. package/scripts/build.js +11 -0
  222. package/scripts/checks/brand-check.js +3 -0
  223. package/scripts/checks/build-output-check.js +37 -0
  224. package/scripts/checks/control-center-smoke.js +91 -6
  225. package/scripts/checks/control-smoke.js +19 -8
  226. package/scripts/checks/http-mcp-smoke.js +82 -4
  227. package/scripts/checks/lifecycle-smoke.js +10 -10
  228. package/scripts/checks/mcp-manifest-check.js +1 -1
  229. package/scripts/checks/mcp-smoke.js +27 -3
  230. package/scripts/checks/package-manifest-check.js +20 -7
  231. package/scripts/checks/unit-tests.js +3 -5
  232. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  233. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  234. package/scripts/control/manage-patchwarden.ps1 +69 -4
  235. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  236. package/scripts/control/run-background-supervisor.ps1 +41 -0
  237. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  238. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  239. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  240. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  241. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  242. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  243. package/scripts/lib/clean-generated-output.js +11 -0
  244. package/scripts/lib/file-discovery.js +12 -0
  245. package/scripts/release/desktop-preflight.js +211 -0
  246. package/scripts/release/pack-clean.js +59 -14
  247. package/src/assessments/agentAssessor.ts +26 -12
  248. package/src/assessments/assessmentStore.ts +52 -40
  249. package/src/config.ts +70 -13
  250. package/src/control/fileManager.ts +40 -0
  251. package/src/control/middleware/auth.ts +22 -4
  252. package/src/control/middleware/static.ts +16 -3
  253. package/src/control/routeTable.ts +301 -0
  254. package/src/control/routes/audit.ts +14 -6
  255. package/src/control/routes/evidence.ts +1 -1
  256. package/src/control/routes/lineage.ts +1 -1
  257. package/src/control/routes/process.ts +341 -40
  258. package/src/control/routes/sessions.ts +88 -25
  259. package/src/control/routes/status.ts +97 -17
  260. package/src/control/routes/taskActions.ts +24 -58
  261. package/src/control/routes/tasks.ts +66 -58
  262. package/src/control/routes/workspace.ts +56 -25
  263. package/src/control/runtime.ts +69 -33
  264. package/src/control/server.ts +36 -342
  265. package/src/control/shared.ts +77 -11
  266. package/src/direct/directAudit.ts +17 -9
  267. package/src/direct/directGuards.ts +72 -44
  268. package/src/direct/directPatch.ts +95 -8
  269. package/src/direct/directSessionStore.ts +211 -55
  270. package/src/direct/directVerification.ts +9 -23
  271. package/src/doctor.ts +73 -59
  272. package/src/goal/acceptanceEngine.ts +1 -1
  273. package/src/goal/goalProgress.ts +17 -13
  274. package/src/goal/goalReport.ts +5 -13
  275. package/src/goal/goalStatus.ts +19 -3
  276. package/src/goal/goalStore.ts +241 -54
  277. package/src/goal/handoffExport.ts +6 -6
  278. package/src/goal/specKitImport.ts +12 -12
  279. package/src/goal/subgoalSync.ts +11 -8
  280. package/src/goal/worktreeManager.ts +139 -22
  281. package/src/httpServer.ts +113 -30
  282. package/src/logging.ts +10 -3
  283. package/src/policy/projectPolicy.ts +5 -2
  284. package/src/release/releaseGate.ts +74 -27
  285. package/src/runner/agentInvocation.ts +126 -4
  286. package/src/runner/changeCapture.ts +291 -57
  287. package/src/runner/postTaskCleanup.ts +41 -17
  288. package/src/runner/processSecurity.ts +433 -0
  289. package/src/runner/runTask.ts +138 -91
  290. package/src/runner/simpleProcess.ts +66 -31
  291. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  292. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  293. package/src/runner/taskStatusStore.ts +54 -0
  294. package/src/runner/watch.ts +281 -51
  295. package/src/security/contentRedaction.ts +2 -2
  296. package/src/security/discoveryTokenStore.ts +18 -4
  297. package/src/security/loopbackHost.ts +19 -0
  298. package/src/security/pathGuard.ts +35 -16
  299. package/src/security/planGuard.ts +2 -1
  300. package/src/security/runtimeGuard.ts +9 -6
  301. package/src/security/secretComparison.ts +13 -0
  302. package/src/security/sensitiveGuard.ts +27 -8
  303. package/src/security/toolInvocationGuard.ts +2 -2
  304. package/src/security/workspaceRootGuard.ts +53 -0
  305. package/src/smoke-test.ts +25 -24
  306. package/src/tools/catalog/index.ts +4 -0
  307. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  308. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  309. package/src/tools/definitions/toolDefs.ts +1264 -0
  310. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  311. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
  312. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
  313. package/src/tools/diagnostics/index.ts +6 -0
  314. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  315. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  316. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  317. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  318. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  319. package/src/tools/direct/index.ts +3 -0
  320. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  321. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  322. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  323. package/src/tools/discovery/index.ts +3 -0
  324. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  325. package/src/tools/dispatch/coreDispatch.ts +36 -31
  326. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  327. package/src/tools/dispatch/directDispatch.ts +11 -10
  328. package/src/tools/dispatch/goalDispatch.ts +5 -4
  329. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  330. package/src/tools/dispatch/validation.ts +78 -0
  331. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  332. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  333. package/src/tools/goals/index.ts +3 -0
  334. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  335. package/src/tools/registry.ts +17 -1261
  336. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  337. package/src/tools/release/index.ts +2 -0
  338. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  339. package/src/tools/tasks/cancelTask.ts +109 -0
  340. package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
  341. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  342. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  343. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  344. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  345. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  346. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  347. package/src/tools/tasks/index.ts +18 -0
  348. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  349. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  350. package/src/tools/tasks/retryTask.ts +69 -0
  351. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
  352. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  353. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  354. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  355. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  356. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  357. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  358. package/src/tools/workspace/index.ts +10 -0
  359. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  360. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  361. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  362. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  363. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  364. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  365. package/src/tools/workspace/syncFile.ts +292 -0
  366. package/src/utils/atomicFile.ts +58 -0
  367. package/src/utils/boundedFile.ts +116 -0
  368. package/src/utils/lockedJsonFile.ts +251 -0
  369. package/src/utils/platform.ts +31 -0
  370. package/src/utils/stableJson.ts +12 -0
  371. package/src/version.ts +3 -2
  372. package/src/watcherStatus.ts +19 -1
  373. package/ui/desktop-bootstrap.js +11 -0
  374. package/ui/desktop-bridge.js +36 -0
  375. package/ui/desktop.css +160 -0
  376. package/ui/getting-started.js +93 -0
  377. package/ui/i18n.js +484 -0
  378. package/ui/log-parser.js +30 -0
  379. package/ui/pages/audit.html +12 -0
  380. package/ui/pages/dashboard.html +90 -147
  381. package/ui/pages/direct-sessions.html +123 -26
  382. package/ui/pages/getting-started.html +43 -0
  383. package/ui/pages/logs.html +105 -22
  384. package/ui/pages/settings.html +101 -0
  385. package/ui/pages/task-detail.html +12 -0
  386. package/ui/pages/tasks.html +54 -8
  387. package/ui/pages/workspace.html +38 -1
  388. package/ui/settings.js +245 -0
  389. package/PatchWarden-Control.cmd +0 -6
  390. package/Restart-PatchWarden-Control.cmd +0 -6
  391. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  392. package/dist/test/unit/acceptance-engine.test.js +0 -228
  393. package/dist/test/unit/agent-router.test.d.ts +0 -1
  394. package/dist/test/unit/agent-router.test.js +0 -287
  395. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  396. package/dist/test/unit/android-doctor.test.js +0 -118
  397. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  398. package/dist/test/unit/apply-patch.test.js +0 -225
  399. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  400. package/dist/test/unit/audit-checks.test.js +0 -350
  401. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  402. package/dist/test/unit/chinese-path.test.js +0 -91
  403. package/dist/test/unit/command-guard.test.d.ts +0 -1
  404. package/dist/test/unit/command-guard.test.js +0 -160
  405. package/dist/test/unit/create-task.test.d.ts +0 -1
  406. package/dist/test/unit/create-task.test.js +0 -197
  407. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  408. package/dist/test/unit/diagnose-task.test.js +0 -457
  409. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  410. package/dist/test/unit/direct-guards.test.js +0 -328
  411. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  412. package/dist/test/unit/discovery-token-store.test.js +0 -139
  413. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  414. package/dist/test/unit/evidence-pack.test.js +0 -224
  415. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  416. package/dist/test/unit/get-task-status.test.js +0 -174
  417. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  418. package/dist/test/unit/get-task-summary.test.js +0 -146
  419. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  420. package/dist/test/unit/goal-graph.test.js +0 -298
  421. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  422. package/dist/test/unit/goal-progress.test.js +0 -381
  423. package/dist/test/unit/goal-report.test.d.ts +0 -1
  424. package/dist/test/unit/goal-report.test.js +0 -159
  425. package/dist/test/unit/goal-status.test.d.ts +0 -1
  426. package/dist/test/unit/goal-status.test.js +0 -215
  427. package/dist/test/unit/goal-store.test.d.ts +0 -1
  428. package/dist/test/unit/goal-store.test.js +0 -253
  429. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  430. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  431. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  432. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  433. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  434. package/dist/test/unit/handoff-export.test.js +0 -263
  435. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  436. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  437. package/dist/test/unit/logging.test.d.ts +0 -1
  438. package/dist/test/unit/logging.test.js +0 -397
  439. package/dist/test/unit/path-guard.test.d.ts +0 -1
  440. package/dist/test/unit/path-guard.test.js +0 -133
  441. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  442. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  443. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  444. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  445. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  446. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  447. package/dist/test/unit/release-gate.test.d.ts +0 -1
  448. package/dist/test/unit/release-gate.test.js +0 -242
  449. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  450. package/dist/test/unit/run-task-loop.test.js +0 -380
  451. package/dist/test/unit/safe-status.test.d.ts +0 -1
  452. package/dist/test/unit/safe-status.test.js +0 -165
  453. package/dist/test/unit/safe-views.test.d.ts +0 -1
  454. package/dist/test/unit/safe-views.test.js +0 -171
  455. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  456. package/dist/test/unit/schema-drift-check.test.js +0 -176
  457. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  458. package/dist/test/unit/sensitive-guard.test.js +0 -104
  459. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  460. package/dist/test/unit/spec-kit-import.test.js +0 -341
  461. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  462. package/dist/test/unit/subgoal-sync.test.js +0 -183
  463. package/dist/test/unit/sync-file.test.d.ts +0 -1
  464. package/dist/test/unit/sync-file.test.js +0 -154
  465. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  466. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  467. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  468. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  469. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  470. package/dist/test/unit/toolSearch.test.js +0 -571
  471. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  472. package/dist/test/unit/wait-for-task.test.js +0 -144
  473. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  474. package/dist/test/unit/watcher-status.test.js +0 -169
  475. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  476. package/dist/test/unit/worktree-manager.test.js +0 -176
  477. package/dist/tools/applyPatch.js +0 -41
  478. package/dist/tools/cancelTask.d.ts +0 -30
  479. package/dist/tools/cancelTask.js +0 -64
  480. package/dist/tools/finalizeDirectSession.js +0 -84
  481. package/dist/tools/goalSubgoalTask.js +0 -110
  482. package/dist/tools/killTask.d.ts +0 -15
  483. package/dist/tools/retryTask.js +0 -45
  484. package/dist/tools/runVerification.js +0 -36
  485. package/dist/tools/syncFile.js +0 -65
  486. package/docs/control-center/control-center-daily-driver.md +0 -211
  487. package/docs/control-center/control-center-mvp.md +0 -205
  488. package/docs/control-center/control-center-phase2.md +0 -159
  489. package/docs/execution-plan-2026-07-09.md +0 -315
  490. package/docs/release-v0.6.0.md +0 -71
  491. package/docs/release-v0.6.1.md +0 -75
  492. package/docs/release-v0.6.4.md +0 -45
  493. package/docs/roadmap-execution-and-acceptance.md +0 -365
  494. package/src/test/unit/acceptance-engine.test.ts +0 -261
  495. package/src/test/unit/agent-router.test.ts +0 -342
  496. package/src/test/unit/android-doctor.test.ts +0 -158
  497. package/src/test/unit/apply-patch.test.ts +0 -293
  498. package/src/test/unit/audit-checks.test.ts +0 -567
  499. package/src/test/unit/chinese-path.test.ts +0 -106
  500. package/src/test/unit/command-guard.test.ts +0 -221
  501. package/src/test/unit/create-task.test.ts +0 -255
  502. package/src/test/unit/diagnose-task.test.ts +0 -544
  503. package/src/test/unit/direct-guards.test.ts +0 -467
  504. package/src/test/unit/discovery-token-store.test.ts +0 -181
  505. package/src/test/unit/evidence-pack.test.ts +0 -251
  506. package/src/test/unit/get-task-status.test.ts +0 -203
  507. package/src/test/unit/get-task-summary.test.ts +0 -173
  508. package/src/test/unit/goal-graph.test.ts +0 -347
  509. package/src/test/unit/goal-progress.test.ts +0 -538
  510. package/src/test/unit/goal-report.test.ts +0 -189
  511. package/src/test/unit/goal-status.test.ts +0 -270
  512. package/src/test/unit/goal-store.test.ts +0 -318
  513. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  514. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  515. package/src/test/unit/handoff-export.test.ts +0 -295
  516. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  517. package/src/test/unit/logging.test.ts +0 -497
  518. package/src/test/unit/path-guard.test.ts +0 -180
  519. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  520. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  521. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  522. package/src/test/unit/release-gate.test.ts +0 -314
  523. package/src/test/unit/run-task-loop.test.ts +0 -425
  524. package/src/test/unit/safe-status.test.ts +0 -187
  525. package/src/test/unit/safe-views.test.ts +0 -184
  526. package/src/test/unit/schema-drift-check.test.ts +0 -260
  527. package/src/test/unit/sensitive-guard.test.ts +0 -124
  528. package/src/test/unit/spec-kit-import.test.ts +0 -429
  529. package/src/test/unit/subgoal-sync.test.ts +0 -236
  530. package/src/test/unit/sync-file.test.ts +0 -231
  531. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  532. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  533. package/src/test/unit/toolSearch.test.ts +0 -756
  534. package/src/test/unit/wait-for-task.test.ts +0 -176
  535. package/src/test/unit/watcher-status.test.ts +0 -190
  536. package/src/test/unit/worktree-manager.test.ts +0 -247
  537. package/src/tools/cancelTask.ts +0 -70
  538. package/src/tools/retryTask.ts +0 -53
  539. package/src/tools/syncFile.ts +0 -122
  540. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  541. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  542. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  543. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  544. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  545. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  546. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  547. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  548. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  549. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  550. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  551. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  552. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  553. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  554. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  555. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  556. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  557. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  558. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  559. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  560. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  561. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  562. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  563. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  564. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  565. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  566. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  567. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  568. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  569. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  570. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  571. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  572. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  573. /package/src/tools/{waitForTask.ts → tasks/waitForTask.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;