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,251 @@
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
+
6
+ export interface LockedJsonMutation<T extends object, R> {
7
+ next?: T;
8
+ result: R;
9
+ }
10
+
11
+ export interface LockedJsonOptions {
12
+ waitMs?: number;
13
+ corruptLockStaleMs?: number;
14
+ busyError?: () => Error;
15
+ }
16
+
17
+ const waitArray = new Int32Array(new SharedArrayBuffer(4));
18
+ const lockReleaseRetryCodes = new Set(["EACCES", "EBUSY", "ENOTEMPTY", "EPERM"]);
19
+ const lockReleaseRetryMs = 5_000;
20
+ const lockReleaseRetryDelayMs = 10;
21
+ const deadOwnerGraceMs = 250;
22
+ const lockOwnerFile = "owner.json";
23
+
24
+ interface AcquiredLock {
25
+ path: string;
26
+ owner: string;
27
+ }
28
+
29
+ interface LockRecord {
30
+ owner?: string;
31
+ pid?: number;
32
+ }
33
+
34
+ export function readJsonObjectFileSync<T extends object>(path: string): T {
35
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
36
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
37
+ throw new Error(`Expected a JSON object in ${path}`);
38
+ }
39
+ return parsed as T;
40
+ }
41
+
42
+ export function mutateLockedJsonFileSync<T extends object, R>(
43
+ path: string,
44
+ mutation: (current: T) => LockedJsonMutation<T, R>,
45
+ options: LockedJsonOptions = {},
46
+ ): R {
47
+ return withFileLockSync(path, () => {
48
+ const current = readJsonObjectFileSync<T>(path);
49
+ const outcome = mutation(current);
50
+ if (outcome.next) atomicWriteJsonFileSync(path, outcome.next);
51
+ return outcome.result;
52
+ }, options);
53
+ }
54
+
55
+ /** Serialize an arbitrary same-file read/modify/write sequence across processes. */
56
+ export function withFileLockSync<R>(
57
+ path: string,
58
+ action: () => R,
59
+ options: LockedJsonOptions = {},
60
+ ): R {
61
+ const lock = acquireLock(path, options);
62
+ try {
63
+ return action();
64
+ } finally {
65
+ releaseLock(lock.path, lock.owner);
66
+ }
67
+ }
68
+
69
+ /** Hold the same cross-process lock for the lifetime of an async operation. */
70
+ export async function withFileLock<R>(
71
+ path: string,
72
+ action: () => Promise<R>,
73
+ options: LockedJsonOptions = {},
74
+ ): Promise<R> {
75
+ // Async callers must not use the synchronous Atomics.wait loop: a second
76
+ // request in the same Node event loop would otherwise block the first
77
+ // request from finishing its awaited mutation and releasing the lock.
78
+ const lock = await acquireLockAsync(path, options);
79
+ try {
80
+ return await action();
81
+ } finally {
82
+ releaseLock(lock.path, lock.owner);
83
+ }
84
+ }
85
+
86
+ async function acquireLockAsync(path: string, options: LockedJsonOptions): Promise<AcquiredLock> {
87
+ const lockPath = `${path}.lock`;
88
+ const owner = `${process.pid}-${randomBytes(8).toString("hex")}`;
89
+
90
+ const deadline = Date.now() + (options.waitMs ?? 2000);
91
+ while (true) {
92
+ if (tryCreateLock(lockPath, owner)) return { path: lockPath, owner };
93
+ if (
94
+ removeStaleLock(lockPath, options.corruptLockStaleMs ?? 30_000) &&
95
+ tryCreateLock(lockPath, owner)
96
+ ) {
97
+ return { path: lockPath, owner };
98
+ }
99
+ if (Date.now() >= deadline) {
100
+ throw options.busyError?.() ?? new Error(`JSON file is busy: ${path}`);
101
+ }
102
+ await new Promise<void>((resolve) => setTimeout(resolve, lockReleaseRetryDelayMs));
103
+ }
104
+ }
105
+
106
+ function acquireLock(path: string, options: LockedJsonOptions): AcquiredLock {
107
+ const lockPath = `${path}.lock`;
108
+ const owner = `${process.pid}-${randomBytes(8).toString("hex")}`;
109
+
110
+ const deadline = Date.now() + (options.waitMs ?? 2000);
111
+ while (true) {
112
+ if (tryCreateLock(lockPath, owner)) return { path: lockPath, owner };
113
+ if (
114
+ removeStaleLock(lockPath, options.corruptLockStaleMs ?? 30_000) &&
115
+ tryCreateLock(lockPath, owner)
116
+ ) {
117
+ return { path: lockPath, owner };
118
+ }
119
+ if (Date.now() >= deadline) {
120
+ throw options.busyError?.() ?? new Error(`JSON file is busy: ${path}`);
121
+ }
122
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
123
+ }
124
+ }
125
+
126
+ function tryCreateLock(lockPath: string, owner: string): boolean {
127
+ try {
128
+ mkdirSync(lockPath, { mode: 0o700 });
129
+ } catch (error) {
130
+ const code = errorCode(error);
131
+ if (code === "EEXIST" || lockReleaseRetryCodes.has(code || "")) return false;
132
+ throw error;
133
+ }
134
+
135
+ try {
136
+ writeFileSync(join(lockPath, lockOwnerFile), JSON.stringify({
137
+ owner,
138
+ pid: process.pid,
139
+ created_at: new Date().toISOString(),
140
+ }), { encoding: "utf-8", flag: "wx", mode: 0o600 });
141
+ return true;
142
+ } catch (error) {
143
+ try { rmSync(lockPath, { recursive: true, force: true }); } catch { /* leave fail-closed */ }
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ function releaseLock(lockPath: string, owner: string): void {
149
+ const detachDeadline = Date.now() + lockReleaseRetryMs;
150
+ const releasePath = `${lockPath}.release-${owner}`;
151
+
152
+ // Rename the owned lock directory out of the active lock path before
153
+ // recursive cleanup. A new contender can then create lockPath immediately,
154
+ // while Windows rimraf retries cannot delete that replacement lock.
155
+ while (true) {
156
+ const currentOwner = readLock(lockPath)?.owner;
157
+ if (currentOwner !== owner) {
158
+ if (!existsSync(lockPath)) return;
159
+ if (currentOwner !== undefined) return;
160
+ if (Date.now() >= detachDeadline) {
161
+ throw new Error(`Unable to verify lock ownership before release: ${lockPath}`);
162
+ }
163
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
164
+ continue;
165
+ }
166
+ try {
167
+ renameSync(lockPath, releasePath);
168
+ break;
169
+ } catch (error) {
170
+ const code = errorCode(error);
171
+ if (code === "ENOENT") return;
172
+ if (!code || !lockReleaseRetryCodes.has(code) || Date.now() >= detachDeadline) throw error;
173
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
174
+ }
175
+ }
176
+
177
+ // Cleanup is no longer ownership-sensitive after the atomic detach, so give
178
+ // it an independent retry budget instead of consuming whatever remained
179
+ // from a slow antivirus-delayed rename.
180
+ const cleanupDeadline = Date.now() + lockReleaseRetryMs;
181
+ while (true) {
182
+ try {
183
+ rmSync(releasePath, { recursive: true, force: false });
184
+ return;
185
+ } catch (error) {
186
+ const code = errorCode(error);
187
+ if (code === "ENOENT") return;
188
+ if (!code || !lockReleaseRetryCodes.has(code) || Date.now() >= cleanupDeadline) throw error;
189
+ Atomics.wait(waitArray, 0, 0, lockReleaseRetryDelayMs);
190
+ }
191
+ }
192
+ }
193
+
194
+ function removeStaleLock(lockPath: string, corruptLockStaleMs: number): boolean {
195
+ const lock = readLock(lockPath);
196
+ let ageMs: number;
197
+ try {
198
+ ageMs = Date.now() - statSync(lockPath).mtimeMs;
199
+ } catch (error) {
200
+ if (errorCode(error) === "ENOENT") return true;
201
+ throw error;
202
+ }
203
+ const stale = typeof lock?.pid === "number"
204
+ ? !processIsAlive(lock.pid) && ageMs >= deadOwnerGraceMs
205
+ : ageMs >= corruptLockStaleMs;
206
+ if (!stale) return false;
207
+
208
+ // Re-read immediately before removal so one stale-lock contender cannot
209
+ // delete a replacement acquired after its first observation.
210
+ const latest = readLock(lockPath);
211
+ if (lock?.owner) {
212
+ if (latest?.owner !== lock.owner || latest.pid !== lock.pid) return false;
213
+ } else if (latest) {
214
+ return false;
215
+ }
216
+ try {
217
+ rmSync(lockPath, { recursive: true, force: false });
218
+ return true;
219
+ } catch (error) {
220
+ if (errorCode(error) === "ENOENT") return true;
221
+ if (lockReleaseRetryCodes.has(errorCode(error) || "")) return false;
222
+ throw error;
223
+ }
224
+ }
225
+
226
+ function readLock(path: string): LockRecord | null {
227
+ try {
228
+ const metadata = statSync(path);
229
+ const recordPath = metadata.isDirectory() ? join(path, lockOwnerFile) : path;
230
+ return readJsonObjectFileSync<LockRecord>(recordPath);
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+
236
+ function processIsAlive(pid: number): boolean {
237
+ try {
238
+ process.kill(pid, 0);
239
+ return true;
240
+ } catch (error) {
241
+ // Only ESRCH proves the process does not exist. Access-denied and unknown
242
+ // platform errors must keep the lock fail-closed.
243
+ return errorCode(error) !== "ESRCH";
244
+ }
245
+ }
246
+
247
+ function errorCode(error: unknown): string | null {
248
+ return error && typeof error === "object" && "code" in error
249
+ ? String((error as NodeJS.ErrnoException).code || "")
250
+ : null;
251
+ }
@@ -0,0 +1,31 @@
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
+
12
+ export const isWindows: boolean = process.platform === "win32";
13
+
14
+ export const pathSep: string = sep;
15
+
16
+ export const nullDevice: string = isWindows ? "NUL" : "/dev/null";
17
+
18
+ export function isSamePath(left: string, right: string): boolean {
19
+ return relative(left, right) === "";
20
+ }
21
+
22
+ /**
23
+ * Returns true when `child` is located inside `parent` (or equals it is NOT
24
+ * treated as a child here — callers that need equality should check it
25
+ * separately). Uses `path.relative` so mixed Windows separators are normalized
26
+ * before comparison, unlike `String.prototype.startsWith`.
27
+ */
28
+ export function isPathChildOf(child: string, parent: string): boolean {
29
+ const rel = relative(parent, child);
30
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
31
+ }
@@ -0,0 +1,12 @@
1
+ /** Canonical JSON with object keys sorted recursively. */
2
+ export function stableJsonStringify(value: unknown): string {
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 as Record<string, unknown>)
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/src/version.ts 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;
@@ -2,10 +2,11 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { getConfig, getTasksDir, type PatchWardenConfig } from "./config.js";
4
4
 
5
- export type WatcherState = "healthy" | "stale" | "missing" | "unreadable";
5
+ export type WatcherState = "healthy" | "degraded" | "stale" | "missing" | "unreadable";
6
6
  export type PendingReason =
7
7
  | "queued_waiting_for_watcher"
8
8
  | "queued_but_watcher_stale"
9
+ | "queued_but_watcher_degraded"
9
10
  | "queued_but_watcher_missing"
10
11
  | "queued_but_watcher_unreadable"
11
12
  | "agent_running"
@@ -75,6 +76,22 @@ export function readWatcherStatus(
75
76
  if (!Number.isFinite(heartbeatMs)) throw new Error("invalid heartbeat timestamp");
76
77
  const ageMs = Math.max(0, nowMs - heartbeatMs);
77
78
  const ageSeconds = Math.round(ageMs / 1000);
79
+ if (data.status === "degraded") {
80
+ return {
81
+ status: "degraded",
82
+ available: false,
83
+ stale_after_seconds: staleAfterSeconds,
84
+ last_heartbeat_at: String(data.last_heartbeat_at),
85
+ heartbeat_age_seconds: ageSeconds,
86
+ heartbeat_pid: Number.isInteger(Number(data.pid)) ? Number(data.pid) : null,
87
+ instance_id: typeof data.instance_id === "string" ? data.instance_id : null,
88
+ launcher_pid: Number.isInteger(Number(data.launcher_pid)) ? Number(data.launcher_pid) : null,
89
+ reason: typeof data.last_error === "string" && data.last_error
90
+ ? `Watcher is degraded after repeated tick failures: ${data.last_error}`
91
+ : "Watcher is degraded after repeated tick failures. Restart the PatchWarden watcher.",
92
+ activity: null,
93
+ };
94
+ }
78
95
  const healthy = ageMs < staleAfterSeconds * 1000;
79
96
  if (healthy) {
80
97
  return {
@@ -186,6 +203,7 @@ export function derivePendingReason(
186
203
  watcher: WatcherStatusSnapshot
187
204
  ): PendingReason {
188
205
  if (task.status === "pending") {
206
+ if (watcher.status === "degraded") return "queued_but_watcher_degraded";
189
207
  if (watcher.status === "stale") return "queued_but_watcher_stale";
190
208
  if (watcher.status === "missing") return "queued_but_watcher_missing";
191
209
  if (watcher.status === "unreadable") return "queued_but_watcher_unreadable";
@@ -0,0 +1,11 @@
1
+ (function () {
2
+ if (!window.patchwardenDesktop) return;
3
+ var html = document.documentElement;
4
+ var theme = localStorage.getItem("patchwarden.desktop.theme") || "system";
5
+ var dark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
6
+ html.classList.add("pw-desktop", dark ? "dark" : "light");
7
+ html.dataset.theme = dark ? "dark" : "light";
8
+ html.style.backgroundColor = dark ? "#0f1413" : "#eef2f1";
9
+ var language = localStorage.getItem("patchwarden.language");
10
+ if (language === "zh-CN" || language === "en") html.lang = language;
11
+ })();
@@ -0,0 +1,36 @@
1
+ (function () {
2
+ var api = window.patchwardenDesktop;
3
+ if (!api) return;
4
+
5
+ var html = document.documentElement;
6
+ html.classList.add("pw-desktop");
7
+
8
+ var nativeFetch = window.fetch.bind(window);
9
+ window.fetch = function (input, init) {
10
+ var options = init || {};
11
+ var method = String(options.method || "GET").toUpperCase();
12
+ if (method !== "GET" || options.signal) return nativeFetch(input, options);
13
+ var controller = new AbortController();
14
+ var timer = window.setTimeout(function () { controller.abort(); }, 10000);
15
+ return nativeFetch(input, Object.assign({}, options, { signal: controller.signal })).catch(function (error) {
16
+ if (error && error.name === "AbortError") throw new Error("请求超过 10 秒,请重试");
17
+ throw error;
18
+ }).finally(function () { window.clearTimeout(timer); });
19
+ };
20
+
21
+ function applyTheme(theme) {
22
+ var dark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
23
+ html.dataset.theme = dark ? "dark" : "light";
24
+ html.classList.toggle("dark", dark);
25
+ html.classList.toggle("light", !dark);
26
+ localStorage.setItem("patchwarden.desktop.theme", theme || "system");
27
+ html.style.backgroundColor = dark ? "#0f1413" : "#eef2f1";
28
+ }
29
+
30
+ window.patchwardenApplyTheme = applyTheme;
31
+
32
+ api.getPreferences().then(function (preferences) { applyTheme(preferences.theme || "system"); });
33
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function () {
34
+ api.getPreferences().then(function (preferences) { if (preferences.theme === "system") applyTheme("system"); });
35
+ });
36
+ })();
package/ui/desktop.css ADDED
@@ -0,0 +1,160 @@
1
+ /* Shared responsive shell plus desktop-only theme refinements. */
2
+ .desktop-only-nav { display: none !important; }
3
+
4
+ @view-transition { navigation: auto; }
5
+ ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 120ms; animation-timing-function: ease-out; }
6
+ @media (prefers-reduced-motion: reduce) {
7
+ ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0s; }
8
+ }
9
+
10
+ html, body { max-width: 100%; overflow-x: hidden; }
11
+ .pw-dashboard-toolbar {
12
+ display: grid !important;
13
+ grid-template-columns: auto minmax(180px, 1fr) auto;
14
+ align-items: center;
15
+ gap: 10px;
16
+ min-width: 0;
17
+ }
18
+ .pw-toolbar-actions, .pw-toolbar-links { display: flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; }
19
+ .pw-toolbar-repo { min-width: 0; }
20
+ .pw-toolbar-repo select { width: 100%; min-width: 0 !important; max-width: 100%; }
21
+ @media (min-width: 1121px) {
22
+ .pw-dashboard-toolbar { grid-template-columns: max-content minmax(160px, 1fr) max-content; }
23
+ .pw-toolbar-actions, .pw-toolbar-links { flex-wrap: nowrap; }
24
+ .pw-dashboard-toolbar .pw-toolbar-actions button,
25
+ .pw-dashboard-toolbar .pw-toolbar-links a,
26
+ .pw-dashboard-toolbar .pw-toolbar-links button { padding-left: 9px !important; padding-right: 9px !important; }
27
+ }
28
+
29
+ .pw-app-shell { display: flex; height: 100vh; margin: 0; color: var(--pw-text-primary); background: var(--pw-bg-elevated); font-family: var(--pw-font-sans); }
30
+ .pw-sidebar { display: flex; flex: 0 0 220px; flex-direction: column; height: 100vh; background: var(--pw-bg); border-right: 1px solid var(--pw-border); }
31
+ .pw-brand { display: flex; align-items: center; gap: 10px; min-height: 58px; padding: 0 18px; border-bottom: 1px solid var(--pw-border-subtle); }
32
+ .pw-brand svg { width: 21px; color: var(--pw-accent); }
33
+ .pw-sidebar nav { display: flex; flex-direction: column; gap: 4px; padding: 14px 10px; overflow-y: auto; }
34
+ .pw-sidebar nav a { display: flex; align-items: center; gap: 10px; min-height: 38px; padding: 0 10px; color: var(--pw-text-secondary); text-decoration: none; border-radius: 6px; }
35
+ .pw-sidebar nav a:hover { color: var(--pw-text-primary); background: var(--pw-bg-hover); }
36
+ .pw-sidebar nav a[data-active="true"] { color: var(--pw-accent); background: var(--pw-accent-subtle); border-left: 2px solid var(--pw-accent); }
37
+ .pw-sidebar nav svg { width: 17px; flex: none; }
38
+ .pw-page { display: flex; flex: 1; min-width: 0; flex-direction: column; height: 100vh; }
39
+ .pw-page-header { display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 74px; padding: 12px 24px; background: var(--pw-bg-surface); border-bottom: 1px solid var(--pw-border); }
40
+ .pw-page-header h1 { margin: 0; font-size: 19px; letter-spacing: 0; }
41
+ .pw-page-header p { margin: 4px 0 0; color: var(--pw-text-secondary); font-size: 12px; }
42
+ .pw-page-header select { min-height: 34px; max-width: 190px; padding: 0 28px 0 10px; color: var(--pw-text-primary); background: var(--pw-bg-elevated); border: 1px solid var(--pw-border); border-radius: 6px; }
43
+ .getting-started-main { flex: 1; min-width: 0; padding: 22px 24px; overflow-y: auto; }
44
+ .readiness-list { max-width: 980px; background: var(--pw-bg-surface); border: 1px solid var(--pw-border); border-radius: 7px; }
45
+ .readiness-item { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 15px; min-height: 112px; padding: 18px; border-bottom: 1px solid var(--pw-border-subtle); }
46
+ .readiness-item:last-child { border-bottom: 0; }
47
+ .check-index { display: grid; width: 30px; height: 30px; place-items: center; color: var(--pw-text-secondary); border: 1px solid var(--pw-border); border-radius: 50%; font: 600 12px var(--pw-font-mono); }
48
+ .readiness-item[data-state="ready"] .check-index { color: var(--pw-text-inverse); background: var(--pw-state-success); border-color: var(--pw-state-success); }
49
+ .check-heading { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
50
+ .check-heading h2 { margin: 0; font-size: 14px; letter-spacing: 0; }
51
+ .readiness-item p { margin: 7px 0 0; color: var(--pw-text-secondary); font-size: 12px; line-height: 1.5; }
52
+ .readiness-item .reason-detail { color: var(--pw-state-error); }
53
+ .check-status { padding: 3px 7px; color: var(--pw-state-warning); background: var(--pw-state-warning-bg); border-radius: 4px; font-size: 11px; }
54
+ .readiness-item[data-state="ready"] .check-status { color: var(--pw-state-success); background: var(--pw-state-success-bg); }
55
+ .check-status.manual { color: var(--pw-text-secondary); background: var(--pw-bg-hover); }
56
+ .primary-command { display: none; align-items: center; justify-content: center; min-height: 36px; padding: 0 13px; color: var(--pw-text-inverse); background: var(--pw-accent); border: 1px solid var(--pw-accent); border-radius: 6px; text-decoration: none; font: 600 12px var(--pw-font-sans); cursor: pointer; white-space: nowrap; }
57
+ .readiness-item[data-primary-action="true"] .primary-command { display: inline-flex; }
58
+ .primary-command:disabled { cursor: wait; opacity: .62; }
59
+ .guide-band { max-width: 980px; margin-top: 16px; padding: 18px 0; }
60
+ .guide-band h2 { margin: 0 0 10px; font-size: 13px; }
61
+ .guide-links { display: flex; gap: 16px; flex-wrap: wrap; }
62
+ .guide-links a { color: var(--pw-accent); font-size: 12px; }
63
+ .pw-toast { position: fixed; right: 20px; bottom: 20px; max-width: min(360px, calc(100vw - 40px)); padding: 10px 13px; color: var(--pw-text-inverse); background: var(--pw-text-primary); border-radius: 6px; opacity: 0; pointer-events: none; transform: translateY(8px); transition: .18s ease; }
64
+ .pw-toast.visible { opacity: 1; transform: translateY(0); }
65
+ .pw-language-switcher { min-height: 34px; max-width: 190px; margin-left: auto; padding: 0 28px 0 10px; color: var(--pw-text-primary); background: var(--pw-bg-elevated); border: 1px solid var(--pw-border); border-radius: 6px; }
66
+ aside > .pw-language-switcher { width: calc(100% - 20px); max-width: none; margin: 10px; flex: none; }
67
+
68
+ html.pw-desktop {
69
+ --pw-bg: #eef2f1;
70
+ --pw-bg-elevated: #f7f9f8;
71
+ --pw-bg-surface: #ffffff;
72
+ --pw-bg-hover: #e6ece9;
73
+ --pw-bg-active: #dbe5e1;
74
+ --pw-border: #cad4d0;
75
+ --pw-border-subtle: #dce3e0;
76
+ --pw-border-focus: #167a58;
77
+ --pw-text-primary: #192521;
78
+ --pw-text-secondary: #52625c;
79
+ --pw-text-tertiary: #74837e;
80
+ --pw-text-inverse: #ffffff;
81
+ --pw-text-accent: #167a58;
82
+ --pw-accent: #167a58;
83
+ --pw-accent-hover: #105f45;
84
+ --pw-accent-muted: rgba(22, 122, 88, 0.11);
85
+ --pw-accent-subtle: rgba(22, 122, 88, 0.06);
86
+ --pw-code-bg: #18201e;
87
+ color-scheme: light;
88
+ }
89
+
90
+ html.pw-desktop.dark {
91
+ --pw-bg: #0f1413;
92
+ --pw-bg-elevated: #141a18;
93
+ --pw-bg-surface: #1b2321;
94
+ --pw-bg-hover: #25302d;
95
+ --pw-bg-active: #2c3935;
96
+ --pw-border: #34423e;
97
+ --pw-border-subtle: #293530;
98
+ --pw-text-primary: #edf4f1;
99
+ --pw-text-secondary: #a0afa9;
100
+ --pw-text-tertiary: #71817b;
101
+ --pw-accent: #43c798;
102
+ --pw-accent-hover: #60d8ad;
103
+ --pw-text-accent: #43c798;
104
+ --pw-accent-muted: rgba(67, 199, 152, 0.13);
105
+ --pw-accent-subtle: rgba(67, 199, 152, 0.07);
106
+ color-scheme: dark;
107
+ }
108
+
109
+ html.pw-desktop body { background: var(--pw-bg) !important; color: var(--pw-text-primary) !important; }
110
+ html.pw-desktop .desktop-only-nav { display: flex !important; }
111
+ html.pw-desktop aside { width: 220px !important; background: var(--pw-bg) !important; }
112
+ html.pw-desktop aside > div:first-child { padding-top: 17px !important; padding-bottom: 17px !important; }
113
+ html.pw-desktop header { min-height: 58px; background: var(--pw-bg-surface) !important; }
114
+ html.pw-desktop main { background: var(--pw-bg-elevated) !important; }
115
+ html.pw-desktop section,
116
+ html.pw-desktop table,
117
+ html.pw-desktop .bg-white,
118
+ html.pw-desktop .bg-gray-50,
119
+ html.pw-desktop .bg-slate-50 { background-color: var(--pw-bg-surface) !important; }
120
+ html.pw-desktop input,
121
+ html.pw-desktop select,
122
+ html.pw-desktop textarea { background-color: var(--pw-bg-elevated) !important; color: var(--pw-text-primary) !important; border-color: var(--pw-border) !important; }
123
+ html.pw-desktop .text-gray-900,
124
+ html.pw-desktop .text-slate-900 { color: var(--pw-text-primary) !important; }
125
+ html.pw-desktop .text-gray-500,
126
+ html.pw-desktop .text-gray-600,
127
+ html.pw-desktop .text-slate-500,
128
+ html.pw-desktop .text-slate-600 { color: var(--pw-text-secondary) !important; }
129
+ html.pw-desktop .border-gray-200,
130
+ html.pw-desktop .border-slate-200 { border-color: var(--pw-border) !important; }
131
+ html.pw-desktop [data-nav-key] { border-radius: 6px !important; }
132
+ html.pw-desktop [data-nav-key][data-active="true"] { border-left-width: 2px !important; }
133
+ html.pw-desktop button, html.pw-desktop select, html.pw-desktop input { border-radius: 6px !important; letter-spacing: 0; }
134
+ html.pw-desktop .rounded-lg { border-radius: 7px !important; }
135
+ html.pw-desktop .rounded-xl { border-radius: 8px !important; }
136
+ html.pw-desktop [style*="box-shadow"] { box-shadow: none !important; }
137
+ html.pw-desktop .pw-desktop-badge { color: var(--pw-accent); background: var(--pw-accent-subtle); border: 1px solid var(--pw-border); }
138
+
139
+ @media (max-width: 1050px) {
140
+ html.pw-desktop aside { width: 196px !important; }
141
+ html.pw-desktop main { padding: 18px !important; }
142
+ }
143
+
144
+ @media (max-width: 1120px) {
145
+ .pw-dashboard-toolbar { grid-template-columns: minmax(0, 1fr) auto; }
146
+ .pw-toolbar-actions { grid-column: 1 / -1; }
147
+ .pw-toolbar-links { justify-content: flex-end; }
148
+ }
149
+
150
+ @media (max-width: 980px) {
151
+ .pw-sidebar { flex-basis: 190px; }
152
+ .pw-page-header, .getting-started-main { padding-left: 18px; padding-right: 18px; }
153
+ .readiness-item { grid-template-columns: 34px minmax(0, 1fr); }
154
+ .readiness-item .primary-command { grid-column: 2; justify-self: start; }
155
+ }
156
+
157
+ @media (max-width: 760px) {
158
+ .pw-dashboard-toolbar { grid-template-columns: 1fr; }
159
+ .pw-toolbar-actions, .pw-toolbar-links { flex-wrap: wrap; justify-content: flex-start; }
160
+ }
@@ -0,0 +1,93 @@
1
+ (function () {
2
+ var api = window.patchwardenDesktop;
3
+ var i18n = window.PatchWardenI18n;
4
+ var statusData = null;
5
+ var tunnelData = null;
6
+ var coreStarting = false;
7
+ var lastCoreReady = false;
8
+ function tr(key) { return i18n ? i18n.t(key) : key; }
9
+ function item(name) { return document.querySelector('[data-check="' + name + '"]'); }
10
+ function reasonText(reason, fallback) {
11
+ if (!reason) return "";
12
+ var key = "reason." + reason; var translated = tr(key);
13
+ return translated === key ? (fallback || reason) : translated;
14
+ }
15
+ function render(name, state, message, reason, fallback) {
16
+ var root = item(name); if (!root) return;
17
+ root.dataset.state = state;
18
+ root.querySelector("[data-status]").textContent = tr(state === "ready" ? "home.ready" : state === "manual" ? "home.manual" : "home.needsAction");
19
+ root.querySelector("[data-message]").textContent = tr(message);
20
+ var detail = root.querySelector("[data-reason]"); if (detail) detail.textContent = reasonText(reason, fallback);
21
+ }
22
+ function renderLiveCheck(name, check) {
23
+ if (!check) return;
24
+ var root = item(name); if (!root) return;
25
+ var state = check.state === "ready" ? "ready" : check.state === "optional" ? "manual" : "action";
26
+ root.dataset.state = state;
27
+ root.querySelector("[data-status]").textContent = tr(state === "ready" ? "home.ready" : state === "manual" ? "home.manual" : "home.needsAction");
28
+ root.querySelector("[data-message]").textContent = check.detail || "";
29
+ var detail = root.querySelector("[data-reason]"); if (detail) detail.textContent = "";
30
+ }
31
+ function setPrimaryAction(name) {
32
+ ["workspace", "core", "tunnel", "chatgpt"].forEach(function (key) {
33
+ var root = item(key);
34
+ if (root) root.dataset.primaryAction = key === name ? "true" : "false";
35
+ });
36
+ }
37
+ function syncCoreButton() {
38
+ var button = document.getElementById("startCore");
39
+ button.disabled = coreStarting || lastCoreReady;
40
+ button.textContent = tr(coreStarting ? "home.startingCore" : lastCoreReady ? "home.coreRunning" : "home.startCore");
41
+ }
42
+ async function fetchJson(url, options) {
43
+ var response = await fetch(url, options); var body = await response.json().catch(function () { return {}; });
44
+ if (!response.ok || body.ok === false) { var error = new Error(body.error || "Request failed"); error.reasonCode = body.reason_code; error.nextSteps = body.next_steps || []; throw error; }
45
+ return body;
46
+ }
47
+ async function refresh() {
48
+ statusData = await fetchJson("/api/status");
49
+ var agents = Array.isArray(statusData.agents) ? statusData.agents : [];
50
+ var workspaceReady = !!statusData.workspace_root && agents.some(function (agent) { return agent && agent.available; });
51
+ var coreAvailable = !!(statusData.core && statusData.core.available);
52
+ var watcherReady = !!(statusData.watcher && statusData.watcher.status === "healthy");
53
+ var coreReady = coreAvailable && watcherReady;
54
+ var coreTunnel = statusData.tunnel && statusData.tunnel.core ? statusData.tunnel.core : {};
55
+ var tunnelReady = coreTunnel.ready === true;
56
+
57
+ render("workspace", workspaceReady ? "ready" : "action", workspaceReady ? "home.workspaceReady" : "home.workspaceMissing", workspaceReady ? null : "workspace_agent_missing");
58
+ render("core", coreReady ? "ready" : "action", coreReady ? "home.coreReady" : "home.coreMissing", coreReady ? null : (statusData.watcher && statusData.watcher.reason) || (statusData.core && statusData.core.reason) || "watcher_unhealthy");
59
+ render("tunnel", tunnelReady ? "ready" : "action", tunnelReady ? "home.tunnelReady" : "home.tunnelMissing", tunnelReady ? null : coreTunnel.reason_code || "tunnel_not_ready");
60
+ render("chatgpt", "manual", "home.chatgptManual", null);
61
+
62
+ lastCoreReady = coreReady;
63
+ syncCoreButton();
64
+ setPrimaryAction(!workspaceReady ? "workspace" : !coreReady ? "core" : !tunnelReady ? "tunnel" : "chatgpt");
65
+ }
66
+ async function startCore() {
67
+ coreStarting = true;
68
+ syncCoreButton();
69
+ render("core", "action", "home.coreStarting");
70
+ try {
71
+ var token = await fetchJson("/control-token.json");
72
+ await fetchJson("/api/core/start", { method: "POST", headers: { "X-PatchWarden-Control-Token": token.token } });
73
+ await refresh();
74
+ } catch (error) {
75
+ lastCoreReady = false;
76
+ var fallback = error.message + (error.nextSteps && error.nextSteps[0] ? " " + error.nextSteps[0] : "");
77
+ render("core", "action", "home.startFailed", error.reasonCode || "start_failed", fallback);
78
+ toastText(reasonText(error.reasonCode || "start_failed", fallback));
79
+ } finally {
80
+ coreStarting = false;
81
+ syncCoreButton();
82
+ }
83
+ }
84
+ function toastText(value) { var node = document.getElementById("toast"); node.textContent = value; node.classList.add("visible"); setTimeout(function () { node.classList.remove("visible"); }, 4000); }
85
+ function toast(key) { toastText(tr(key)); }
86
+ document.getElementById("startCore").addEventListener("click", startCore);
87
+ document.getElementById("copyPrompt").addEventListener("click", function () { navigator.clipboard.writeText("Call health_check and report Core readiness, Watcher health, active profile, and the discovered tool count.").then(function () { toast("home.copied"); }); });
88
+ var language = document.getElementById("language");
89
+ window.addEventListener("patchwarden:i18nready", function () { language.value = i18n.getSelectedLanguage(); void refresh(); });
90
+ language.addEventListener("change", function () { void i18n.setLanguage(language.value).then(refresh); });
91
+ window.addEventListener("patchwarden:languagechange", function () { if (statusData) void refresh(); });
92
+ if (window.lucide) window.lucide.createIcons();
93
+ })();