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
@@ -0,0 +1,134 @@
1
+ import { readFileSync, existsSync, mkdirSync, statSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname } from "node:path";
4
+ import { PatchWardenError } from "../../errors.js";
5
+ import { getConfig } from "../../config.js";
6
+ import { guardDirectReadPath, guardDirectFileSize, guardDirectSessionActive, guardDirectWritePath, } from "../../direct/directGuards.js";
7
+ import { appendDirectSessionOperation, readDirectSession, withDirectSessionMutationLock, } from "../../direct/directSessionStore.js";
8
+ import { computeFileSha256 } from "../../direct/directPatch.js";
9
+ import { atomicWriteFileSync } from "../../utils/atomicFile.js";
10
+ import { redactSensitiveContent } from "../../security/contentRedaction.js";
11
+ /**
12
+ * Copy a file from source to target within the same session repo.
13
+ * Both source and target must be inside the session's repo_path.
14
+ */
15
+ export function syncFile(sessionId, sourcePath, targetPath, options, config) {
16
+ const cfg = config || getConfig();
17
+ return withDirectSessionMutationLock(sessionId, () => {
18
+ const session = readDirectSession(sessionId, cfg);
19
+ guardDirectSessionActive(session);
20
+ const repoPath = session.resolved_repo_path;
21
+ const workspaceRoot = cfg.workspaceRoot;
22
+ // Guard source path — must be inside repo and not internal/sensitive/binary.
23
+ const resolvedSource = guardDirectReadPath(sourcePath, repoPath, workspaceRoot);
24
+ if (!existsSync(resolvedSource)) {
25
+ throw new PatchWardenError("source_file_not_found", `Source file does not exist: "${sourcePath}".`, "Ensure the source path is correct.", true, { source_path: sourcePath });
26
+ }
27
+ // Guard target path — must be inside repo, not in blocked dirs
28
+ const resolvedTarget = guardDirectWritePath(targetPath, repoPath, workspaceRoot);
29
+ const sourceMetadata = statSync(resolvedSource);
30
+ if (!sourceMetadata.isFile()) {
31
+ throw new PatchWardenError("direct_source_not_file", `Source path is not a regular file: "${sourcePath}".`, "Choose an existing text file inside the Direct session repository.", true, { source_path: sourcePath });
32
+ }
33
+ guardDirectFileSize(sourceMetadata.size, cfg);
34
+ // Read once, then hash the exact bytes that will be copied.
35
+ const sourceContent = readFileSync(resolvedSource);
36
+ guardDirectFileSize(sourceContent.length, cfg);
37
+ const sourceText = sourceContent.toString("utf-8");
38
+ if (!Buffer.from(sourceText, "utf-8").equals(sourceContent)) {
39
+ throw new PatchWardenError("unsupported_text_encoding", `Source file "${sourcePath}" is not valid UTF-8 text.`, "Use a normal task or convert the file to UTF-8 before Direct sync.", true, { source_path: sourcePath });
40
+ }
41
+ const sensitive = redactSensitiveContent(sourceText);
42
+ if (sensitive.redacted) {
43
+ throw new PatchWardenError("sensitive_content_blocked", `Source file contains credential-like content (${sensitive.redaction_categories.join(", ")}).`, "Remove the sensitive value and retry with placeholders or environment-variable references.", true, { source_path: sourcePath, redaction_categories: sensitive.redaction_categories });
44
+ }
45
+ const sourceSha256 = createHash("sha256").update(sourceContent).digest("hex");
46
+ if (options?.expected_source_sha256 && options.expected_source_sha256 !== sourceSha256) {
47
+ throw new PatchWardenError("source_hash_mismatch", `Source file hash mismatch. Expected "${options.expected_source_sha256}" but got "${sourceSha256}".`, "Re-read the source file to get the current sha256.", true, { expected_sha256: options.expected_source_sha256, actual_sha256: sourceSha256 });
48
+ }
49
+ // Get target sha256 before copy
50
+ let beforeTargetSha256 = null;
51
+ if (existsSync(resolvedTarget)) {
52
+ const targetMetadata = statSync(resolvedTarget);
53
+ if (!targetMetadata.isFile()) {
54
+ throw new PatchWardenError("direct_target_not_file", `Target path is not a regular file: "${targetPath}".`, "Choose a text-file target inside the Direct session repository.", true, { target_path: targetPath });
55
+ }
56
+ guardDirectFileSize(targetMetadata.size, cfg);
57
+ beforeTargetSha256 = computeFileSha256(resolvedTarget);
58
+ // Verify target sha256 if provided
59
+ if (options?.expected_target_sha256 && options.expected_target_sha256 !== beforeTargetSha256) {
60
+ throw new PatchWardenError("target_hash_mismatch", `Target file hash mismatch. Expected "${options.expected_target_sha256}" but got "${beforeTargetSha256}".`, "Re-read the target file to get the current sha256.", true, { expected_sha256: options.expected_target_sha256, actual_sha256: beforeTargetSha256 });
61
+ }
62
+ }
63
+ const copiedBytes = sourceContent.length;
64
+ if (beforeTargetSha256 === sourceSha256) {
65
+ assertSourceUnchanged(sourcePath, resolvedSource, sourceSha256, repoPath, workspaceRoot);
66
+ appendSyncOperation(sessionId, sourcePath, targetPath, beforeTargetSha256, sourceSha256, 0, 0, cfg);
67
+ return buildSyncResult(sourcePath, targetPath, beforeTargetSha256, sourceSha256, copiedBytes, false);
68
+ }
69
+ // Create target directory if needed
70
+ mkdirSync(dirname(resolvedTarget), { recursive: true });
71
+ // Re-resolve after directory creation so a newly exposed link/reparse point
72
+ // cannot reuse the earlier lexical decision. Replace atomically in the
73
+ // validated target directory and preserve an existing file's permissions.
74
+ const revalidatedTarget = guardDirectWritePath(resolvedTarget, repoPath, workspaceRoot);
75
+ const currentTargetSha256 = existsSync(revalidatedTarget)
76
+ ? computeFileSha256(revalidatedTarget)
77
+ : null;
78
+ if (currentTargetSha256 !== beforeTargetSha256) {
79
+ throw new PatchWardenError("direct_target_changed_during_sync", `Target file "${targetPath}" changed while sync_file was preparing the copy.`, "Re-read the target file and retry after concurrent filesystem changes stop.", true, {
80
+ path: targetPath,
81
+ before_sha256: beforeTargetSha256,
82
+ current_sha256: currentTargetSha256,
83
+ });
84
+ }
85
+ assertSourceUnchanged(sourcePath, resolvedSource, sourceSha256, repoPath, workspaceRoot);
86
+ const existingMode = existsSync(revalidatedTarget) ? statSync(revalidatedTarget).mode : undefined;
87
+ atomicWriteFileSync(revalidatedTarget, sourceContent, { mode: existingMode });
88
+ const finalTarget = guardDirectWritePath(revalidatedTarget, repoPath, workspaceRoot);
89
+ if (finalTarget !== revalidatedTarget) {
90
+ throw new PatchWardenError("direct_path_changed_during_write", `Target path "${targetPath}" changed while sync_file was writing it.`, "Inspect the repository path and retry only after concurrent filesystem changes stop.", true, { path: targetPath, operation: "sync_file" });
91
+ }
92
+ // Compute after hash
93
+ const afterTargetSha256 = computeFileSha256(finalTarget);
94
+ const changed = beforeTargetSha256 !== afterTargetSha256;
95
+ appendSyncOperation(sessionId, sourcePath, targetPath, beforeTargetSha256, afterTargetSha256, 1, copiedBytes, cfg);
96
+ return buildSyncResult(sourcePath, targetPath, beforeTargetSha256, afterTargetSha256, copiedBytes, changed);
97
+ }, cfg);
98
+ }
99
+ function assertSourceUnchanged(sourcePath, resolvedSource, expectedSha256, repoPath, workspaceRoot) {
100
+ const currentPath = guardDirectReadPath(sourcePath, repoPath, workspaceRoot);
101
+ const samePath = process.platform === "win32"
102
+ ? currentPath.toLowerCase() === resolvedSource.toLowerCase()
103
+ : currentPath === resolvedSource;
104
+ const currentSha256 = samePath && existsSync(currentPath)
105
+ ? computeFileSha256(currentPath)
106
+ : null;
107
+ if (!samePath || currentSha256 !== expectedSha256) {
108
+ throw new PatchWardenError("direct_source_changed_during_sync", `Source file "${sourcePath}" changed while sync_file was preparing the copy.`, "Re-read the source file and retry after concurrent filesystem changes stop.", true, { source_path: sourcePath, expected_sha256: expectedSha256, actual_sha256: currentSha256 });
109
+ }
110
+ }
111
+ function appendSyncOperation(sessionId, sourcePath, targetPath, beforeSha256, afterSha256, operationsApplied, bytesChanged, config) {
112
+ appendDirectSessionOperation(sessionId, {
113
+ index: 0,
114
+ timestamp: new Date().toISOString(),
115
+ path: targetPath,
116
+ source_path: sourcePath,
117
+ operation_type: "sync",
118
+ before_sha256: beforeSha256,
119
+ after_sha256: afterSha256,
120
+ operations_applied: operationsApplied,
121
+ bytes_changed: bytesChanged,
122
+ }, config);
123
+ }
124
+ function buildSyncResult(sourcePath, targetPath, beforeSha256, afterSha256, copiedBytes, changed) {
125
+ return {
126
+ source_path: sourcePath,
127
+ target_path: targetPath,
128
+ before_target_sha256: beforeSha256,
129
+ after_target_sha256: afterSha256,
130
+ source_sha256: afterSha256,
131
+ copied_bytes: copiedBytes,
132
+ changed,
133
+ };
134
+ }
@@ -0,0 +1,7 @@
1
+ export interface AtomicWriteOptions {
2
+ encoding?: BufferEncoding;
3
+ mode?: number;
4
+ }
5
+ /** Replace a file from a complete same-directory temporary file. */
6
+ export declare function atomicWriteFileSync(path: string, content: string | NodeJS.ArrayBufferView, options?: AtomicWriteOptions): void;
7
+ export declare function atomicWriteJsonFileSync(path: string, value: unknown, mode?: number): void;
@@ -0,0 +1,48 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { renameSync, rmSync, writeFileSync } from "node:fs";
3
+ const transientWindowsRenameErrors = new Set(["EACCES", "EBUSY", "EPERM"]);
4
+ const renameRetryDeadlineMs = 1_000;
5
+ const renameRetryDelayMs = 10;
6
+ const waitArray = new Int32Array(new SharedArrayBuffer(4));
7
+ /** Replace a file from a complete same-directory temporary file. */
8
+ export function atomicWriteFileSync(path, content, options = {}) {
9
+ const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
10
+ try {
11
+ writeFileSync(temporaryPath, content, {
12
+ encoding: options.encoding ?? "utf-8",
13
+ flag: "wx",
14
+ ...(options.mode === undefined ? {} : { mode: options.mode }),
15
+ });
16
+ replaceFileSync(temporaryPath, path);
17
+ }
18
+ catch (error) {
19
+ try {
20
+ rmSync(temporaryPath, { force: true });
21
+ }
22
+ catch { /* best effort */ }
23
+ throw error;
24
+ }
25
+ }
26
+ function replaceFileSync(temporaryPath, path) {
27
+ const deadline = Date.now() + renameRetryDeadlineMs;
28
+ while (true) {
29
+ try {
30
+ renameSync(temporaryPath, path);
31
+ return;
32
+ }
33
+ catch (error) {
34
+ const code = error && typeof error === "object" && "code" in error
35
+ ? String(error.code || "")
36
+ : "";
37
+ if (process.platform !== "win32" ||
38
+ !transientWindowsRenameErrors.has(code) ||
39
+ Date.now() >= deadline) {
40
+ throw error;
41
+ }
42
+ Atomics.wait(waitArray, 0, 0, renameRetryDelayMs);
43
+ }
44
+ }
45
+ }
46
+ export function atomicWriteJsonFileSync(path, value, mode = 0o600) {
47
+ atomicWriteFileSync(path, `${JSON.stringify(value, null, 2)}\n`, { mode });
48
+ }
@@ -0,0 +1,8 @@
1
+ export interface BoundedTextRead {
2
+ content: string;
3
+ totalBytes: number;
4
+ truncated: boolean;
5
+ }
6
+ export declare function readTextFilePrefixSync(path: string, maxBytes: number): BoundedTextRead;
7
+ export declare function readTextFileTailLinesSync(path: string, lines: number, maxBytes?: number): string;
8
+ export declare function appendBoundedTextFileSync(path: string, content: string, maxBytes?: number): void;
@@ -0,0 +1,93 @@
1
+ import { appendFileSync, closeSync, existsSync, fstatSync, openSync, readSync, statSync, } from "node:fs";
2
+ import { atomicWriteFileSync } from "./atomicFile.js";
3
+ import { withFileLockSync } from "./lockedJsonFile.js";
4
+ const TRUNCATION_MARKER = Buffer.from("[earlier content truncated]\n", "utf-8");
5
+ export function readTextFilePrefixSync(path, maxBytes) {
6
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
7
+ throw new Error("maxBytes must be a positive integer");
8
+ }
9
+ const descriptor = openSync(path, "r");
10
+ try {
11
+ const totalBytes = fstatSync(descriptor).size;
12
+ const length = Math.min(totalBytes, maxBytes);
13
+ const buffer = Buffer.alloc(length);
14
+ let offset = 0;
15
+ while (offset < length) {
16
+ const bytesRead = readSync(descriptor, buffer, offset, length - offset, offset);
17
+ if (bytesRead === 0)
18
+ break;
19
+ offset += bytesRead;
20
+ }
21
+ let content = buffer.subarray(0, offset).toString("utf-8");
22
+ if (totalBytes > offset && content.endsWith("\uFFFD"))
23
+ content = content.slice(0, -1);
24
+ return { content, totalBytes, truncated: totalBytes > offset };
25
+ }
26
+ finally {
27
+ closeSync(descriptor);
28
+ }
29
+ }
30
+ export function readTextFileTailLinesSync(path, lines, maxBytes = 1024 * 1024) {
31
+ if (!existsSync(path))
32
+ return "";
33
+ const boundedLines = Number.isFinite(lines)
34
+ ? Math.max(1, Math.min(1000, Math.trunc(lines)))
35
+ : 100;
36
+ const suffix = readFileSuffix(path, maxBytes);
37
+ let text = suffix.content.toString("utf-8");
38
+ if (suffix.truncated) {
39
+ const firstNewline = text.indexOf("\n");
40
+ if (firstNewline >= 0)
41
+ text = text.slice(firstNewline + 1);
42
+ }
43
+ const allLines = text.split(/\r?\n/);
44
+ if (allLines.at(-1) === "")
45
+ allLines.pop();
46
+ return allLines.slice(-boundedLines).join("\n");
47
+ }
48
+ export function appendBoundedTextFileSync(path, content, maxBytes = 2 * 1024 * 1024) {
49
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= TRUNCATION_MARKER.length) {
50
+ throw new Error("maxBytes must exceed the truncation marker size");
51
+ }
52
+ withFileLockSync(path, () => {
53
+ const incoming = Buffer.from(content, "utf-8");
54
+ const existingSize = existsSync(path) ? statSync(path).size : 0;
55
+ if (existingSize + incoming.length <= maxBytes) {
56
+ appendFileSync(path, incoming, { mode: 0o600 });
57
+ return;
58
+ }
59
+ const incomingSuffix = incoming.subarray(Math.max(0, incoming.length - maxBytes + TRUNCATION_MARKER.length));
60
+ const existingBudget = Math.max(0, maxBytes - TRUNCATION_MARKER.length - incomingSuffix.length);
61
+ const existingSuffix = existingBudget > 0 && existingSize > 0
62
+ ? readFileSuffix(path, existingBudget).content
63
+ : Buffer.alloc(0);
64
+ const output = Buffer.concat([TRUNCATION_MARKER, existingSuffix, incomingSuffix]);
65
+ const mode = existsSync(path) ? statSync(path).mode : 0o600;
66
+ atomicWriteFileSync(path, output, { mode });
67
+ });
68
+ }
69
+ function readFileSuffix(path, maxBytes) {
70
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
71
+ throw new Error("maxBytes must be a positive integer");
72
+ }
73
+ const descriptor = openSync(path, "r");
74
+ try {
75
+ const size = fstatSync(descriptor).size;
76
+ const length = Math.min(size, maxBytes);
77
+ const buffer = Buffer.alloc(length);
78
+ let offset = 0;
79
+ while (offset < length) {
80
+ const bytesRead = readSync(descriptor, buffer, offset, length - offset, size - length + offset);
81
+ if (bytesRead === 0)
82
+ break;
83
+ offset += bytesRead;
84
+ }
85
+ return {
86
+ content: offset === length ? buffer : buffer.subarray(0, offset),
87
+ truncated: size > length,
88
+ };
89
+ }
90
+ finally {
91
+ closeSync(descriptor);
92
+ }
93
+ }
@@ -0,0 +1,15 @@
1
+ export interface LockedJsonMutation<T extends object, R> {
2
+ next?: T;
3
+ result: R;
4
+ }
5
+ export interface LockedJsonOptions {
6
+ waitMs?: number;
7
+ corruptLockStaleMs?: number;
8
+ busyError?: () => Error;
9
+ }
10
+ export declare function readJsonObjectFileSync<T extends object>(path: string): T;
11
+ export declare function mutateLockedJsonFileSync<T extends object, R>(path: string, mutation: (current: T) => LockedJsonMutation<T, R>, options?: LockedJsonOptions): R;
12
+ /** Serialize an arbitrary same-file read/modify/write sequence across processes. */
13
+ export declare function withFileLockSync<R>(path: string, action: () => R, options?: LockedJsonOptions): R;
14
+ /** Hold the same cross-process lock for the lifetime of an async operation. */
15
+ export declare function withFileLock<R>(path: string, action: () => Promise<R>, options?: LockedJsonOptions): Promise<R>;
@@ -0,0 +1,224 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { atomicWriteJsonFileSync } from "./atomicFile.js";
5
+ const waitArray = new Int32Array(new SharedArrayBuffer(4));
6
+ const lockReleaseRetryCodes = new Set(["EACCES", "EBUSY", "ENOTEMPTY", "EPERM"]);
7
+ const lockReleaseRetryMs = 5_000;
8
+ const lockReleaseRetryDelayMs = 10;
9
+ const deadOwnerGraceMs = 250;
10
+ const lockOwnerFile = "owner.json";
11
+ export function readJsonObjectFileSync(path) {
12
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
13
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
14
+ throw new Error(`Expected a JSON object in ${path}`);
15
+ }
16
+ return parsed;
17
+ }
18
+ export function mutateLockedJsonFileSync(path, mutation, options = {}) {
19
+ return withFileLockSync(path, () => {
20
+ const current = readJsonObjectFileSync(path);
21
+ const outcome = mutation(current);
22
+ if (outcome.next)
23
+ atomicWriteJsonFileSync(path, outcome.next);
24
+ return outcome.result;
25
+ }, options);
26
+ }
27
+ /** Serialize an arbitrary same-file read/modify/write sequence across processes. */
28
+ export function withFileLockSync(path, action, options = {}) {
29
+ const lock = acquireLock(path, options);
30
+ try {
31
+ return action();
32
+ }
33
+ finally {
34
+ releaseLock(lock.path, lock.owner);
35
+ }
36
+ }
37
+ /** Hold the same cross-process lock for the lifetime of an async operation. */
38
+ export async function withFileLock(path, action, options = {}) {
39
+ // Async callers must not use the synchronous Atomics.wait loop: a second
40
+ // request in the same Node event loop would otherwise block the first
41
+ // request from finishing its awaited mutation and releasing the lock.
42
+ const lock = await acquireLockAsync(path, options);
43
+ try {
44
+ return await action();
45
+ }
46
+ finally {
47
+ releaseLock(lock.path, lock.owner);
48
+ }
49
+ }
50
+ async function acquireLockAsync(path, options) {
51
+ const lockPath = `${path}.lock`;
52
+ const owner = `${process.pid}-${randomBytes(8).toString("hex")}`;
53
+ const deadline = Date.now() + (options.waitMs ?? 2000);
54
+ while (true) {
55
+ if (tryCreateLock(lockPath, owner))
56
+ return { path: lockPath, owner };
57
+ if (removeStaleLock(lockPath, options.corruptLockStaleMs ?? 30_000) &&
58
+ tryCreateLock(lockPath, owner)) {
59
+ return { path: lockPath, owner };
60
+ }
61
+ if (Date.now() >= deadline) {
62
+ throw options.busyError?.() ?? new Error(`JSON file is busy: ${path}`);
63
+ }
64
+ await new Promise((resolve) => setTimeout(resolve, lockReleaseRetryDelayMs));
65
+ }
66
+ }
67
+ function acquireLock(path, options) {
68
+ const lockPath = `${path}.lock`;
69
+ const owner = `${process.pid}-${randomBytes(8).toString("hex")}`;
70
+ const deadline = Date.now() + (options.waitMs ?? 2000);
71
+ while (true) {
72
+ if (tryCreateLock(lockPath, owner))
73
+ return { path: lockPath, owner };
74
+ if (removeStaleLock(lockPath, options.corruptLockStaleMs ?? 30_000) &&
75
+ tryCreateLock(lockPath, owner)) {
76
+ return { path: lockPath, owner };
77
+ }
78
+ if (Date.now() >= deadline) {
79
+ throw options.busyError?.() ?? new Error(`JSON file is busy: ${path}`);
80
+ }
81
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
82
+ }
83
+ }
84
+ function tryCreateLock(lockPath, owner) {
85
+ try {
86
+ mkdirSync(lockPath, { mode: 0o700 });
87
+ }
88
+ catch (error) {
89
+ const code = errorCode(error);
90
+ if (code === "EEXIST" || lockReleaseRetryCodes.has(code || ""))
91
+ return false;
92
+ throw error;
93
+ }
94
+ try {
95
+ writeFileSync(join(lockPath, lockOwnerFile), JSON.stringify({
96
+ owner,
97
+ pid: process.pid,
98
+ created_at: new Date().toISOString(),
99
+ }), { encoding: "utf-8", flag: "wx", mode: 0o600 });
100
+ return true;
101
+ }
102
+ catch (error) {
103
+ try {
104
+ rmSync(lockPath, { recursive: true, force: true });
105
+ }
106
+ catch { /* leave fail-closed */ }
107
+ throw error;
108
+ }
109
+ }
110
+ function releaseLock(lockPath, owner) {
111
+ const detachDeadline = Date.now() + lockReleaseRetryMs;
112
+ const releasePath = `${lockPath}.release-${owner}`;
113
+ // Rename the owned lock directory out of the active lock path before
114
+ // recursive cleanup. A new contender can then create lockPath immediately,
115
+ // while Windows rimraf retries cannot delete that replacement lock.
116
+ while (true) {
117
+ const currentOwner = readLock(lockPath)?.owner;
118
+ if (currentOwner !== owner) {
119
+ if (!existsSync(lockPath))
120
+ return;
121
+ if (currentOwner !== undefined)
122
+ return;
123
+ if (Date.now() >= detachDeadline) {
124
+ throw new Error(`Unable to verify lock ownership before release: ${lockPath}`);
125
+ }
126
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
127
+ continue;
128
+ }
129
+ try {
130
+ renameSync(lockPath, releasePath);
131
+ break;
132
+ }
133
+ catch (error) {
134
+ const code = errorCode(error);
135
+ if (code === "ENOENT")
136
+ return;
137
+ if (!code || !lockReleaseRetryCodes.has(code) || Date.now() >= detachDeadline)
138
+ throw error;
139
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
140
+ }
141
+ }
142
+ // Cleanup is no longer ownership-sensitive after the atomic detach, so give
143
+ // it an independent retry budget instead of consuming whatever remained
144
+ // from a slow antivirus-delayed rename.
145
+ const cleanupDeadline = Date.now() + lockReleaseRetryMs;
146
+ while (true) {
147
+ try {
148
+ rmSync(releasePath, { recursive: true, force: false });
149
+ return;
150
+ }
151
+ catch (error) {
152
+ const code = errorCode(error);
153
+ if (code === "ENOENT")
154
+ return;
155
+ if (!code || !lockReleaseRetryCodes.has(code) || Date.now() >= cleanupDeadline)
156
+ throw error;
157
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
158
+ }
159
+ }
160
+ }
161
+ function removeStaleLock(lockPath, corruptLockStaleMs) {
162
+ const lock = readLock(lockPath);
163
+ let ageMs;
164
+ try {
165
+ ageMs = Date.now() - statSync(lockPath).mtimeMs;
166
+ }
167
+ catch (error) {
168
+ if (errorCode(error) === "ENOENT")
169
+ return true;
170
+ throw error;
171
+ }
172
+ const stale = typeof lock?.pid === "number"
173
+ ? !processIsAlive(lock.pid) && ageMs >= deadOwnerGraceMs
174
+ : ageMs >= corruptLockStaleMs;
175
+ if (!stale)
176
+ return false;
177
+ // Re-read immediately before removal so one stale-lock contender cannot
178
+ // delete a replacement acquired after its first observation.
179
+ const latest = readLock(lockPath);
180
+ if (lock?.owner) {
181
+ if (latest?.owner !== lock.owner || latest.pid !== lock.pid)
182
+ return false;
183
+ }
184
+ else if (latest) {
185
+ return false;
186
+ }
187
+ try {
188
+ rmSync(lockPath, { recursive: true, force: false });
189
+ return true;
190
+ }
191
+ catch (error) {
192
+ if (errorCode(error) === "ENOENT")
193
+ return true;
194
+ if (lockReleaseRetryCodes.has(errorCode(error) || ""))
195
+ return false;
196
+ throw error;
197
+ }
198
+ }
199
+ function readLock(path) {
200
+ try {
201
+ const metadata = statSync(path);
202
+ const recordPath = metadata.isDirectory() ? join(path, lockOwnerFile) : path;
203
+ return readJsonObjectFileSync(recordPath);
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ function processIsAlive(pid) {
210
+ try {
211
+ process.kill(pid, 0);
212
+ return true;
213
+ }
214
+ catch (error) {
215
+ // Only ESRCH proves the process does not exist. Access-denied and unknown
216
+ // platform errors must keep the lock fail-closed.
217
+ return errorCode(error) !== "ESRCH";
218
+ }
219
+ }
220
+ function errorCode(error) {
221
+ return error && typeof error === "object" && "code" in error
222
+ ? String(error.code || "")
223
+ : null;
224
+ }
@@ -0,0 +1,11 @@
1
+ export declare const isWindows: boolean;
2
+ export declare const pathSep: string;
3
+ export declare const nullDevice: string;
4
+ export declare function isSamePath(left: string, right: string): boolean;
5
+ /**
6
+ * Returns true when `child` is located inside `parent` (or equals it is NOT
7
+ * treated as a child here — callers that need equality should check it
8
+ * separately). Uses `path.relative` so mixed Windows separators are normalized
9
+ * before comparison, unlike `String.prototype.startsWith`.
10
+ */
11
+ export declare function isPathChildOf(child: string, parent: string): boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cross-platform abstractions for PatchWarden.
3
+ *
4
+ * Centralizes platform detection, path separators, the platform-specific null
5
+ * device, and a path-containment helper that works correctly on Windows where
6
+ * forward and backward slashes may be mixed. Use these helpers instead of
7
+ * hard-coded Unix assumptions so the runtime guard and change capture behave
8
+ * consistently across Windows and Unix hosts.
9
+ */
10
+ import { sep, relative, isAbsolute } from "node:path";
11
+ export const isWindows = process.platform === "win32";
12
+ export const pathSep = sep;
13
+ export const nullDevice = isWindows ? "NUL" : "/dev/null";
14
+ export function isSamePath(left, right) {
15
+ return relative(left, right) === "";
16
+ }
17
+ /**
18
+ * Returns true when `child` is located inside `parent` (or equals it is NOT
19
+ * treated as a child here — callers that need equality should check it
20
+ * separately). Uses `path.relative` so mixed Windows separators are normalized
21
+ * before comparison, unlike `String.prototype.startsWith`.
22
+ */
23
+ export function isPathChildOf(child, parent) {
24
+ const rel = relative(parent, child);
25
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
26
+ }
@@ -0,0 +1,2 @@
1
+ /** Canonical JSON with object keys sorted recursively. */
2
+ export declare function stableJsonStringify(value: unknown): string;
@@ -0,0 +1,12 @@
1
+ /** Canonical JSON with object keys sorted recursively. */
2
+ export function stableJsonStringify(value) {
3
+ if (Array.isArray(value)) {
4
+ return `[${value.map(stableJsonStringify).join(",")}]`;
5
+ }
6
+ if (value !== null && typeof value === "object") {
7
+ const entries = Object.entries(value)
8
+ .sort(([left], [right]) => left.localeCompare(right));
9
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableJsonStringify(entry)}`).join(",")}}`;
10
+ }
11
+ return JSON.stringify(value);
12
+ }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export declare const PATCHWARDEN_VERSION = "1.5.1";
2
- export declare const TOOL_SCHEMA_EPOCH = "2026-07-05-v13";
1
+ export declare const PATCHWARDEN_VERSION = "1.6.1";
2
+ export declare const TOOL_SCHEMA_EPOCH = "2026-07-19-v15";
3
+ export declare const ARTIFACT_SCHEMA_VERSION = "2026-07-19-v15";
package/dist/version.js CHANGED
@@ -1,2 +1,3 @@
1
- export const PATCHWARDEN_VERSION = "1.5.1";
2
- export const TOOL_SCHEMA_EPOCH = "2026-07-05-v13";
1
+ export const PATCHWARDEN_VERSION = "1.6.1";
2
+ export const TOOL_SCHEMA_EPOCH = "2026-07-19-v15";
3
+ export const ARTIFACT_SCHEMA_VERSION = TOOL_SCHEMA_EPOCH;
@@ -1,6 +1,6 @@
1
1
  import { type PatchWardenConfig } from "./config.js";
2
- export type WatcherState = "healthy" | "stale" | "missing" | "unreadable";
3
- export type PendingReason = "queued_waiting_for_watcher" | "queued_but_watcher_stale" | "queued_but_watcher_missing" | "queued_but_watcher_unreadable" | "agent_running" | "verification_running" | "preparing" | "collecting_artifacts" | null;
2
+ export type WatcherState = "healthy" | "degraded" | "stale" | "missing" | "unreadable";
3
+ export type PendingReason = "queued_waiting_for_watcher" | "queued_but_watcher_stale" | "queued_but_watcher_degraded" | "queued_but_watcher_missing" | "queued_but_watcher_unreadable" | "agent_running" | "verification_running" | "preparing" | "collecting_artifacts" | null;
4
4
  export interface WatcherStatusSnapshot {
5
5
  status: WatcherState;
6
6
  available: boolean;
@@ -45,6 +45,22 @@ export function readWatcherStatus(config = getConfig(), nowMs = Date.now()) {
45
45
  throw new Error("invalid heartbeat timestamp");
46
46
  const ageMs = Math.max(0, nowMs - heartbeatMs);
47
47
  const ageSeconds = Math.round(ageMs / 1000);
48
+ if (data.status === "degraded") {
49
+ return {
50
+ status: "degraded",
51
+ available: false,
52
+ stale_after_seconds: staleAfterSeconds,
53
+ last_heartbeat_at: String(data.last_heartbeat_at),
54
+ heartbeat_age_seconds: ageSeconds,
55
+ heartbeat_pid: Number.isInteger(Number(data.pid)) ? Number(data.pid) : null,
56
+ instance_id: typeof data.instance_id === "string" ? data.instance_id : null,
57
+ launcher_pid: Number.isInteger(Number(data.launcher_pid)) ? Number(data.launcher_pid) : null,
58
+ reason: typeof data.last_error === "string" && data.last_error
59
+ ? `Watcher is degraded after repeated tick failures: ${data.last_error}`
60
+ : "Watcher is degraded after repeated tick failures. Restart the PatchWarden watcher.",
61
+ activity: null,
62
+ };
63
+ }
48
64
  const healthy = ageMs < staleAfterSeconds * 1000;
49
65
  if (healthy) {
50
66
  return {
@@ -149,6 +165,8 @@ function checkRunningTaskHeartbeat(config, nowMs, staleAfterSeconds) {
149
165
  }
150
166
  export function derivePendingReason(task, watcher) {
151
167
  if (task.status === "pending") {
168
+ if (watcher.status === "degraded")
169
+ return "queued_but_watcher_degraded";
152
170
  if (watcher.status === "stale")
153
171
  return "queued_but_watcher_stale";
154
172
  if (watcher.status === "missing")