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
@@ -1,30 +1,36 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createReadStream, lstatSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
2
+ import { createReadStream, lstatSync, readFileSync, readdirSync, } from "node:fs";
3
3
  import { join, relative, resolve, isAbsolute } from "node:path";
4
4
  import { execFile } from "node:child_process";
5
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
5
6
  import { isSensitivePath } from "../security/sensitiveGuard.js";
7
+ import { nullDevice } from "../utils/platform.js";
8
+ import { buildGitEnvironment, resolveTrustedExecutable } from "./processSecurity.js";
9
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
6
10
  const MAX_HASH_BYTES = 5 * 1024 * 1024;
7
11
  const MAX_SNAPSHOT_FILES = 5000;
8
12
  const MAX_DIFF_BYTES = 20 * 1024 * 1024;
13
+ const DIFF_TRUNCATION_MARKER = "\n[PATCHWARDEN DIFF TRUNCATED]\n";
9
14
  const SKIP_DIRECTORIES = new Set([".git", ".patchwarden", "node_modules"]);
10
- export async function captureRepoSnapshot(repoPath) {
15
+ export async function captureRepoSnapshot(repoPath, signal) {
16
+ throwIfAborted(signal);
11
17
  const warnings = [];
12
- const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]);
18
+ const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"], signal);
13
19
  const isGit = isGitResult.stdout.trim() === "true";
14
20
  let head = null;
15
21
  let status = "";
16
22
  let paths = [];
17
- const trackedPaths = new Set();
18
- const ignoredPaths = new Set();
23
+ const trackedPaths = new Map();
24
+ const ignoredPaths = new Map();
19
25
  const dirtyPaths = new Set();
20
26
  if (isGit) {
21
27
  // The following five git calls are independent of each other and can run in parallel.
22
28
  const [headResult, statusResult, trackedResult, ignoredResult, listedResult] = await Promise.all([
23
- runGit(repoPath, ["rev-parse", "HEAD"]),
24
- runGit(repoPath, ["status", "--porcelain=v1", "-uall"]),
25
- runGit(repoPath, ["ls-files", "-z"]),
26
- runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]),
27
- runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]),
29
+ runGit(repoPath, ["rev-parse", "HEAD"], signal),
30
+ runGit(repoPath, ["status", "--porcelain=v1", "-uall"], signal),
31
+ runGit(repoPath, ["ls-files", "-z"], signal),
32
+ runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"], signal),
33
+ runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"], signal),
28
34
  ]);
29
35
  if (headResult.status === 0)
30
36
  head = headResult.stdout.trim() || null;
@@ -55,11 +61,11 @@ export async function captureRepoSnapshot(repoPath) {
55
61
  }
56
62
  if (trackedResult.status === 0) {
57
63
  for (const path of trackedResult.stdout.split("\0").filter(Boolean))
58
- trackedPaths.add(normalizePath(path));
64
+ addSnapshotPath(trackedPaths, path);
59
65
  }
60
66
  if (ignoredResult.status === 0) {
61
67
  for (const path of ignoredResult.stdout.split("\0").filter(Boolean))
62
- ignoredPaths.add(normalizePath(path));
68
+ addSnapshotPath(ignoredPaths, path);
63
69
  }
64
70
  else {
65
71
  warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
@@ -67,17 +73,17 @@ export async function captureRepoSnapshot(repoPath) {
67
73
  if (listedResult.status === 0) {
68
74
  paths = [...new Set([
69
75
  ...listedResult.stdout.split("\0").filter(Boolean),
70
- ...walkWorkspace(repoPath),
76
+ ...walkWorkspace(repoPath, signal),
71
77
  ])];
72
78
  }
73
79
  else {
74
80
  warnings.push("git ls-files failed; using bounded filesystem scan");
75
- paths = walkWorkspace(repoPath);
81
+ paths = walkWorkspace(repoPath, signal);
76
82
  }
77
83
  }
78
84
  else {
79
85
  warnings.push("repository is not a Git worktree; diff will contain file-change evidence only");
80
- paths = walkWorkspace(repoPath);
86
+ paths = walkWorkspace(repoPath, signal);
81
87
  }
82
88
  if (paths.length > MAX_SNAPSHOT_FILES) {
83
89
  warnings.push(`snapshot limited to ${MAX_SNAPSHOT_FILES} files`);
@@ -85,6 +91,7 @@ export async function captureRepoSnapshot(repoPath) {
85
91
  }
86
92
  const files = {};
87
93
  for (const inputPath of paths.sort()) {
94
+ throwIfAborted(signal);
88
95
  const normalized = normalizePath(inputPath);
89
96
  if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized))
90
97
  continue;
@@ -99,8 +106,8 @@ export async function captureRepoSnapshot(repoPath) {
99
106
  files[normalized] = {
100
107
  size: stat.size,
101
108
  sha256,
102
- tracked: trackedPaths.has(normalized),
103
- ignored: !trackedPaths.has(normalized) && ignoredPaths.has(normalized),
109
+ tracked: hasSnapshotPath(trackedPaths, normalized),
110
+ ignored: !hasSnapshotPath(trackedPaths, normalized) && hasSnapshotPath(ignoredPaths, normalized),
104
111
  };
105
112
  }
106
113
  catch {
@@ -119,32 +126,14 @@ export async function captureRepoSnapshot(repoPath) {
119
126
  };
120
127
  }
121
128
  export function writeSnapshot(taskDir, filename, snapshot) {
122
- writeFileSync(join(taskDir, filename), JSON.stringify(snapshot, null, 2), "utf-8");
129
+ atomicWriteJsonFileSync(join(taskDir, filename), snapshot);
123
130
  }
124
- export async function buildChangeArtifacts(repoPath, before, after) {
131
+ export async function buildChangeArtifacts(repoPath, before, after, signal) {
132
+ throwIfAborted(signal);
125
133
  const changedFiles = compareSnapshots(before, after);
126
134
  const artifactHygiene = classifyArtifactHygiene(changedFiles);
127
135
  const sections = [];
128
136
  const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
129
- if (before.is_git && after.is_git && scopedPaths.length > 0) {
130
- if (before.head && after.head && before.head !== after.head) {
131
- const committed = await runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
132
- if (committed.stdout.trim())
133
- sections.push("# Changes committed during task\n", committed.stdout.trimEnd());
134
- }
135
- const base = after.head || "HEAD";
136
- const working = await runGit(repoPath, ["diff", "--no-color", "--binary", base, "--", ...scopedPaths]);
137
- if (working.stdout.trim())
138
- sections.push("# Staged and unstaged changes\n", working.stdout.trimEnd());
139
- for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
140
- const tracked = await runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path]);
141
- if (tracked.status === 0)
142
- continue;
143
- const untracked = await runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--", "/dev/null", file.path]);
144
- if (untracked.stdout.trim())
145
- sections.push("# Untracked file\n", untracked.stdout.trimEnd());
146
- }
147
- }
148
137
  const evidence = [
149
138
  "# PatchWarden change evidence",
150
139
  `# changed_files: ${changedFiles.length}`,
@@ -152,16 +141,70 @@ export async function buildChangeArtifacts(repoPath, before, after) {
152
141
  `# workspace_dirty_after: ${after.workspace_dirty}`,
153
142
  ...changedFiles.map((file) => `# ${file.change}: ${file.path}`),
154
143
  ].join("\n");
155
- const body = sections.join("\n\n");
156
- const fullDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
144
+ let retainedSectionBytes = 0;
145
+ let diffTruncated = Buffer.byteLength(evidence, "utf-8") > MAX_DIFF_BYTES;
146
+ const sectionBudget = Math.max(0, MAX_DIFF_BYTES
147
+ - Math.min(Buffer.byteLength(evidence, "utf-8"), MAX_DIFF_BYTES)
148
+ - Buffer.byteLength(DIFF_TRUNCATION_MARKER, "utf-8")
149
+ - 2);
150
+ const appendSection = (label, content, commandTruncated = false) => {
151
+ if (!content.trim()) {
152
+ if (commandTruncated)
153
+ diffTruncated = true;
154
+ return;
155
+ }
156
+ const block = `${sections.length > 0 ? "\n\n" : ""}${label}\n\n${content.trimEnd()}`;
157
+ const remaining = Math.max(0, sectionBudget - retainedSectionBytes);
158
+ const bounded = utf8Prefix(block, remaining);
159
+ if (bounded) {
160
+ sections.push(bounded);
161
+ retainedSectionBytes += Buffer.byteLength(bounded, "utf-8");
162
+ }
163
+ if (commandTruncated || Buffer.byteLength(bounded, "utf-8") < Buffer.byteLength(block, "utf-8")) {
164
+ diffTruncated = true;
165
+ }
166
+ };
167
+ if (before.is_git && after.is_git && scopedPaths.length > 0) {
168
+ if (before.head && after.head && before.head !== after.head) {
169
+ const committed = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths], signal);
170
+ appendSection("# Changes committed during task", committed.stdout, committed.truncated);
171
+ }
172
+ if (retainedSectionBytes < sectionBudget) {
173
+ const base = after.head || "HEAD";
174
+ const working = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", base, "--", ...scopedPaths], signal);
175
+ appendSection("# Staged and unstaged changes", working.stdout, working.truncated);
176
+ }
177
+ else {
178
+ diffTruncated = true;
179
+ }
180
+ for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
181
+ throwIfAborted(signal);
182
+ if (retainedSectionBytes >= sectionBudget) {
183
+ diffTruncated = true;
184
+ break;
185
+ }
186
+ const tracked = await runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path], signal);
187
+ if (tracked.status === 0)
188
+ continue;
189
+ const untracked = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-index", "--no-color", "--binary", "--", nullDevice, file.path], signal);
190
+ appendSection("# Untracked file", untracked.stdout, untracked.truncated);
191
+ }
192
+ }
193
+ const body = sections.join("");
194
+ const rawDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
195
+ const sanitized = sanitizeDiffEvidence(rawDiff, MAX_DIFF_BYTES);
196
+ const fullDiff = sanitized.content;
197
+ diffTruncated ||= sanitized.truncated;
157
198
  const additions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
158
199
  const deletions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
159
- const fileStats = await buildFileStats(repoPath, before, after, changedFiles);
200
+ const fileStats = await buildFileStats(repoPath, before, after, changedFiles, signal);
160
201
  return {
161
202
  changed_files: changedFiles,
162
203
  diff: fullDiff,
163
204
  diff_available: changedFiles.length > 0,
164
- diff_truncated: false,
205
+ diff_truncated: diffTruncated,
206
+ diff_redacted: sanitized.redacted,
207
+ diff_redaction_categories: sanitized.redaction_categories,
165
208
  diff_size_bytes: Buffer.byteLength(fullDiff, "utf-8"),
166
209
  additions,
167
210
  deletions,
@@ -177,8 +220,32 @@ export async function buildChangeArtifacts(repoPath, before, after) {
177
220
  artifact_hygiene: artifactHygiene,
178
221
  };
179
222
  }
180
- async function buildFileStats(repoPath, before, after, changedFiles) {
223
+ export function sanitizeDiffEvidence(input, maxBytes = MAX_DIFF_BYTES) {
224
+ if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
225
+ throw new Error("maxBytes must be a positive integer");
226
+ }
227
+ const redaction = redactSensitiveContent(input);
228
+ const encodedBytes = Buffer.byteLength(redaction.content, "utf-8");
229
+ if (encodedBytes <= maxBytes) {
230
+ return {
231
+ content: redaction.content,
232
+ truncated: false,
233
+ redacted: redaction.redacted,
234
+ redaction_categories: redaction.redaction_categories,
235
+ };
236
+ }
237
+ const marker = utf8Prefix(DIFF_TRUNCATION_MARKER, maxBytes);
238
+ const markerBytes = Buffer.byteLength(marker, "utf-8");
239
+ return {
240
+ content: utf8Prefix(redaction.content, Math.max(0, maxBytes - markerBytes)) + marker,
241
+ truncated: true,
242
+ redacted: redaction.redacted,
243
+ redaction_categories: redaction.redaction_categories,
244
+ };
245
+ }
246
+ async function buildFileStats(repoPath, before, after, changedFiles, signal) {
181
247
  const results = await Promise.all(changedFiles.map(async (file) => {
248
+ throwIfAborted(signal);
182
249
  let additions = 0;
183
250
  let deletions = 0;
184
251
  const paths = file.old_path ? [file.old_path, file.path] : [file.path];
@@ -188,7 +255,7 @@ async function buildFileStats(repoPath, before, after, changedFiles) {
188
255
  ranges.push([before.head, after.head]);
189
256
  }
190
257
  ranges.push([after.head || "HEAD"]);
191
- const rangeResults = await Promise.all(ranges.map((range) => runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths])));
258
+ const rangeResults = await Promise.all(ranges.map((range) => runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--numstat", ...range, "--", ...paths], signal)));
192
259
  for (const result of rangeResults) {
193
260
  for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
194
261
  const [added, removed] = line.split(/\s+/);
@@ -215,19 +282,30 @@ function countLines(content) {
215
282
  return 0;
216
283
  return content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
217
284
  }
218
- export function compareSnapshots(before, after) {
219
- const paths = [...new Set([...Object.keys(before.files), ...Object.keys(after.files)])].sort();
285
+ export function compareSnapshots(before, after, platform = process.platform) {
220
286
  const changed = [];
221
- for (const path of paths) {
222
- const left = before.files[path];
223
- const right = after.files[path];
287
+ for (const { path, leftPath, rightPath, left, right } of pairSnapshotFiles(before.files, after.files, platform)) {
224
288
  if (!left && right) {
225
289
  changed.push(classifyChangedFile(path, "added", null, right));
226
290
  }
227
291
  else if (left && !right) {
228
292
  changed.push(classifyChangedFile(path, "deleted", left, null));
229
293
  }
230
- else if (left.sha256 !== right.sha256) {
294
+ else if (left && right && left.sha256 === right.sha256 && leftPath !== rightPath) {
295
+ // Windows lookup is case-insensitive, but a case-only Git rename is still
296
+ // auditable work and must not disappear from the evidence set.
297
+ changed.push({
298
+ path: rightPath,
299
+ old_path: leftPath,
300
+ change: "renamed",
301
+ before_sha256: left.sha256,
302
+ after_sha256: right.sha256,
303
+ tracked: left.tracked || right.tracked,
304
+ ignored: right.ignored,
305
+ kind: classifyPathKind(rightPath),
306
+ });
307
+ }
308
+ else if (left && right && left.sha256 !== right.sha256) {
231
309
  changed.push(classifyChangedFile(path, "modified", left, right));
232
310
  }
233
311
  }
@@ -280,9 +358,11 @@ export function emptyArtifactHygiene() {
280
358
  * Extract files that are dirty in the workspace but outside the target repo.
281
359
  * Used to establish a baseline before task execution.
282
360
  */
283
- export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspaceRoot) {
361
+ export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspaceRoot, platform = process.platform) {
284
362
  const dirtyFiles = [];
285
- const dirtyPathSet = new Set(workspaceSnapshot.dirty_paths);
363
+ const dirtyPathSet = new Map();
364
+ for (const path of workspaceSnapshot.dirty_paths)
365
+ addSnapshotPath(dirtyPathSet, path, platform);
286
366
  for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
287
367
  const absolutePath = resolve(workspaceRoot, path);
288
368
  const rel = relative(repoPath, absolutePath);
@@ -292,7 +372,7 @@ export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspace
292
372
  // 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
293
373
  // 2. It's not tracked by git (untracked file), OR
294
374
  // 3. It's explicitly ignored
295
- const isDirty = dirtyPathSet.has(path);
375
+ const isDirty = hasSnapshotPath(dirtyPathSet, path, platform);
296
376
  const isUntracked = !fingerprint.tracked;
297
377
  const isIgnored = fingerprint.ignored;
298
378
  if (isDirty || isUntracked || isIgnored) {
@@ -312,10 +392,17 @@ export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspace
312
392
  * Returns files that are NEW (not present in baseline) or CHANGED
313
393
  * (same path but different sha256, meaning the task modified them).
314
394
  */
315
- export function findNewExternalDirtyFiles(baseline, current) {
316
- const baselineMap = new Map(baseline.map((f) => [f.path, f]));
395
+ export function findNewExternalDirtyFiles(baseline, current, platform = process.platform) {
396
+ const baselineMap = new Map();
397
+ for (const file of baseline) {
398
+ const key = comparableSnapshotPath(file.path, platform);
399
+ baselineMap.set(key, [...(baselineMap.get(key) ?? []), file]);
400
+ }
317
401
  return current.filter((f) => {
318
- const baselineFile = baselineMap.get(f.path);
402
+ const candidates = baselineMap.get(comparableSnapshotPath(f.path, platform)) ?? [];
403
+ const baselineFile = candidates.length <= 1
404
+ ? candidates[0]
405
+ : candidates.find((candidate) => normalizePath(candidate.path) === normalizePath(f.path));
319
406
  if (!baselineFile)
320
407
  return true; // New path — definitely new
321
408
  // Same path but content changed during task execution
@@ -481,9 +568,72 @@ function classificationReason(change) {
481
568
  function normalizePath(value) {
482
569
  return value.replace(/\\/g, "/");
483
570
  }
484
- function walkWorkspace(root) {
571
+ function comparableSnapshotPath(value, platform = process.platform) {
572
+ const normalized = normalizePath(value);
573
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
574
+ }
575
+ function addSnapshotPath(index, value, platform = process.platform) {
576
+ const normalized = normalizePath(value);
577
+ const key = comparableSnapshotPath(normalized, platform);
578
+ const exactPaths = index.get(key) ?? new Set();
579
+ exactPaths.add(normalized);
580
+ index.set(key, exactPaths);
581
+ }
582
+ function hasSnapshotPath(index, value, platform = process.platform) {
583
+ const normalized = normalizePath(value);
584
+ const exactPaths = index.get(comparableSnapshotPath(normalized, platform));
585
+ if (!exactPaths)
586
+ return false;
587
+ return platform !== "win32" || exactPaths.size <= 1 || exactPaths.has(normalized);
588
+ }
589
+ function pairSnapshotFiles(before, after, platform) {
590
+ const beforeGroups = groupSnapshotFiles(before, platform);
591
+ const afterGroups = groupSnapshotFiles(after, platform);
592
+ const pairs = [];
593
+ const comparablePaths = [...new Set([...beforeGroups.keys(), ...afterGroups.keys()])].sort();
594
+ for (const comparablePath of comparablePaths) {
595
+ const beforeEntries = beforeGroups.get(comparablePath) ?? [];
596
+ const afterEntries = afterGroups.get(comparablePath) ?? [];
597
+ if (platform !== "win32" || (beforeEntries.length <= 1 && afterEntries.length <= 1)) {
598
+ pairs.push({
599
+ path: afterEntries[0]?.path ?? beforeEntries[0].path,
600
+ leftPath: beforeEntries[0]?.path,
601
+ rightPath: afterEntries[0]?.path,
602
+ left: beforeEntries[0]?.fingerprint,
603
+ right: afterEntries[0]?.fingerprint,
604
+ });
605
+ continue;
606
+ }
607
+ // NTFS can opt individual directories into case-sensitive mode. If either
608
+ // snapshot contains a case collision, preserve every exact path instead of
609
+ // silently dropping one entry from the evidence.
610
+ const beforeExact = new Map(beforeEntries.map((entry) => [normalizePath(entry.path), entry]));
611
+ const afterExact = new Map(afterEntries.map((entry) => [normalizePath(entry.path), entry]));
612
+ const exactPaths = [...new Set([...beforeExact.keys(), ...afterExact.keys()])].sort();
613
+ for (const exactPath of exactPaths) {
614
+ pairs.push({
615
+ path: afterExact.get(exactPath)?.path ?? beforeExact.get(exactPath).path,
616
+ leftPath: beforeExact.get(exactPath)?.path,
617
+ rightPath: afterExact.get(exactPath)?.path,
618
+ left: beforeExact.get(exactPath)?.fingerprint,
619
+ right: afterExact.get(exactPath)?.fingerprint,
620
+ });
621
+ }
622
+ }
623
+ return pairs;
624
+ }
625
+ function groupSnapshotFiles(files, platform) {
626
+ const grouped = new Map();
627
+ for (const [path, fingerprint] of Object.entries(files)) {
628
+ const key = comparableSnapshotPath(path, platform);
629
+ grouped.set(key, [...(grouped.get(key) ?? []), { path, fingerprint }]);
630
+ }
631
+ return grouped;
632
+ }
633
+ function walkWorkspace(root, signal) {
485
634
  const result = [];
486
635
  const visit = (directory) => {
636
+ throwIfAborted(signal);
487
637
  if (result.length >= MAX_SNAPSHOT_FILES)
488
638
  return;
489
639
  let entries;
@@ -494,6 +644,7 @@ function walkWorkspace(root) {
494
644
  return;
495
645
  }
496
646
  for (const entry of entries) {
647
+ throwIfAborted(signal);
497
648
  if (result.length >= MAX_SNAPSHOT_FILES)
498
649
  break;
499
650
  if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name))
@@ -508,14 +659,24 @@ function walkWorkspace(root) {
508
659
  visit(root);
509
660
  return result;
510
661
  }
511
- function runGit(repoPath, args) {
512
- return new Promise((resolve) => {
513
- execFile("git", args, {
662
+ function runGit(repoPath, args, signal) {
663
+ throwIfAborted(signal);
664
+ return new Promise((resolve, reject) => {
665
+ const env = buildGitEnvironment(repoPath);
666
+ const git = resolveTrustedExecutable("git", repoPath, { pathValue: env.PATH });
667
+ execFile(git, args, {
514
668
  cwd: repoPath,
515
669
  encoding: "utf-8",
516
670
  timeout: 30_000,
517
671
  maxBuffer: MAX_DIFF_BYTES,
672
+ windowsHide: true,
673
+ env,
674
+ signal,
518
675
  }, (err, stdout, stderr) => {
676
+ if (signal?.aborted) {
677
+ reject(abortReason(signal));
678
+ return;
679
+ }
519
680
  let status;
520
681
  if (err) {
521
682
  // err.code is the exit code (number) when the process exited with a non-zero status;
@@ -525,10 +686,31 @@ function runGit(repoPath, args) {
525
686
  else {
526
687
  status = 0;
527
688
  }
528
- resolve({ status, stdout: stdout || "", stderr: stderr || "" });
689
+ const truncated = Boolean(err
690
+ && typeof err.code === "string"
691
+ && err.code.includes("MAXBUFFER"));
692
+ resolve({ status, stdout: stdout || "", stderr: stderr || "", truncated });
529
693
  });
530
694
  });
531
695
  }
696
+ function throwIfAborted(signal) {
697
+ if (signal?.aborted)
698
+ throw abortReason(signal);
699
+ }
700
+ function abortReason(signal) {
701
+ return signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
702
+ }
703
+ function utf8Prefix(value, maxBytes) {
704
+ if (maxBytes <= 0)
705
+ return "";
706
+ const encoded = Buffer.from(value, "utf-8");
707
+ if (encoded.length <= maxBytes)
708
+ return value;
709
+ let prefix = encoded.subarray(0, maxBytes).toString("utf-8").replace(/\uFFFD$/u, "");
710
+ while (Buffer.byteLength(prefix, "utf-8") > maxBytes)
711
+ prefix = prefix.slice(0, -1);
712
+ return prefix;
713
+ }
532
714
  function hashFileAsync(filePath) {
533
715
  return new Promise((resolve, reject) => {
534
716
  const hash = createHash("sha256");
@@ -1,29 +1,50 @@
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
  const isWindows = platform() === "win32";
6
8
  /**
7
9
  * Remove a file or directory, working around Node.js fs.rmSync issues on Windows
8
10
  * where recursive directory deletion silently fails (Node v22+).
9
- * Falls back to `rmdir /s /q` on Windows if rmSync doesn't remove the path.
11
+ * On Windows, clears read-only attributes before one bounded native retry.
10
12
  */
11
13
  function safeRemoveSync(targetPath) {
12
14
  rmSync(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
13
15
  if (isWindows && existsSync(targetPath)) {
14
- try {
15
- const stat = statSync(targetPath);
16
- if (stat.isDirectory()) {
17
- execFileSync("cmd", ["/c", "rmdir", "/s", "/q", targetPath], { windowsHide: true });
18
- }
19
- else {
20
- execFileSync("cmd", ["/c", "del", "/f", "/q", targetPath], { windowsHide: true });
21
- }
22
- }
23
- catch {
24
- // Best effort — caller will verify via existsSync if needed
25
- }
16
+ makeWritableSync(targetPath);
17
+ rmSync(targetPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 150 });
18
+ }
19
+ if (existsSync(targetPath)) {
20
+ throw new Error(`Cleanup path still exists after bounded retries: ${targetPath}`);
21
+ }
22
+ }
23
+ function makeWritableSync(targetPath) {
24
+ let stat;
25
+ try {
26
+ stat = lstatSync(targetPath);
27
+ }
28
+ catch {
29
+ return;
30
+ }
31
+ if (stat.isSymbolicLink())
32
+ return;
33
+ try {
34
+ chmodSync(targetPath, stat.isDirectory() ? 0o700 : 0o600);
35
+ }
36
+ catch { /* best effort */ }
37
+ if (!stat.isDirectory())
38
+ return;
39
+ let entries;
40
+ try {
41
+ entries = readdirSync(targetPath);
42
+ }
43
+ catch {
44
+ return;
26
45
  }
46
+ for (const entry of entries)
47
+ makeWritableSync(join(targetPath, entry));
27
48
  }
28
49
  const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
29
50
  const CLEANUP_DIRS = new Set(["__pycache__", "dist", "release_packages", "temp_files"]);
@@ -63,7 +84,7 @@ export function runPostTaskCleanup(repoPath, taskDir) {
63
84
  });
64
85
  }
65
86
  }
66
- writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(report, null, 2), "utf-8");
87
+ atomicWriteJsonFileSync(join(taskDir, "post-task-cleanup.json"), report);
67
88
  return report;
68
89
  }
69
90
  function collectCandidates(root) {
@@ -109,10 +130,12 @@ function hasTrackedGitContent(root, rel) {
109
130
  if (!isGitWorktree(root))
110
131
  return false;
111
132
  try {
112
- const output = execFileSync("git", ["ls-files", "--", rel], {
133
+ const env = buildGitEnvironment(root);
134
+ const output = execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["ls-files", "--", rel], {
113
135
  cwd: root,
114
136
  encoding: "utf-8",
115
137
  windowsHide: true,
138
+ env,
116
139
  });
117
140
  return output.trim().length > 0;
118
141
  }
@@ -123,20 +146,24 @@ function hasTrackedGitContent(root, rel) {
123
146
  function isIgnoredOrUntracked(root, rel) {
124
147
  if (!isGitWorktree(root))
125
148
  return true;
149
+ const env = buildGitEnvironment(root);
150
+ const git = resolveTrustedExecutable("git", root, { pathValue: env.PATH });
126
151
  try {
127
- execFileSync("git", ["check-ignore", "-q", "--", rel], {
152
+ execFileSync(git, ["check-ignore", "-q", "--", rel], {
128
153
  cwd: root,
129
154
  stdio: "ignore",
130
155
  windowsHide: true,
156
+ env,
131
157
  });
132
158
  return true;
133
159
  }
134
160
  catch {
135
161
  try {
136
- const output = execFileSync("git", ["ls-files", "--others", "--exclude-standard", "--", rel], {
162
+ const output = execFileSync(git, ["ls-files", "--others", "--exclude-standard", "--", rel], {
137
163
  cwd: root,
138
164
  encoding: "utf-8",
139
165
  windowsHide: true,
166
+ env,
140
167
  });
141
168
  return output.trim().length > 0 || !existsSync(resolve(root, rel));
142
169
  }
@@ -147,10 +174,12 @@ function isIgnoredOrUntracked(root, rel) {
147
174
  }
148
175
  function isGitWorktree(root) {
149
176
  try {
150
- return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
177
+ const env = buildGitEnvironment(root);
178
+ return execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["rev-parse", "--is-inside-work-tree"], {
151
179
  cwd: root,
152
180
  encoding: "utf-8",
153
181
  windowsHide: true,
182
+ env,
154
183
  }).trim() === "true";
155
184
  }
156
185
  catch {
@@ -0,0 +1,56 @@
1
+ export declare const DEFAULT_MAX_PROCESS_LOG_BYTES: number;
2
+ export interface ChildEnvironmentOptions {
3
+ cwd: string;
4
+ allowedNames?: readonly string[];
5
+ blockedNames?: readonly string[];
6
+ sourceEnvironment?: NodeJS.ProcessEnv;
7
+ platform?: NodeJS.Platform;
8
+ }
9
+ export interface ExecutableResolutionOptions {
10
+ platform?: NodeJS.Platform;
11
+ pathValue?: string;
12
+ pathExtValue?: string;
13
+ fileExists?: (path: string) => boolean;
14
+ }
15
+ export interface ResolvedProcessCommand {
16
+ command: string;
17
+ argsPrefix: string[];
18
+ }
19
+ export interface PreparedProcessCommand {
20
+ command: string;
21
+ args: string[];
22
+ }
23
+ /**
24
+ * Build the environment for a task-owned child. Provider credentials are not
25
+ * ambient capability: an agent receives them only when their variable names
26
+ * are explicitly allow-listed in trusted local configuration.
27
+ */
28
+ export declare function buildChildEnvironment(options: ChildEnvironmentOptions): NodeJS.ProcessEnv;
29
+ /** Build a non-interactive Git environment that disables repo-defined hooks and fsmonitor. */
30
+ export declare function buildGitEnvironment(cwd: string): NodeJS.ProcessEnv;
31
+ /** Resolve a Windows bare command before spawn so CreateProcess never searches cwd. */
32
+ export declare function resolveTrustedExecutable(command: string, cwd: string, options?: ExecutableResolutionOptions): string;
33
+ /**
34
+ * Resolve Windows package-manager shims to their JavaScript CLI entry point.
35
+ * This keeps npm/npx/pnpm execution shell-free while still supporting their
36
+ * normal .cmd installations.
37
+ */
38
+ export declare function resolvePackageManagerInvocation(managerCommand: string, cwd: string, options?: ExecutableResolutionOptions): ResolvedProcessCommand;
39
+ /** Replace the one supported Windows cmd wrapper with a native Node CLI. */
40
+ export declare function prepareShellFreeCommand(command: string, args: readonly string[], cwd: string, options?: ExecutableResolutionOptions): PreparedProcessCommand;
41
+ /** Parse the intentionally simple allow-listed command grammar without a shell. */
42
+ export declare function resolveTrustedCommandLine(commandLine: string, cwd: string, options?: ExecutableResolutionOptions): ResolvedProcessCommand;
43
+ export declare function sanitizeTrustedPath(pathValue: string, cwd: string, platform?: NodeJS.Platform): string;
44
+ export declare function allowedEnvironmentValues(names: readonly string[] | undefined, sourceEnvironment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string[];
45
+ export declare function redactProcessOutput(input: string, exactValues?: readonly string[]): string;
46
+ /** Captures complete process logs up to one auditable, redacted byte budget. */
47
+ export declare class SecureProcessLogCapture {
48
+ private readonly chunks;
49
+ private readonly maxBytes;
50
+ private readonly existingBytes;
51
+ private capturedBytes;
52
+ private truncatedPath;
53
+ constructor(paths: Array<string | undefined>, maxBytes?: number);
54
+ append(path: string | undefined, chunk: Buffer | string): void;
55
+ flush(exactValues?: readonly string[]): void;
56
+ }