patchwarden 1.5.1 → 1.6.2

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