@ashlr/hub 3.12.0 → 3.14.0

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 (719) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +29 -16
  3. package/dist/authority-surface.json +274 -148
  4. package/dist/build-identity.json +2 -2
  5. package/dist/cli/authority.d.ts +145 -33
  6. package/dist/cli/authority.js +1292 -315
  7. package/dist/cli/authority.js.map +1 -1
  8. package/dist/cli/cloud.js +12 -3
  9. package/dist/cli/cloud.js.map +1 -1
  10. package/dist/cli/comms.d.ts +33 -19
  11. package/dist/cli/comms.js +128 -151
  12. package/dist/cli/comms.js.map +1 -1
  13. package/dist/cli/completions.js +1 -1
  14. package/dist/cli/completions.js.map +1 -1
  15. package/dist/cli/daemon.js +29 -0
  16. package/dist/cli/daemon.js.map +1 -1
  17. package/dist/cli/director.d.ts +8 -12
  18. package/dist/cli/director.js +17 -52
  19. package/dist/cli/director.js.map +1 -1
  20. package/dist/cli/fleet.js +4 -1
  21. package/dist/cli/fleet.js.map +1 -1
  22. package/dist/cli/help.js +1 -0
  23. package/dist/cli/help.js.map +1 -1
  24. package/dist/cli/leader.d.ts +27 -3
  25. package/dist/cli/leader.js +253 -4
  26. package/dist/cli/leader.js.map +1 -1
  27. package/dist/core/authority/protected-paths.js +11 -0
  28. package/dist/core/authority/protected-paths.js.map +1 -1
  29. package/dist/core/authority/resident.d.ts +101 -0
  30. package/dist/core/authority/resident.js +246 -0
  31. package/dist/core/authority/resident.js.map +1 -0
  32. package/dist/core/authority/server-enforcement.d.ts +110 -0
  33. package/dist/core/authority/server-enforcement.js +260 -0
  34. package/dist/core/authority/server-enforcement.js.map +1 -0
  35. package/dist/core/authority/standing-grant.d.ts +24 -4
  36. package/dist/core/authority/standing-grant.js +19 -3
  37. package/dist/core/authority/standing-grant.js.map +1 -1
  38. package/dist/core/authority/types.d.ts +43 -0
  39. package/dist/core/authority/types.js +6 -0
  40. package/dist/core/authority/types.js.map +1 -1
  41. package/dist/core/cloud/budget.js +11 -1
  42. package/dist/core/cloud/budget.js.map +1 -1
  43. package/dist/core/cloud/cloud-api.d.ts +25 -2
  44. package/dist/core/cloud/cloud-api.js +139 -5
  45. package/dist/core/cloud/cloud-api.js.map +1 -1
  46. package/dist/core/cloud/pr-actions.d.ts +93 -0
  47. package/dist/core/cloud/pr-actions.js +436 -0
  48. package/dist/core/cloud/pr-actions.js.map +1 -0
  49. package/dist/core/cloud/pr-preview.d.ts +128 -0
  50. package/dist/core/cloud/pr-preview.js +228 -0
  51. package/dist/core/cloud/pr-preview.js.map +1 -0
  52. package/dist/core/cloud/store.d.ts +2 -0
  53. package/dist/core/cloud/store.js +28 -0
  54. package/dist/core/cloud/store.js.map +1 -1
  55. package/dist/core/cloud/timeline-api.d.ts +24 -0
  56. package/dist/core/cloud/timeline-api.js +46 -0
  57. package/dist/core/cloud/timeline-api.js.map +1 -0
  58. package/dist/core/cloud/timeline-types.d.ts +62 -0
  59. package/dist/core/cloud/timeline-types.js +36 -0
  60. package/dist/core/cloud/timeline-types.js.map +1 -0
  61. package/dist/core/cloud/timeline.d.ts +102 -0
  62. package/dist/core/cloud/timeline.js +925 -0
  63. package/dist/core/cloud/timeline.js.map +1 -0
  64. package/dist/core/cloud/tracker.js +67 -0
  65. package/dist/core/cloud/tracker.js.map +1 -1
  66. package/dist/core/cloud/types.d.ts +38 -0
  67. package/dist/core/cloud/types.js +1 -1
  68. package/dist/core/cloud/types.js.map +1 -1
  69. package/dist/core/comms/change-digest.d.ts +130 -0
  70. package/dist/core/comms/change-digest.js +451 -0
  71. package/dist/core/comms/change-digest.js.map +1 -0
  72. package/dist/core/comms/director-prompt.d.ts +4 -5
  73. package/dist/core/comms/director-prompt.js +4 -71
  74. package/dist/core/comms/director-prompt.js.map +1 -1
  75. package/dist/core/comms/director.d.ts +24 -61
  76. package/dist/core/comms/director.js +29 -382
  77. package/dist/core/comms/director.js.map +1 -1
  78. package/dist/core/comms/dispatch.d.ts +37 -12
  79. package/dist/core/comms/dispatch.js +244 -177
  80. package/dist/core/comms/dispatch.js.map +1 -1
  81. package/dist/core/comms/elon-dialogue.d.ts +23 -15
  82. package/dist/core/comms/elon-dialogue.js +36 -307
  83. package/dist/core/comms/elon-dialogue.js.map +1 -1
  84. package/dist/core/comms/fleet-pulse.js +4 -7
  85. package/dist/core/comms/fleet-pulse.js.map +1 -1
  86. package/dist/core/comms/handlers.d.ts +10 -6
  87. package/dist/core/comms/handlers.js +14 -48
  88. package/dist/core/comms/handlers.js.map +1 -1
  89. package/dist/core/comms/migrations.d.ts +47 -0
  90. package/dist/core/comms/migrations.js +125 -0
  91. package/dist/core/comms/migrations.js.map +1 -0
  92. package/dist/core/comms/requests.d.ts +32 -5
  93. package/dist/core/comms/requests.js +101 -5
  94. package/dist/core/comms/requests.js.map +1 -1
  95. package/dist/core/comms/telegram-channel.d.ts +104 -0
  96. package/dist/core/comms/telegram-channel.js +622 -0
  97. package/dist/core/comms/telegram-channel.js.map +1 -0
  98. package/dist/core/comms/telegram-thread-map.d.ts +56 -0
  99. package/dist/core/comms/telegram-thread-map.js +144 -0
  100. package/dist/core/comms/telegram-thread-map.js.map +1 -0
  101. package/dist/core/daemon/capacity-publisher.d.ts +8 -1
  102. package/dist/core/daemon/capacity-publisher.js +40 -2
  103. package/dist/core/daemon/capacity-publisher.js.map +1 -1
  104. package/dist/core/daemon/loop.js +18 -4
  105. package/dist/core/daemon/loop.js.map +1 -1
  106. package/dist/core/daemon/service-install-authority.d.ts +10 -2
  107. package/dist/core/daemon/service-install-authority.js +14 -5
  108. package/dist/core/daemon/service-install-authority.js.map +1 -1
  109. package/dist/core/daemon/service.d.ts +15 -0
  110. package/dist/core/daemon/service.js +26 -0
  111. package/dist/core/daemon/service.js.map +1 -1
  112. package/dist/core/daemon/tick-deadline.d.ts +44 -0
  113. package/dist/core/daemon/tick-deadline.js +83 -0
  114. package/dist/core/daemon/tick-deadline.js.map +1 -0
  115. package/dist/core/daemon/tick-hooks.d.ts +7 -0
  116. package/dist/core/daemon/tick-hooks.js.map +1 -1
  117. package/dist/core/daemon/tick-progress.d.ts +39 -0
  118. package/dist/core/daemon/tick-progress.js +194 -0
  119. package/dist/core/daemon/tick-progress.js.map +1 -0
  120. package/dist/core/dashboard.js +3 -1
  121. package/dist/core/dashboard.js.map +1 -1
  122. package/dist/core/fleet/cloud-intake.d.ts +95 -0
  123. package/dist/core/fleet/cloud-intake.js +557 -0
  124. package/dist/core/fleet/cloud-intake.js.map +1 -0
  125. package/dist/core/fleet/dispatch-router.d.ts +13 -0
  126. package/dist/core/fleet/dispatch-router.js +32 -8
  127. package/dist/core/fleet/dispatch-router.js.map +1 -1
  128. package/dist/core/fleet/fleet-merge-state.d.ts +16 -0
  129. package/dist/core/fleet/fleet-merge-state.js.map +1 -1
  130. package/dist/core/fleet/host-merge.d.ts +14 -0
  131. package/dist/core/fleet/host-merge.js +24 -2
  132. package/dist/core/fleet/host-merge.js.map +1 -1
  133. package/dist/core/fleet/merge-gates.d.ts +21 -0
  134. package/dist/core/fleet/merge-gates.js +39 -5
  135. package/dist/core/fleet/merge-gates.js.map +1 -1
  136. package/dist/core/fleet/mirrors.d.ts +162 -6
  137. package/dist/core/fleet/mirrors.js +1051 -49
  138. package/dist/core/fleet/mirrors.js.map +1 -1
  139. package/dist/core/fleet/standing-merge-pass.d.ts +8 -0
  140. package/dist/core/fleet/standing-merge-pass.js +102 -1
  141. package/dist/core/fleet/standing-merge-pass.js.map +1 -1
  142. package/dist/core/fleet/status.d.ts +17 -0
  143. package/dist/core/fleet/status.js +22 -0
  144. package/dist/core/fleet/status.js.map +1 -1
  145. package/dist/core/fleet/tick-hooks-live.d.ts +10 -1
  146. package/dist/core/fleet/tick-hooks-live.js +57 -8
  147. package/dist/core/fleet/tick-hooks-live.js.map +1 -1
  148. package/dist/core/fleet/verify-check-run.d.ts +90 -0
  149. package/dist/core/fleet/verify-check-run.js +274 -0
  150. package/dist/core/fleet/verify-check-run.js.map +1 -0
  151. package/dist/core/git.d.ts +17 -0
  152. package/dist/core/git.js +124 -25
  153. package/dist/core/git.js.map +1 -1
  154. package/dist/core/goals/open-goals.d.ts +47 -0
  155. package/dist/core/goals/open-goals.js +33 -0
  156. package/dist/core/goals/open-goals.js.map +1 -0
  157. package/dist/core/inbox/merge.d.ts +5 -0
  158. package/dist/core/inbox/merge.js +58 -2
  159. package/dist/core/inbox/merge.js.map +1 -1
  160. package/dist/core/integrations/locus.d.ts +28 -0
  161. package/dist/core/integrations/locus.js +155 -30
  162. package/dist/core/integrations/locus.js.map +1 -1
  163. package/dist/core/integrations/telegram-format.d.ts +40 -0
  164. package/dist/core/integrations/telegram-format.js +123 -0
  165. package/dist/core/integrations/telegram-format.js.map +1 -0
  166. package/dist/core/integrations/telegram.d.ts +60 -4
  167. package/dist/core/integrations/telegram.js +161 -28
  168. package/dist/core/integrations/telegram.js.map +1 -1
  169. package/dist/core/mcp-registry.d.ts +12 -0
  170. package/dist/core/mcp-registry.js +70 -0
  171. package/dist/core/mcp-registry.js.map +1 -1
  172. package/dist/core/observability/rollup.d.ts +12 -10
  173. package/dist/core/observability/rollup.js +44 -17
  174. package/dist/core/observability/rollup.js.map +1 -1
  175. package/dist/core/resources/connection-monitor.d.ts +9 -0
  176. package/dist/core/resources/connection-monitor.js +19 -9
  177. package/dist/core/resources/connection-monitor.js.map +1 -1
  178. package/dist/core/resources/quota-refresh-lease.d.ts +17 -0
  179. package/dist/core/resources/quota-refresh-lease.js +34 -1
  180. package/dist/core/resources/quota-refresh-lease.js.map +1 -1
  181. package/dist/core/routing/budget-api.d.ts +18 -1
  182. package/dist/core/routing/budget-api.js +120 -10
  183. package/dist/core/routing/budget-api.js.map +1 -1
  184. package/dist/core/routing/budget-store.d.ts +22 -1
  185. package/dist/core/routing/budget-store.js +22 -8
  186. package/dist/core/routing/budget-store.js.map +1 -1
  187. package/dist/core/routing/readiness-types.d.ts +82 -0
  188. package/dist/core/routing/readiness-types.js +23 -0
  189. package/dist/core/routing/readiness-types.js.map +1 -0
  190. package/dist/core/routing/readiness.d.ts +103 -0
  191. package/dist/core/routing/readiness.js +480 -0
  192. package/dist/core/routing/readiness.js.map +1 -0
  193. package/dist/core/sandbox/confine.js +22 -1
  194. package/dist/core/sandbox/confine.js.map +1 -1
  195. package/dist/core/sandbox/policy.d.ts +23 -0
  196. package/dist/core/sandbox/policy.js +100 -0
  197. package/dist/core/sandbox/policy.js.map +1 -1
  198. package/dist/core/types.d.ts +13 -0
  199. package/dist/core/types.js.map +1 -1
  200. package/dist/core/universe/builtins/preparation/manifest.json +1 -1
  201. package/dist/core/universe/builtins/preparation/preparation-bridge.mjs +1204 -939
  202. package/dist/core/universe/builtins/preparation/preparation-verification-fixtures.mjs +1319 -1054
  203. package/dist/core/usage/frontier-usage.d.ts +20 -2
  204. package/dist/core/usage/frontier-usage.js +75 -17
  205. package/dist/core/usage/frontier-usage.js.map +1 -1
  206. package/dist/core/verse/account-health.js +9 -3
  207. package/dist/core/verse/account-health.js.map +1 -1
  208. package/dist/core/verse/accounts.d.ts +45 -0
  209. package/dist/core/verse/accounts.js +246 -17
  210. package/dist/core/verse/accounts.js.map +1 -1
  211. package/dist/core/verse/activity-api.d.ts +5 -1
  212. package/dist/core/verse/activity-api.js +62 -14
  213. package/dist/core/verse/activity-api.js.map +1 -1
  214. package/dist/core/verse/apps-api.js +23 -11
  215. package/dist/core/verse/apps-api.js.map +1 -1
  216. package/dist/core/verse/apps.d.ts +11 -9
  217. package/dist/core/verse/apps.js +2 -2
  218. package/dist/core/verse/apps.js.map +1 -1
  219. package/dist/core/verse/authority-api.d.ts +17 -3
  220. package/dist/core/verse/authority-api.js +122 -7
  221. package/dist/core/verse/authority-api.js.map +1 -1
  222. package/dist/core/verse/autonomy-ladder.d.ts +172 -0
  223. package/dist/core/verse/autonomy-ladder.js +251 -0
  224. package/dist/core/verse/autonomy-ladder.js.map +1 -0
  225. package/dist/core/verse/context-fit.js +7 -5
  226. package/dist/core/verse/context-fit.js.map +1 -1
  227. package/dist/core/verse/control-api.d.ts +23 -1
  228. package/dist/core/verse/control-api.js +117 -10
  229. package/dist/core/verse/control-api.js.map +1 -1
  230. package/dist/core/verse/fleet-live-api.js +13 -5
  231. package/dist/core/verse/fleet-live-api.js.map +1 -1
  232. package/dist/core/verse/folder-io.d.ts +50 -0
  233. package/dist/core/verse/folder-io.js +142 -0
  234. package/dist/core/verse/folder-io.js.map +1 -0
  235. package/dist/core/verse/git-api.js +5 -4
  236. package/dist/core/verse/git-api.js.map +1 -1
  237. package/dist/core/verse/git-ops.d.ts +10 -1
  238. package/dist/core/verse/git-ops.js +18 -2
  239. package/dist/core/verse/git-ops.js.map +1 -1
  240. package/dist/core/verse/github-api.d.ts +1 -1
  241. package/dist/core/verse/github-api.js +5 -4
  242. package/dist/core/verse/github-api.js.map +1 -1
  243. package/dist/core/verse/github-repo.js +118 -14
  244. package/dist/core/verse/github-repo.js.map +1 -1
  245. package/dist/core/verse/leader-api.d.ts +5 -4
  246. package/dist/core/verse/leader-api.js +237 -10
  247. package/dist/core/verse/leader-api.js.map +1 -1
  248. package/dist/core/verse/local-models.d.ts +10 -0
  249. package/dist/core/verse/local-models.js +10 -1
  250. package/dist/core/verse/local-models.js.map +1 -1
  251. package/dist/core/verse/login-path.d.ts +73 -0
  252. package/dist/core/verse/login-path.js +123 -14
  253. package/dist/core/verse/login-path.js.map +1 -1
  254. package/dist/core/verse/mcp-control-api.js +4 -4
  255. package/dist/core/verse/mcp-control-api.js.map +1 -1
  256. package/dist/core/verse/mcp-scope.d.ts +22 -0
  257. package/dist/core/verse/mcp-scope.js +41 -1
  258. package/dist/core/verse/mcp-scope.js.map +1 -1
  259. package/dist/core/verse/mcp-seat-view.d.ts +10 -0
  260. package/dist/core/verse/mcp-seat-view.js +30 -4
  261. package/dist/core/verse/mcp-seat-view.js.map +1 -1
  262. package/dist/core/verse/overnight-api.js +20 -2
  263. package/dist/core/verse/overnight-api.js.map +1 -1
  264. package/dist/core/verse/path-guard.d.ts +24 -0
  265. package/dist/core/verse/path-guard.js +91 -22
  266. package/dist/core/verse/path-guard.js.map +1 -1
  267. package/dist/core/verse/preview-api.js +22 -0
  268. package/dist/core/verse/preview-api.js.map +1 -1
  269. package/dist/core/verse/projects.d.ts +21 -0
  270. package/dist/core/verse/projects.js +82 -0
  271. package/dist/core/verse/projects.js.map +1 -1
  272. package/dist/core/verse/seat-readiness.d.ts +25 -0
  273. package/dist/core/verse/seat-readiness.js +43 -6
  274. package/dist/core/verse/seat-readiness.js.map +1 -1
  275. package/dist/core/verse/session-controls-api.js +5 -3
  276. package/dist/core/verse/session-controls-api.js.map +1 -1
  277. package/dist/core/verse/session-handoff.d.ts +26 -0
  278. package/dist/core/verse/session-handoff.js +90 -12
  279. package/dist/core/verse/session-handoff.js.map +1 -1
  280. package/dist/core/verse/terminal-api.js +7 -4
  281. package/dist/core/verse/terminal-api.js.map +1 -1
  282. package/dist/core/verse/terminal.js +9 -5
  283. package/dist/core/verse/terminal.js.map +1 -1
  284. package/dist/core/verse/verse-api.d.ts +3 -1
  285. package/dist/core/verse/verse-api.js +87 -27
  286. package/dist/core/verse/verse-api.js.map +1 -1
  287. package/dist/core/verse/workspaces.d.ts +51 -0
  288. package/dist/core/verse/workspaces.js +177 -47
  289. package/dist/core/verse/workspaces.js.map +1 -1
  290. package/dist/core/vision/leader-apply.d.ts +40 -0
  291. package/dist/core/vision/leader-apply.js +101 -30
  292. package/dist/core/vision/leader-apply.js.map +1 -1
  293. package/dist/core/vision/leader-cadence.d.ts +114 -0
  294. package/dist/core/vision/leader-cadence.js +127 -0
  295. package/dist/core/vision/leader-cadence.js.map +1 -0
  296. package/dist/core/vision/leader-goal-evidence.d.ts +25 -0
  297. package/dist/core/vision/leader-goal-evidence.js +36 -0
  298. package/dist/core/vision/leader-goal-evidence.js.map +1 -0
  299. package/dist/core/vision/leader-health.d.ts +53 -0
  300. package/dist/core/vision/leader-health.js +92 -0
  301. package/dist/core/vision/leader-health.js.map +1 -0
  302. package/dist/core/vision/leader-operator.d.ts +176 -0
  303. package/dist/core/vision/leader-operator.js +355 -0
  304. package/dist/core/vision/leader-operator.js.map +1 -0
  305. package/dist/core/vision/leader-run-chain.d.ts +41 -0
  306. package/dist/core/vision/leader-run-chain.js +54 -0
  307. package/dist/core/vision/leader-run-chain.js.map +1 -0
  308. package/dist/core/vision/leader-seat-plan.d.ts +68 -0
  309. package/dist/core/vision/leader-seat-plan.js +93 -0
  310. package/dist/core/vision/leader-seat-plan.js.map +1 -0
  311. package/dist/core/vision/leader-seat.d.ts +71 -5
  312. package/dist/core/vision/leader-seat.js +235 -33
  313. package/dist/core/vision/leader-seat.js.map +1 -1
  314. package/dist/core/vision/leader-thread-types.d.ts +104 -0
  315. package/dist/core/vision/leader-thread-types.js +9 -0
  316. package/dist/core/vision/leader-thread-types.js.map +1 -0
  317. package/dist/core/vision/leader-thread.d.ts +138 -0
  318. package/dist/core/vision/leader-thread.js +926 -0
  319. package/dist/core/vision/leader-thread.js.map +1 -0
  320. package/dist/core/vision/leader-types.d.ts +75 -1
  321. package/dist/core/vision/leader.d.ts +88 -18
  322. package/dist/core/vision/leader.js +269 -69
  323. package/dist/core/vision/leader.js.map +1 -1
  324. package/dist/core/web/public/next/assets/{ActionMenu-B4-39f2S.js → ActionMenu-CIiX2X8w.js} +2 -2
  325. package/dist/core/web/public/next/assets/{ActionMenu-B4-39f2S.js.map → ActionMenu-CIiX2X8w.js.map} +1 -1
  326. package/dist/core/web/public/next/assets/Advanced-DHsysoIL.js +2 -0
  327. package/dist/core/web/public/next/assets/{Advanced-B-LqYXvd.js.map → Advanced-DHsysoIL.js.map} +1 -1
  328. package/dist/core/web/public/next/assets/App-CMeAocmW.js +7 -0
  329. package/dist/core/web/public/next/assets/{App-E2zCDRxm.js.map → App-CMeAocmW.js.map} +1 -1
  330. package/dist/core/web/public/next/assets/AppsSection-DISYnMnc.js +2 -0
  331. package/dist/core/web/public/next/assets/{AppsSection-8wXPPhZb.js.map → AppsSection-DISYnMnc.js.map} +1 -1
  332. package/dist/core/web/public/next/assets/AreaTrend-DK2Y2TbZ.js +2 -0
  333. package/dist/core/web/public/next/assets/AreaTrend-DK2Y2TbZ.js.map +1 -0
  334. package/dist/core/web/public/next/assets/AutonomyOffState-CEPSZToP.js +3 -0
  335. package/dist/core/web/public/next/assets/AutonomyOffState-CEPSZToP.js.map +1 -0
  336. package/dist/core/web/public/next/assets/AutonomyOffState-CmSSTHVe.css +1 -0
  337. package/dist/core/web/public/next/assets/AutonomySection-DWc3ciAD.js +2 -0
  338. package/dist/core/web/public/next/assets/{AutonomySection-B46igJKR.js.map → AutonomySection-DWc3ciAD.js.map} +1 -1
  339. package/dist/core/web/public/next/assets/AutonomyStatus-CYaHjJ9U.js +2 -0
  340. package/dist/core/web/public/next/assets/AutonomyStatus-CYaHjJ9U.js.map +1 -0
  341. package/dist/core/web/public/next/assets/{BranchBar-BRkrEQGs.js → BranchBar-BxhdiIpx.js} +2 -2
  342. package/dist/core/web/public/next/assets/{BranchBar-BRkrEQGs.js.map → BranchBar-BxhdiIpx.js.map} +1 -1
  343. package/dist/core/web/public/next/assets/BudgetControl-C-0KhjRS.js +2 -0
  344. package/dist/core/web/public/next/assets/BudgetControl-C-0KhjRS.js.map +1 -0
  345. package/dist/core/web/public/next/assets/{Button-BhO-WKPp.js → Button-D90dzI_o.js} +2 -2
  346. package/dist/core/web/public/next/assets/{Button-BhO-WKPp.js.map → Button-D90dzI_o.js.map} +1 -1
  347. package/dist/core/web/public/next/assets/{CapacityStrip-CkFz5vq2.js → CapacityStrip-qKDBC65C.js} +2 -2
  348. package/dist/core/web/public/next/assets/{CapacityStrip-CkFz5vq2.js.map → CapacityStrip-qKDBC65C.js.map} +1 -1
  349. package/dist/core/web/public/next/assets/ChartFrame-DY-GVUb3.js +2 -0
  350. package/dist/core/web/public/next/assets/ChartFrame-DY-GVUb3.js.map +1 -0
  351. package/dist/core/web/public/next/assets/ChartFrame-DyjmyV8N.css +1 -0
  352. package/dist/core/web/public/next/assets/ChatSection-DlT4QU6a.js +3 -0
  353. package/dist/core/web/public/next/assets/ChatSection-DlT4QU6a.js.map +1 -0
  354. package/dist/core/web/public/next/assets/{CloudBudgetForm-Bd91iStN.js → CloudBudgetForm-BrNun_Yu.js} +2 -2
  355. package/dist/core/web/public/next/assets/{CloudBudgetForm-Bd91iStN.js.map → CloudBudgetForm-BrNun_Yu.js.map} +1 -1
  356. package/dist/core/web/public/next/assets/CommandPalette-C5lDDKN0.js +2 -0
  357. package/dist/core/web/public/next/assets/CommandPalette-C5lDDKN0.js.map +1 -0
  358. package/dist/core/web/public/next/assets/CommandSection-B0xuU-yZ.css +1 -0
  359. package/dist/core/web/public/next/assets/CommandSection-DXFvPgKp.js +3 -0
  360. package/dist/core/web/public/next/assets/CommandSection-DXFvPgKp.js.map +1 -0
  361. package/dist/core/web/public/next/assets/{ConfirmDialog-Bv4hdK2T.js → ConfirmDialog-BR4nZQ2D.js} +2 -2
  362. package/dist/core/web/public/next/assets/{ConfirmDialog-Bv4hdK2T.js.map → ConfirmDialog-BR4nZQ2D.js.map} +1 -1
  363. package/dist/core/web/public/next/assets/{DeleteChatDialog-CH1Ix8Eh.js → DeleteChatDialog-BCfP12GH.js} +2 -2
  364. package/dist/core/web/public/next/assets/{DeleteChatDialog-CH1Ix8Eh.js.map → DeleteChatDialog-BCfP12GH.js.map} +1 -1
  365. package/dist/core/web/public/next/assets/{Dialog-BSsIFNqD.js → Dialog-D3msQlCj.js} +2 -2
  366. package/dist/core/web/public/next/assets/{Dialog-BSsIFNqD.js.map → Dialog-D3msQlCj.js.map} +1 -1
  367. package/dist/core/web/public/next/assets/{DiffPane-BpNuJmth.js → DiffPane-Ix6hvXlY.js} +2 -2
  368. package/dist/core/web/public/next/assets/{DiffPane-BpNuJmth.js.map → DiffPane-Ix6hvXlY.js.map} +1 -1
  369. package/dist/core/web/public/next/assets/{DiffViewer-CJarUvs0.js → DiffViewer-C4WKsCIe.js} +2 -2
  370. package/dist/core/web/public/next/assets/{DiffViewer-CJarUvs0.js.map → DiffViewer-C4WKsCIe.js.map} +1 -1
  371. package/dist/core/web/public/next/assets/DockHost-aWP7liri.js +2 -0
  372. package/dist/core/web/public/next/assets/{DockHost-DW9q0NdR.js.map → DockHost-aWP7liri.js.map} +1 -1
  373. package/dist/core/web/public/next/assets/{EmptyState-DPrd2aak.js → EmptyState-D2IRy3Cg.js} +2 -2
  374. package/dist/core/web/public/next/assets/{EmptyState-DPrd2aak.js.map → EmptyState-D2IRy3Cg.js.map} +1 -1
  375. package/dist/core/web/public/next/assets/{Epistemic-D7MxSmED.js → Epistemic-DMr18muX.js} +2 -2
  376. package/dist/core/web/public/next/assets/{Epistemic-D7MxSmED.js.map → Epistemic-DMr18muX.js.map} +1 -1
  377. package/dist/core/web/public/next/assets/EvidenceTimeline-DXmmz6Fi.js +2 -0
  378. package/dist/core/web/public/next/assets/EvidenceTimeline-DXmmz6Fi.js.map +1 -0
  379. package/dist/core/web/public/next/assets/EvidenceTimeline-PBDAiSuG.css +1 -0
  380. package/dist/core/web/public/next/assets/FleetSection-BfplcwFU.js +3 -0
  381. package/dist/core/web/public/next/assets/FleetSection-BfplcwFU.js.map +1 -0
  382. package/dist/core/web/public/next/assets/Funnel-Dh2yVHws.js +2 -0
  383. package/dist/core/web/public/next/assets/Funnel-Dh2yVHws.js.map +1 -0
  384. package/dist/core/web/public/next/assets/{GearTray-CIPtzeIx.js → GearTray-Dt-ee32-.js} +2 -2
  385. package/dist/core/web/public/next/assets/{GearTray-CIPtzeIx.js.map → GearTray-Dt-ee32-.js.map} +1 -1
  386. package/dist/core/web/public/next/assets/GrowthSection-P9EUfGeA.js +2 -0
  387. package/dist/core/web/public/next/assets/{GrowthSection-iiQg6wvK.js.map → GrowthSection-P9EUfGeA.js.map} +1 -1
  388. package/dist/core/web/public/next/assets/{HandoffDialog-DrQ6ofuy.js → HandoffDialog-CLJQLe7M.js} +2 -2
  389. package/dist/core/web/public/next/assets/{HandoffDialog-DrQ6ofuy.js.map → HandoffDialog-CLJQLe7M.js.map} +1 -1
  390. package/dist/core/web/public/next/assets/{Input-C0MQxEM6.js → Input-D_KRJA9V.js} +2 -2
  391. package/dist/core/web/public/next/assets/{Input-C0MQxEM6.js.map → Input-D_KRJA9V.js.map} +1 -1
  392. package/dist/core/web/public/next/assets/LeaderCard-D-7DlJuR.js +2 -0
  393. package/dist/core/web/public/next/assets/LeaderCard-D-7DlJuR.js.map +1 -0
  394. package/dist/core/web/public/next/assets/LeaderCard-DNsbLwY3.css +1 -0
  395. package/dist/core/web/public/next/assets/LeaderConversation-DGZjWI-E.js +2 -0
  396. package/dist/core/web/public/next/assets/LeaderConversation-DGZjWI-E.js.map +1 -0
  397. package/dist/core/web/public/next/assets/LeaderConversation-DPHUTq6W.css +1 -0
  398. package/dist/core/web/public/next/assets/MatrixHeatmap-C7BCG2d7.js +2 -0
  399. package/dist/core/web/public/next/assets/MatrixHeatmap-C7BCG2d7.js.map +1 -0
  400. package/dist/core/web/public/next/assets/McpSection-DGbRcyv8.js +2 -0
  401. package/dist/core/web/public/next/assets/{McpSection-BRuCcTjT.js.map → McpSection-DGbRcyv8.js.map} +1 -1
  402. package/dist/core/web/public/next/assets/{MessageMarkdown-bRvD5nSc.js → MessageMarkdown-DFvk06qG.js} +3 -3
  403. package/dist/core/web/public/next/assets/{MessageMarkdown-bRvD5nSc.js.map → MessageMarkdown-DFvk06qG.js.map} +1 -1
  404. package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-FjjkPKA2.js → MessageMarkdownRenderer-BAv4oEUf.js} +2 -2
  405. package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-FjjkPKA2.js.map → MessageMarkdownRenderer-BAv4oEUf.js.map} +1 -1
  406. package/dist/core/web/public/next/assets/{Meter-CFNLpwA-.js → Meter-CMj-VafN.js} +2 -2
  407. package/dist/core/web/public/next/assets/{Meter-CFNLpwA-.js.map → Meter-CMj-VafN.js.map} +1 -1
  408. package/dist/core/web/public/next/assets/MindSection-C6kLPIjt.js +3 -0
  409. package/dist/core/web/public/next/assets/MindSection-C6kLPIjt.js.map +1 -0
  410. package/dist/core/web/public/next/assets/{MutationTokenDialog-T7gv3a9r.js → MutationTokenDialog-DbnO8aYi.js} +2 -2
  411. package/dist/core/web/public/next/assets/{MutationTokenDialog-T7gv3a9r.js.map → MutationTokenDialog-DbnO8aYi.js.map} +1 -1
  412. package/dist/core/web/public/next/assets/NeedsYouDrawer-crsiX-Nc.js +3 -0
  413. package/dist/core/web/public/next/assets/NeedsYouDrawer-crsiX-Nc.js.map +1 -0
  414. package/dist/core/web/public/next/assets/NeedsYouList-CCsijdWt.js +2 -0
  415. package/dist/core/web/public/next/assets/{NeedsYouList-BgH8ezqf.js.map → NeedsYouList-CCsijdWt.js.map} +1 -1
  416. package/dist/core/web/public/next/assets/NeedsYouList-CH6Mq-E_.css +1 -0
  417. package/dist/core/web/public/next/assets/NewChatDialog-BlCHtOIr.js +3 -0
  418. package/dist/core/web/public/next/assets/{NewChatDialog-Db5Xjrk8.js.map → NewChatDialog-BlCHtOIr.js.map} +1 -1
  419. package/dist/core/web/public/next/assets/OnboardingFlow-Cy9BA9eR.js +3 -0
  420. package/dist/core/web/public/next/assets/OnboardingFlow-Cy9BA9eR.js.map +1 -0
  421. package/dist/core/web/public/next/assets/OnboardingGate-BXpNFoLt.js +3 -0
  422. package/dist/core/web/public/next/assets/{OnboardingGate-C4VXn7BJ.js.map → OnboardingGate-BXpNFoLt.js.map} +1 -1
  423. package/dist/core/web/public/next/assets/PreviewPane-COid5A0f.js +2 -0
  424. package/dist/core/web/public/next/assets/{PreviewPane-BPBbcROb.js.map → PreviewPane-COid5A0f.js.map} +1 -1
  425. package/dist/core/web/public/next/assets/{ProviderLogo-CT2cSfkg.js → ProviderLogo-Cq-CYIwT.js} +2 -2
  426. package/dist/core/web/public/next/assets/{ProviderLogo-CT2cSfkg.js.map → ProviderLogo-Cq-CYIwT.js.map} +1 -1
  427. package/dist/core/web/public/next/assets/{RailStatus-Vgl0GYtd.js → RailStatus-Co5UB6We.js} +2 -2
  428. package/dist/core/web/public/next/assets/{RailStatus-Vgl0GYtd.js.map → RailStatus-Co5UB6We.js.map} +1 -1
  429. package/dist/core/web/public/next/assets/{RefreshIndicator-CSyqpY_q.js → RefreshIndicator-DO9ibnSq.js} +2 -2
  430. package/dist/core/web/public/next/assets/{RefreshIndicator-CSyqpY_q.js.map → RefreshIndicator-DO9ibnSq.js.map} +1 -1
  431. package/dist/core/web/public/next/assets/ResourcePoolConsoleApp-Dr2WRgkq.js +7 -0
  432. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-BX2nkdmZ.js.map → ResourcePoolConsoleApp-Dr2WRgkq.js.map} +1 -1
  433. package/dist/core/web/public/next/assets/ResourcesChrome-CwMnbqI1.js +3 -0
  434. package/dist/core/web/public/next/assets/ResourcesChrome-CwMnbqI1.js.map +1 -0
  435. package/dist/core/web/public/next/assets/ResourcesDrawer-B_Xp_JNc.js +2 -0
  436. package/dist/core/web/public/next/assets/ResourcesDrawer-B_Xp_JNc.js.map +1 -0
  437. package/dist/core/web/public/next/assets/ResourcesDrawer-DrGoO8KG.css +1 -0
  438. package/dist/core/web/public/next/assets/{RouteErrorBoundary-BaC7YVzR.js → RouteErrorBoundary-SkNhhUbt.js} +2 -2
  439. package/dist/core/web/public/next/assets/{RouteErrorBoundary-BaC7YVzR.js.map → RouteErrorBoundary-SkNhhUbt.js.map} +1 -1
  440. package/dist/core/web/public/next/assets/{RunInCloudAction-D5PDHJPE.js → RunInCloudAction-BXPlZwJQ.js} +2 -2
  441. package/dist/core/web/public/next/assets/{RunInCloudAction-D5PDHJPE.js.map → RunInCloudAction-BXPlZwJQ.js.map} +1 -1
  442. package/dist/core/web/public/next/assets/{SeatSelector-Cg75LInq.js → SeatSelector-J5R3zDkB.js} +3 -3
  443. package/dist/core/web/public/next/assets/{SeatSelector-Cg75LInq.js.map → SeatSelector-J5R3zDkB.js.map} +1 -1
  444. package/dist/core/web/public/next/assets/{Segmented-CametjLt.js → Segmented-CGh_-tBb.js} +2 -2
  445. package/dist/core/web/public/next/assets/{Segmented-CametjLt.js.map → Segmented-CGh_-tBb.js.map} +1 -1
  446. package/dist/core/web/public/next/assets/{Select-De52fghU.js → Select-CWnmUzs3.js} +2 -2
  447. package/dist/core/web/public/next/assets/{Select-De52fghU.js.map → Select-CWnmUzs3.js.map} +1 -1
  448. package/dist/core/web/public/next/assets/{SessionGate-CzlcNq3O.js → SessionGate-sp4KsnUo.js} +2 -2
  449. package/dist/core/web/public/next/assets/{SessionGate-CzlcNq3O.js.map → SessionGate-sp4KsnUo.js.map} +1 -1
  450. package/dist/core/web/public/next/assets/{SessionInsightChip-Bk_Owa7u.js → SessionInsightChip-DLFAzdbi.js} +2 -2
  451. package/dist/core/web/public/next/assets/{SessionInsightChip-Bk_Owa7u.js.map → SessionInsightChip-DLFAzdbi.js.map} +1 -1
  452. package/dist/core/web/public/next/assets/{SessionSearch-C6C8DHN8.js → SessionSearch-DbFjwR1h.js} +2 -2
  453. package/dist/core/web/public/next/assets/{SessionSearch-C6C8DHN8.js.map → SessionSearch-DbFjwR1h.js.map} +1 -1
  454. package/dist/core/web/public/next/assets/{SettingsSection-mI9wQYZg.js → SettingsSection-Ch23msJ5.js} +2 -2
  455. package/dist/core/web/public/next/assets/SettingsSection-Ch23msJ5.js.map +1 -0
  456. package/dist/core/web/public/next/assets/SetupChecklist-Bd76MBAR.js +2 -0
  457. package/dist/core/web/public/next/assets/SetupChecklist-Bd76MBAR.js.map +1 -0
  458. package/dist/core/web/public/next/assets/SetupChecklist-BhZpFLsj.css +1 -0
  459. package/dist/core/web/public/next/assets/ShadowDecisions-C5RAY91f.js +3 -0
  460. package/dist/core/web/public/next/assets/ShadowDecisions-C5RAY91f.js.map +1 -0
  461. package/dist/core/web/public/next/assets/{Sheet-CJhK9Dqv.js → Sheet-BZ2AG4Eo.js} +2 -2
  462. package/dist/core/web/public/next/assets/{Sheet-CJhK9Dqv.js.map → Sheet-BZ2AG4Eo.js.map} +1 -1
  463. package/dist/core/web/public/next/assets/{ShortcutsOverlay-DDW-i9Fc.js → ShortcutsOverlay-C8n8y3zf.js} +2 -2
  464. package/dist/core/web/public/next/assets/{ShortcutsOverlay-DDW-i9Fc.js.map → ShortcutsOverlay-C8n8y3zf.js.map} +1 -1
  465. package/dist/core/web/public/next/assets/Sidebar-DN--cPYN.js +3 -0
  466. package/dist/core/web/public/next/assets/Sidebar-DN--cPYN.js.map +1 -0
  467. package/dist/core/web/public/next/assets/{Slider-Bwd9HfXl.js → Slider-KVoJLDD6.js} +2 -2
  468. package/dist/core/web/public/next/assets/{Slider-Bwd9HfXl.js.map → Slider-KVoJLDD6.js.map} +1 -1
  469. package/dist/core/web/public/next/assets/StatTile-C4KgjltA.js +2 -0
  470. package/dist/core/web/public/next/assets/StatTile-C4KgjltA.js.map +1 -0
  471. package/dist/core/web/public/next/assets/StatTile-CNCqDoPO.css +1 -0
  472. package/dist/core/web/public/next/assets/{StatusBadge-DHKRtttH.js → StatusBadge-CXsYqmdU.js} +2 -2
  473. package/dist/core/web/public/next/assets/{StatusBadge-DHKRtttH.js.map → StatusBadge-CXsYqmdU.js.map} +1 -1
  474. package/dist/core/web/public/next/assets/StepBand-BMtVOFpK.js +2 -0
  475. package/dist/core/web/public/next/assets/StepBand-BMtVOFpK.js.map +1 -0
  476. package/dist/core/web/public/next/assets/{Surface-D--LXgQm.js → Surface-BfDmQcKe.js} +2 -2
  477. package/dist/core/web/public/next/assets/{Surface-D--LXgQm.js.map → Surface-BfDmQcKe.js.map} +1 -1
  478. package/dist/core/web/public/next/assets/Swimlane-B9Exxekk.js +2 -0
  479. package/dist/core/web/public/next/assets/Swimlane-B9Exxekk.js.map +1 -0
  480. package/dist/core/web/public/next/assets/Swimlane-Bf4Rjoy9.css +1 -0
  481. package/dist/core/web/public/next/assets/{Switch-PMixU6b0.js → Switch-9NtO_6Lj.js} +2 -2
  482. package/dist/core/web/public/next/assets/{Switch-PMixU6b0.js.map → Switch-9NtO_6Lj.js.map} +1 -1
  483. package/dist/core/web/public/next/assets/{Tag-fordBsrJ.js → Tag-DscCRFcN.js} +2 -2
  484. package/dist/core/web/public/next/assets/{Tag-fordBsrJ.js.map → Tag-DscCRFcN.js.map} +1 -1
  485. package/dist/core/web/public/next/assets/{TerminalPane-BSnGdUuF.js → TerminalPane-B668-DJW.js} +5 -5
  486. package/dist/core/web/public/next/assets/{TerminalPane-BSnGdUuF.js.map → TerminalPane-B668-DJW.js.map} +1 -1
  487. package/dist/core/web/public/next/assets/UniverseConsoleApp-BUV_brY7.js +2 -0
  488. package/dist/core/web/public/next/assets/{UniverseConsoleApp-DtXFhAhw.js.map → UniverseConsoleApp-BUV_brY7.js.map} +1 -1
  489. package/dist/core/web/public/next/assets/{UniverseView-sWm3pnTW.js → UniverseView-DG70zH0W.js} +4 -4
  490. package/dist/core/web/public/next/assets/{UniverseView-sWm3pnTW.js.map → UniverseView-DG70zH0W.js.map} +1 -1
  491. package/dist/core/web/public/next/assets/UsageSection-DY7uOTm1.js +3 -0
  492. package/dist/core/web/public/next/assets/UsageSection-DY7uOTm1.js.map +1 -0
  493. package/dist/core/web/public/next/assets/VerseConsoleApp-CWU6R3By.js +3 -0
  494. package/dist/core/web/public/next/assets/VerseConsoleApp-CWU6R3By.js.map +1 -0
  495. package/dist/core/web/public/next/assets/Workspace-D1nP9BhD.js +10 -0
  496. package/dist/core/web/public/next/assets/{Workspace-BQSZFSeO.js.map → Workspace-D1nP9BhD.js.map} +1 -1
  497. package/dist/core/web/public/next/assets/actions-CY-PjpGc.js +2 -0
  498. package/dist/core/web/public/next/assets/{actions-Ci6I4C0l.js.map → actions-CY-PjpGc.js.map} +1 -1
  499. package/dist/core/web/public/next/assets/actions-DkWhhWIg.css +1 -0
  500. package/dist/core/web/public/next/assets/{apps-model-DlwnfqwJ.js → apps-model-Du02KhAf.js} +2 -2
  501. package/dist/core/web/public/next/assets/{apps-model-DlwnfqwJ.js.map → apps-model-Du02KhAf.js.map} +1 -1
  502. package/dist/core/web/public/next/assets/{authority-model-BFuPezSo.js → authority-model-79SeuKlJ.js} +2 -2
  503. package/dist/core/web/public/next/assets/{authority-model-BFuPezSo.js.map → authority-model-79SeuKlJ.js.map} +1 -1
  504. package/dist/core/web/public/next/assets/autonomy-ladder-BGWmZzP7.css +1 -0
  505. package/dist/core/web/public/next/assets/autonomy-ladder.module-ArDv4bkr.js +2 -0
  506. package/dist/core/web/public/next/assets/autonomy-ladder.module-ArDv4bkr.js.map +1 -0
  507. package/dist/core/web/public/next/assets/budget-queries-D8QV5XtH.js +2 -0
  508. package/dist/core/web/public/next/assets/{budget-queries-DsAjH8ac.js.map → budget-queries-D8QV5XtH.js.map} +1 -1
  509. package/dist/core/web/public/next/assets/{capacity-strip-model-Dn1cNOhz.js → capacity-strip-model-BwZ1ZbkU.js} +2 -2
  510. package/dist/core/web/public/next/assets/{capacity-strip-model-Dn1cNOhz.js.map → capacity-strip-model-BwZ1ZbkU.js.map} +1 -1
  511. package/dist/core/web/public/next/assets/charts-BZ50mZD_.js +2 -0
  512. package/dist/core/web/public/next/assets/charts-BZ50mZD_.js.map +1 -0
  513. package/dist/core/web/public/next/assets/charts-ClCB18la.css +1 -0
  514. package/dist/core/web/public/next/assets/cloud-model-BDivRMqv.js +2 -0
  515. package/dist/core/web/public/next/assets/{cloud-model-CX407gPY.js.map → cloud-model-BDivRMqv.js.map} +1 -1
  516. package/dist/core/web/public/next/assets/{cloud-queries-QAhpExlz.js → cloud-queries-BPQQ5_pJ.js} +2 -2
  517. package/dist/core/web/public/next/assets/{cloud-queries-QAhpExlz.js.map → cloud-queries-BPQQ5_pJ.js.map} +1 -1
  518. package/dist/core/web/public/next/assets/{colors-BZ3a9wFf.js → colors-DvK0wMHH.js} +2 -2
  519. package/dist/core/web/public/next/assets/{colors-BZ3a9wFf.js.map → colors-DvK0wMHH.js.map} +1 -1
  520. package/dist/core/web/public/next/assets/{command-catalog-CF0cQQac.js → command-catalog-BIntSa9l.js} +2 -2
  521. package/dist/core/web/public/next/assets/command-catalog-BIntSa9l.js.map +1 -0
  522. package/dist/core/web/public/next/assets/{context-model-Co4wh3SM.js → context-model-Br8t9jgG.js} +2 -2
  523. package/dist/core/web/public/next/assets/{context-model-Co4wh3SM.js.map → context-model-Br8t9jgG.js.map} +1 -1
  524. package/dist/core/web/public/next/assets/{context-model-DV1zCfAm.js → context-model-CokrgEUq.js} +3 -3
  525. package/dist/core/web/public/next/assets/{context-model-DV1zCfAm.js.map → context-model-CokrgEUq.js.map} +1 -1
  526. package/dist/core/web/public/next/assets/context-queries-eL3caKVG.js +2 -0
  527. package/dist/core/web/public/next/assets/{context-queries-CktFQqSS.js.map → context-queries-eL3caKVG.js.map} +1 -1
  528. package/dist/core/web/public/next/assets/{control-queries-DAvlh_YM.js → control-queries-B9K-wmcB.js} +2 -2
  529. package/dist/core/web/public/next/assets/{control-queries-DAvlh_YM.js.map → control-queries-B9K-wmcB.js.map} +1 -1
  530. package/dist/core/web/public/next/assets/copy-setup-XMgCC5f8.js +2 -0
  531. package/dist/core/web/public/next/assets/copy-setup-XMgCC5f8.js.map +1 -0
  532. package/dist/core/web/public/next/assets/{dock-icons-BFuBbqJl.js → dock-icons-BJ6ZqB2t.js} +2 -2
  533. package/dist/core/web/public/next/assets/{dock-icons-BFuBbqJl.js.map → dock-icons-BJ6ZqB2t.js.map} +1 -1
  534. package/dist/core/web/public/next/assets/{fleet-model-Dfd5JyLR.js → fleet-model-BvDR3gjp.js} +2 -2
  535. package/dist/core/web/public/next/assets/{fleet-model-Dfd5JyLR.js.map → fleet-model-BvDR3gjp.js.map} +1 -1
  536. package/dist/core/web/public/next/assets/{fleet-queries-RtMTd5rR.js → fleet-queries-W2UFVMZJ.js} +2 -2
  537. package/dist/core/web/public/next/assets/{fleet-queries-RtMTd5rR.js.map → fleet-queries-W2UFVMZJ.js.map} +1 -1
  538. package/dist/core/web/public/next/assets/{focus-trap-BdrbWfC4.js → focus-trap-CtI1SlMX.js} +2 -2
  539. package/dist/core/web/public/next/assets/{focus-trap-BdrbWfC4.js.map → focus-trap-CtI1SlMX.js.map} +1 -1
  540. package/dist/core/web/public/next/assets/format-CH4g_sl1.js +2 -0
  541. package/dist/core/web/public/next/assets/format-CH4g_sl1.js.map +1 -0
  542. package/dist/core/web/public/next/assets/{format-DKrNjdwD.js → format-D7nBg2bn.js} +2 -2
  543. package/dist/core/web/public/next/assets/{format-DKrNjdwD.js.map → format-D7nBg2bn.js.map} +1 -1
  544. package/dist/core/web/public/next/assets/{git-model-_BnI_Ogh.js → git-model-Dnkd1TnK.js} +2 -2
  545. package/dist/core/web/public/next/assets/{git-model-_BnI_Ogh.js.map → git-model-Dnkd1TnK.js.map} +1 -1
  546. package/dist/core/web/public/next/assets/global-CMNW_dqn.js +2 -0
  547. package/dist/core/web/public/next/assets/{global-CNNGMril.js.map → global-CMNW_dqn.js.map} +1 -1
  548. package/dist/core/web/public/next/assets/guarded-runners-YeAc9wLQ.js +2 -0
  549. package/dist/core/web/public/next/assets/{guarded-runners-DPh2d32e.js.map → guarded-runners-YeAc9wLQ.js.map} +1 -1
  550. package/dist/core/web/public/next/assets/{icons-0haWy6nh.js → icons-D1sYbCTQ.js} +2 -2
  551. package/dist/core/web/public/next/assets/{icons-0haWy6nh.js.map → icons-D1sYbCTQ.js.map} +1 -1
  552. package/dist/core/web/public/next/assets/{index-QesfXBsb.js → index-DZ-UQ7v8.js} +3 -3
  553. package/dist/core/web/public/next/assets/{index-QesfXBsb.js.map → index-DZ-UQ7v8.js.map} +1 -1
  554. package/dist/core/web/public/next/assets/leader-focus-BjeJv7CW.js +2 -0
  555. package/dist/core/web/public/next/assets/leader-focus-BjeJv7CW.js.map +1 -0
  556. package/dist/core/web/public/next/assets/leader-thread-types-CDb-VPPO.js +2 -0
  557. package/dist/core/web/public/next/assets/leader-thread-types-CDb-VPPO.js.map +1 -0
  558. package/dist/core/web/public/next/assets/{live-model-De-q0ZFK.js → live-model-D_32RAOR.js} +2 -2
  559. package/dist/core/web/public/next/assets/live-model-D_32RAOR.js.map +1 -0
  560. package/dist/core/web/public/next/assets/{mind.module-BrK2nFOL.js → mind.module-7hA92hRg.js} +2 -2
  561. package/dist/core/web/public/next/assets/{mind.module-BrK2nFOL.js.map → mind.module-7hA92hRg.js.map} +1 -1
  562. package/dist/core/web/public/next/assets/mutations-DQPbFdRJ.js +2 -0
  563. package/dist/core/web/public/next/assets/{mutations-DTCtKCn_.js.map → mutations-DQPbFdRJ.js.map} +1 -1
  564. package/dist/core/web/public/next/assets/{nav-KmGX0CVz.js → nav-0dOYe2rJ.js} +2 -2
  565. package/dist/core/web/public/next/assets/{nav-KmGX0CVz.js.map → nav-0dOYe2rJ.js.map} +1 -1
  566. package/dist/core/web/public/next/assets/{overnight-queries-mlO0xzsr.js → overnight-queries-C4wOnFtE.js} +2 -2
  567. package/dist/core/web/public/next/assets/{overnight-queries-mlO0xzsr.js.map → overnight-queries-C4wOnFtE.js.map} +1 -1
  568. package/dist/core/web/public/next/assets/primitives-2A5Eq8FQ.js +1 -0
  569. package/dist/core/web/public/next/assets/{queries-CyBE8nsW.js → queries-BTXWdZzU.js} +2 -2
  570. package/dist/core/web/public/next/assets/{queries-CyBE8nsW.js.map → queries-BTXWdZzU.js.map} +1 -1
  571. package/dist/core/web/public/next/assets/{reasoning-pref-CaoA4_Vl.js → reasoning-pref-Ca6lN6x_.js} +2 -2
  572. package/dist/core/web/public/next/assets/{reasoning-pref-CaoA4_Vl.js.map → reasoning-pref-Ca6lN6x_.js.map} +1 -1
  573. package/dist/core/web/public/next/assets/{reveal-anchor-BP1VqnPU.js → reveal-anchor-DfNLgK_I.js} +2 -2
  574. package/dist/core/web/public/next/assets/{reveal-anchor-BP1VqnPU.js.map → reveal-anchor-DfNLgK_I.js.map} +1 -1
  575. package/dist/core/web/public/next/assets/seat-subscription-C5uPHBsN.js +2 -0
  576. package/dist/core/web/public/next/assets/{seat-subscription-C5Tvr0WQ.js.map → seat-subscription-C5uPHBsN.js.map} +1 -1
  577. package/dist/core/web/public/next/assets/{sidebar-model-B5Qk95k_.js → sidebar-model-CLCdqwkR.js} +2 -2
  578. package/dist/core/web/public/next/assets/{sidebar-model-B5Qk95k_.js.map → sidebar-model-CLCdqwkR.js.map} +1 -1
  579. package/dist/core/web/public/next/assets/{slots-DHKQDoE6.js → slots-C-qBeu-8.js} +3 -3
  580. package/dist/core/web/public/next/assets/{slots-DHKQDoE6.js.map → slots-C-qBeu-8.js.map} +1 -1
  581. package/dist/core/web/public/next/assets/{surface-data-CchClvHo.js → surface-data-Dj71tdD-.js} +2 -2
  582. package/dist/core/web/public/next/assets/{surface-data-CchClvHo.js.map → surface-data-Dj71tdD-.js.map} +1 -1
  583. package/dist/core/web/public/next/assets/surface-prefetch-B0WdJOJG.js +2 -0
  584. package/dist/core/web/public/next/assets/surface-prefetch-B0WdJOJG.js.map +1 -0
  585. package/dist/core/web/public/next/assets/{tasks-model-D6WFVULI.js → tasks-model-O8yR5YXp.js} +2 -2
  586. package/dist/core/web/public/next/assets/{tasks-model-D6WFVULI.js.map → tasks-model-O8yR5YXp.js.map} +1 -1
  587. package/dist/core/web/public/next/assets/{terminal-icons-BvCvuXk2.js → terminal-icons-CoRZq9iO.js} +2 -2
  588. package/dist/core/web/public/next/assets/{terminal-icons-BvCvuXk2.js.map → terminal-icons-CoRZq9iO.js.map} +1 -1
  589. package/dist/core/web/public/next/assets/thread-data-DQB_g66d.js +2 -0
  590. package/dist/core/web/public/next/assets/thread-data-DQB_g66d.js.map +1 -0
  591. package/dist/core/web/public/next/assets/types-IYKrYLku.js +2 -0
  592. package/dist/core/web/public/next/assets/types-IYKrYLku.js.map +1 -0
  593. package/dist/core/web/public/next/assets/{usage-queries-D1SU1t7r.js → usage-queries-AdjEyEtn.js} +2 -2
  594. package/dist/core/web/public/next/assets/{usage-queries-D1SU1t7r.js.map → usage-queries-AdjEyEtn.js.map} +1 -1
  595. package/dist/core/web/public/next/assets/{use-guarded-action-BZmBv7CJ.js → use-guarded-action-CuuMHeDX.js} +2 -2
  596. package/dist/core/web/public/next/assets/{use-guarded-action-BZmBv7CJ.js.map → use-guarded-action-CuuMHeDX.js.map} +1 -1
  597. package/dist/core/web/public/next/assets/{use-ticker-CkJ4-0wU.js → use-ticker-BBcp02SZ.js} +2 -2
  598. package/dist/core/web/public/next/assets/{use-ticker-CkJ4-0wU.js.map → use-ticker-BBcp02SZ.js.map} +1 -1
  599. package/dist/core/web/public/next/assets/{use-token-gate-CYcfk_GS.js → use-token-gate-CbXVYQYI.js} +2 -2
  600. package/dist/core/web/public/next/assets/{use-token-gate-CYcfk_GS.js.map → use-token-gate-CbXVYQYI.js.map} +1 -1
  601. package/dist/core/web/public/next/assets/{useOnboarding-CujRS6PK.js → useOnboarding-C_Fpe_sH.js} +2 -2
  602. package/dist/core/web/public/next/assets/{useOnboarding-CujRS6PK.js.map → useOnboarding-C_Fpe_sH.js.map} +1 -1
  603. package/dist/core/web/public/next/assets/{useSeatsRefresh-BEaQdkZs.js → useSeatsRefresh-Co1CnxLc.js} +2 -2
  604. package/dist/core/web/public/next/assets/{useSeatsRefresh-BEaQdkZs.js.map → useSeatsRefresh-Co1CnxLc.js.map} +1 -1
  605. package/dist/core/web/public/next/assets/{useVerseTranscript-BQ0J6Xhx.js → useVerseTranscript-DgbigUlV.js} +2 -2
  606. package/dist/core/web/public/next/assets/{useVerseTranscript-BQ0J6Xhx.js.map → useVerseTranscript-DgbigUlV.js.map} +1 -1
  607. package/dist/core/web/public/next/assets/{verse-icons-DndgEgKS.js → verse-icons-S56hIiXS.js} +2 -2
  608. package/dist/core/web/public/next/assets/{verse-icons-DndgEgKS.js.map → verse-icons-S56hIiXS.js.map} +1 -1
  609. package/dist/core/web/public/next/assets/verse-model-DAVevkNe.js +2 -0
  610. package/dist/core/web/public/next/assets/{verse-model-MMySehjj.js.map → verse-model-DAVevkNe.js.map} +1 -1
  611. package/dist/core/web/public/next/assets/{warmup-CNEH_-be.js → warmup-B_RAtNZ_.js} +3 -3
  612. package/dist/core/web/public/next/assets/{warmup-CNEH_-be.js.map → warmup-B_RAtNZ_.js.map} +1 -1
  613. package/dist/core/web/public/next/assets/why-seat-model-M_r0wX3b.js +2 -0
  614. package/dist/core/web/public/next/assets/why-seat-model-M_r0wX3b.js.map +1 -0
  615. package/dist/core/web/public/next/assets/{workspace-model-Dukb2wwv.js → workspace-model-dji5-Al3.js} +2 -2
  616. package/dist/core/web/public/next/assets/{workspace-model-Dukb2wwv.js.map → workspace-model-dji5-Al3.js.map} +1 -1
  617. package/dist/core/web/public/next/index.html +1 -1
  618. package/dist/core/web/read-projection-worker.js +2 -1
  619. package/dist/core/web/read-projection-worker.js.map +1 -1
  620. package/dist/core/web/read-projections.d.ts +3 -0
  621. package/dist/core/web/read-projections.js +2 -2
  622. package/dist/core/web/read-projections.js.map +1 -1
  623. package/dist/core/web/visibility.js +3 -4
  624. package/dist/core/web/visibility.js.map +1 -1
  625. package/dist/release-dependency-inventory.json +1 -1
  626. package/docs/AUTONOMY-GAP.md +1 -1
  627. package/package.json +3 -2
  628. package/dist/core/web/public/next/assets/Advanced-B-LqYXvd.js +0 -2
  629. package/dist/core/web/public/next/assets/App-E2zCDRxm.js +0 -7
  630. package/dist/core/web/public/next/assets/AppsSection-8wXPPhZb.js +0 -2
  631. package/dist/core/web/public/next/assets/AreaTrend-BFimExYn.js +0 -2
  632. package/dist/core/web/public/next/assets/AreaTrend-BFimExYn.js.map +0 -1
  633. package/dist/core/web/public/next/assets/AutonomyOffState-B5eEkmiz.js +0 -2
  634. package/dist/core/web/public/next/assets/AutonomyOffState-B5eEkmiz.js.map +0 -1
  635. package/dist/core/web/public/next/assets/AutonomyOffState-DmbE71vI.css +0 -1
  636. package/dist/core/web/public/next/assets/AutonomySection-B46igJKR.js +0 -2
  637. package/dist/core/web/public/next/assets/BudgetControl-T_RjPrw9.js +0 -2
  638. package/dist/core/web/public/next/assets/BudgetControl-T_RjPrw9.js.map +0 -1
  639. package/dist/core/web/public/next/assets/ChartFrame-BVuBe6RP.css +0 -1
  640. package/dist/core/web/public/next/assets/ChartFrame-SygZ7B3x.js +0 -2
  641. package/dist/core/web/public/next/assets/ChartFrame-SygZ7B3x.js.map +0 -1
  642. package/dist/core/web/public/next/assets/ChatSection-D6WybBpB.js +0 -3
  643. package/dist/core/web/public/next/assets/ChatSection-D6WybBpB.js.map +0 -1
  644. package/dist/core/web/public/next/assets/CommandPalette-B6SgDJH-.js +0 -2
  645. package/dist/core/web/public/next/assets/CommandPalette-B6SgDJH-.js.map +0 -1
  646. package/dist/core/web/public/next/assets/CommandSection-CvAVd1w4.css +0 -1
  647. package/dist/core/web/public/next/assets/CommandSection-DgMGoepd.js +0 -2
  648. package/dist/core/web/public/next/assets/CommandSection-DgMGoepd.js.map +0 -1
  649. package/dist/core/web/public/next/assets/DockHost-DW9q0NdR.js +0 -2
  650. package/dist/core/web/public/next/assets/FleetSection-BdZN6rGS.js +0 -3
  651. package/dist/core/web/public/next/assets/FleetSection-BdZN6rGS.js.map +0 -1
  652. package/dist/core/web/public/next/assets/Funnel-BeNQZWv2.js +0 -2
  653. package/dist/core/web/public/next/assets/Funnel-BeNQZWv2.js.map +0 -1
  654. package/dist/core/web/public/next/assets/GrowthSection-iiQg6wvK.js +0 -2
  655. package/dist/core/web/public/next/assets/LeaderCard-BRr3_yE-.js +0 -2
  656. package/dist/core/web/public/next/assets/LeaderCard-BRr3_yE-.js.map +0 -1
  657. package/dist/core/web/public/next/assets/MatrixHeatmap-DZvZeWXL.js +0 -2
  658. package/dist/core/web/public/next/assets/MatrixHeatmap-DZvZeWXL.js.map +0 -1
  659. package/dist/core/web/public/next/assets/McpSection-BRuCcTjT.js +0 -2
  660. package/dist/core/web/public/next/assets/MindSection-BTIiQS8P.js +0 -2
  661. package/dist/core/web/public/next/assets/MindSection-BTIiQS8P.js.map +0 -1
  662. package/dist/core/web/public/next/assets/NeedsYouDrawer-lYN4G_aM.js +0 -2
  663. package/dist/core/web/public/next/assets/NeedsYouDrawer-lYN4G_aM.js.map +0 -1
  664. package/dist/core/web/public/next/assets/NeedsYouList-BgH8ezqf.js +0 -2
  665. package/dist/core/web/public/next/assets/NeedsYouList-CruOt4JT.css +0 -1
  666. package/dist/core/web/public/next/assets/NewChatDialog-Db5Xjrk8.js +0 -3
  667. package/dist/core/web/public/next/assets/OnboardingFlow-LoY-zuSe.js +0 -2
  668. package/dist/core/web/public/next/assets/OnboardingFlow-LoY-zuSe.js.map +0 -1
  669. package/dist/core/web/public/next/assets/OnboardingGate-C4VXn7BJ.js +0 -3
  670. package/dist/core/web/public/next/assets/PreviewPane-BPBbcROb.js +0 -2
  671. package/dist/core/web/public/next/assets/ResourcePoolConsoleApp-BX2nkdmZ.js +0 -7
  672. package/dist/core/web/public/next/assets/ResourcesChrome-B2U2lO1H.js +0 -3
  673. package/dist/core/web/public/next/assets/ResourcesChrome-B2U2lO1H.js.map +0 -1
  674. package/dist/core/web/public/next/assets/ResourcesDrawer-BCFJ2Uq8.css +0 -1
  675. package/dist/core/web/public/next/assets/ResourcesDrawer-DlUKOjl9.js +0 -2
  676. package/dist/core/web/public/next/assets/ResourcesDrawer-DlUKOjl9.js.map +0 -1
  677. package/dist/core/web/public/next/assets/SettingsSection-mI9wQYZg.js.map +0 -1
  678. package/dist/core/web/public/next/assets/Sidebar-DT7ykaqS.js +0 -3
  679. package/dist/core/web/public/next/assets/Sidebar-DT7ykaqS.js.map +0 -1
  680. package/dist/core/web/public/next/assets/StatTile-BuLMsJMx.css +0 -1
  681. package/dist/core/web/public/next/assets/StatTile-DPZ5yyaU.js +0 -2
  682. package/dist/core/web/public/next/assets/StatTile-DPZ5yyaU.js.map +0 -1
  683. package/dist/core/web/public/next/assets/StepBand-DQykLkk_.js +0 -2
  684. package/dist/core/web/public/next/assets/StepBand-DQykLkk_.js.map +0 -1
  685. package/dist/core/web/public/next/assets/Swimlane-BzH6OCee.js +0 -2
  686. package/dist/core/web/public/next/assets/Swimlane-BzH6OCee.js.map +0 -1
  687. package/dist/core/web/public/next/assets/Swimlane-o6_Zsrnr.css +0 -1
  688. package/dist/core/web/public/next/assets/UniverseConsoleApp-DtXFhAhw.js +0 -2
  689. package/dist/core/web/public/next/assets/UsageSection-CiD1DbuF.js +0 -3
  690. package/dist/core/web/public/next/assets/UsageSection-CiD1DbuF.js.map +0 -1
  691. package/dist/core/web/public/next/assets/VerseConsoleApp-BCY5uSok.js +0 -3
  692. package/dist/core/web/public/next/assets/VerseConsoleApp-BCY5uSok.js.map +0 -1
  693. package/dist/core/web/public/next/assets/Workspace-BQSZFSeO.js +0 -10
  694. package/dist/core/web/public/next/assets/actions-BmIf9avl.css +0 -1
  695. package/dist/core/web/public/next/assets/actions-Ci6I4C0l.js +0 -2
  696. package/dist/core/web/public/next/assets/budget-queries-DsAjH8ac.js +0 -2
  697. package/dist/core/web/public/next/assets/charts-C_xJbJiw.js +0 -2
  698. package/dist/core/web/public/next/assets/charts-C_xJbJiw.js.map +0 -1
  699. package/dist/core/web/public/next/assets/charts-DhKTrQLw.css +0 -1
  700. package/dist/core/web/public/next/assets/cloud-model-CX407gPY.js +0 -2
  701. package/dist/core/web/public/next/assets/command-catalog-CF0cQQac.js.map +0 -1
  702. package/dist/core/web/public/next/assets/context-queries-CktFQqSS.js +0 -2
  703. package/dist/core/web/public/next/assets/copy-setup-CqRSfELB.js +0 -2
  704. package/dist/core/web/public/next/assets/copy-setup-CqRSfELB.js.map +0 -1
  705. package/dist/core/web/public/next/assets/format-Dk4Fmxax.js +0 -2
  706. package/dist/core/web/public/next/assets/format-Dk4Fmxax.js.map +0 -1
  707. package/dist/core/web/public/next/assets/global-CNNGMril.js +0 -2
  708. package/dist/core/web/public/next/assets/guarded-runners-DPh2d32e.js +0 -2
  709. package/dist/core/web/public/next/assets/live-model-De-q0ZFK.js.map +0 -1
  710. package/dist/core/web/public/next/assets/mutations-DTCtKCn_.js +0 -2
  711. package/dist/core/web/public/next/assets/primitives-D0Fb3TtS.js +0 -1
  712. package/dist/core/web/public/next/assets/seat-subscription-C5Tvr0WQ.js +0 -2
  713. package/dist/core/web/public/next/assets/surface-prefetch-BWr0LBQn.js +0 -2
  714. package/dist/core/web/public/next/assets/surface-prefetch-BWr0LBQn.js.map +0 -1
  715. package/dist/core/web/public/next/assets/types-D5XOdyw8.js +0 -2
  716. package/dist/core/web/public/next/assets/types-D5XOdyw8.js.map +0 -1
  717. package/dist/core/web/public/next/assets/verse-model-MMySehjj.js +0 -2
  718. package/dist/core/web/public/next/assets/why-seat-model-jlDTwYSc.js +0 -2
  719. package/dist/core/web/public/next/assets/why-seat-model-jlDTwYSc.js.map +0 -1
@@ -15,13 +15,16 @@
15
15
  * github-app [--org <login>] [--yes] GitHub App manifest flow; the key goes straight to custody
16
16
  * rotate-provenance [--yes] replace the provenance HMAC key agents could read
17
17
  * setup [--dry-run [--json]] [--yes] [--source <ashlr-hub checkout>] the guided Phase-0 command
18
+ * resident start|stop|status [--json] the resident daemon service under the grant (docs/RESIDENT-RUNTIME.md)
18
19
  *
19
20
  * LOWERING never asks anything. Everything that raises authority or touches
20
21
  * GitHub asks first (or needs --yes) and prints exactly what it did. Nothing
21
- * here ever runs `sudo`, changes launchd, or prints a secret.
22
+ * here ever runs `sudo` or prints a secret. Only `resident start` / `resident
23
+ * stop` change launchd, only for ai.ashlr.daemon, and `start` only after the
24
+ * operator confirms at a terminal under an active standing grant (no --yes).
22
25
  */
23
- import { spawnSync } from 'node:child_process';
24
- import { createPublicKey, randomBytes } from 'node:crypto';
26
+ import { execFile, spawnSync } from 'node:child_process';
27
+ import { createHash, createPublicKey, randomBytes } from 'node:crypto';
25
28
  import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
26
29
  import { createServer } from 'node:http';
27
30
  import { homedir, tmpdir } from 'node:os';
@@ -30,9 +33,10 @@ import { createInterface } from 'node:readline';
30
33
  import { CUSTODY_HELPER_PATH } from '../core/authority/custody-client.js';
31
34
  // Loaded lazily: `authority stop` and friends must never depend on the daemon
32
35
  // service module being importable.
33
- async function realDaemonService() {
34
- const { serviceStatus } = await import('../core/daemon/service.js');
35
- const status = serviceStatus();
36
+ // `cached` (the Verse probe) reuses a launchd read up to 15 s old.
37
+ async function realDaemonService(cached = false) {
38
+ const { serviceStatus, serviceStatusCached } = await import('../core/daemon/service.js');
39
+ const status = cached ? serviceStatusCached() : serviceStatus();
36
40
  if (status.running)
37
41
  return 'running';
38
42
  if (status.registrationState === 'absent')
@@ -117,8 +121,118 @@ async function defaultDeps() {
117
121
  fetch: globalThis.fetch,
118
122
  custody: await import('../core/authority/custody-client.js'),
119
123
  daemonService: realDaemonService,
124
+ resident: realResidentDeps,
120
125
  };
121
126
  }
127
+ async function residentServiceOptions() {
128
+ const { loadConfigReadOnlyStrict } = await import('../core/config.js');
129
+ const { daemonServiceInstallOptions } = await import('../core/daemon/service-config.js');
130
+ let cfg;
131
+ try {
132
+ cfg = loadConfigReadOnlyStrict();
133
+ }
134
+ catch (error) {
135
+ return { ok: false, problem: `config.json could not be read (${error.message.slice(0, 160)})` };
136
+ }
137
+ // ALWAYS regenerated from config (daemon.dailyBudgetUsd / intervalMs /
138
+ // parallel) — never carried over from an earlier install, so a budget
139
+ // changed in config reaches the service on the next `resident start`.
140
+ const opts = daemonServiceInstallOptions(cfg, { autostart: true });
141
+ return { ok: true, opts, budgetUsd: opts.budget ?? null };
142
+ }
143
+ /** The installed service file: its text, null when absent, undefined when unreadable. */
144
+ function readServiceFile(path) {
145
+ try {
146
+ return readFileSync(path, 'utf8');
147
+ }
148
+ catch (error) {
149
+ return error.code === 'ENOENT' ? null : undefined;
150
+ }
151
+ }
152
+ const realResidentDeps = {
153
+ admission: async () => (await import('../core/authority/resident.js')).observeResidentAdmission().admission,
154
+ observe: async () => {
155
+ const { residentPlistState, plistBudgetUsd } = await import('../core/authority/resident.js');
156
+ const { generateServiceDefinition } = await import('../core/daemon/service.js');
157
+ let state;
158
+ try {
159
+ state = await realDaemonService();
160
+ }
161
+ catch {
162
+ state = 'unknown';
163
+ }
164
+ const prepared = await residentServiceOptions();
165
+ const def = generateServiceDefinition(prepared.ok ? prepared.opts : {});
166
+ const installed = readServiceFile(def.filePath);
167
+ return {
168
+ state,
169
+ plist: residentPlistState(installed, prepared.ok ? def.content : null),
170
+ plistPath: def.filePath,
171
+ installedBudgetUsd: plistBudgetUsd(installed ?? null),
172
+ expectedBudgetUsd: prepared.ok ? prepared.budgetUsd : null,
173
+ problem: prepared.ok ? null : prepared.problem,
174
+ };
175
+ },
176
+ operatorRefusal: async () => {
177
+ const { operatorContextRefusal, currentOperatorContext } = await import('../core/authority/resident.js');
178
+ return operatorContextRefusal(currentOperatorContext());
179
+ },
180
+ start: async () => {
181
+ const resident = await import('../core/authority/resident.js');
182
+ const service = await import('../core/daemon/service.js');
183
+ const { appendLedger } = await import('../core/authority/ledger.js');
184
+ const prepared = await residentServiceOptions();
185
+ if (!prepared.ok)
186
+ return { ok: false, reason: prepared.problem };
187
+ // The mint re-observes the operator context and re-verifies the grant,
188
+ // the build identity and the authority surface itself: nothing this
189
+ // command computed or printed earlier counts toward the decision.
190
+ const minted = resident.mintResidentServiceCapability();
191
+ if (!minted.ok)
192
+ return { ok: false, reason: minted.reason };
193
+ const def = service.generateServiceDefinition(prepared.opts);
194
+ const digest = createHash('sha256').update(def.content, 'utf8').digest('hex');
195
+ const grantId = minted.capability.grantId;
196
+ const detail = `release ${minted.admission.revision?.slice(0, 12) ?? '?'} · plist sha256 ${digest.slice(0, 16)} · `
197
+ + `budget $${prepared.budgetUsd ?? '?'}/day · interval ${prepared.opts.intervalMs ?? '?'} ms · parallel ${prepared.opts.parallel ?? '?'}`;
198
+ // Fail closed: a start the authority ledger cannot record does not happen.
199
+ const recorded = appendLedger({ kind: 'note', actor: 'mason', grantId, repo: null, data: { topic: 'resident-service:start', detail } });
200
+ if (!recorded.ok)
201
+ return { ok: false, reason: `the ledger refused the start record: ${recorded.reason}` };
202
+ try {
203
+ await service.installResidentService(prepared.opts, minted.capability);
204
+ }
205
+ catch (error) {
206
+ appendLedger({ kind: 'note', actor: 'mason', grantId, repo: null, data: { topic: 'resident-service:start-failed', detail: error.message.slice(0, 300) } });
207
+ return { ok: false, reason: error.message };
208
+ }
209
+ const status = service.serviceStatus(prepared.opts);
210
+ const grant = `grant #${minted.admission.grantSeq ?? '?'}`;
211
+ return {
212
+ ok: true,
213
+ detail: status.running
214
+ ? `ai.ashlr.daemon is running under ${grant} (${detail})`
215
+ : `ai.ashlr.daemon is installed and loaded under ${grant}, but launchd has not reported it running yet — check \`${resident.RESIDENT_STATUS_COMMAND}\` (${detail})`,
216
+ };
217
+ },
218
+ stop: async () => {
219
+ const service = await import('../core/daemon/service.js');
220
+ try {
221
+ await service.uninstall({});
222
+ }
223
+ catch (error) {
224
+ return { ok: false, reason: error.message };
225
+ }
226
+ try {
227
+ const { appendLedger } = await import('../core/authority/ledger.js');
228
+ appendLedger({ kind: 'note', actor: 'mason', grantId: null, repo: null, data: { topic: 'resident-service:stop', detail: 'ai.ashlr.daemon booted out and its plist removed' } });
229
+ }
230
+ catch {
231
+ // Lowering never depends on the ledger.
232
+ }
233
+ return { ok: true, detail: 'ai.ashlr.daemon is stopped and its plist removed' };
234
+ },
235
+ };
122
236
  const VALUE_FLAGS = new Set(['--reason', '--payload', '--switch', '--limit', '--kind', '--repo', '--org', '--source']);
123
237
  function parseArgs(args) {
124
238
  const out = { positional: [], flags: new Set(), values: new Map() };
@@ -163,6 +277,9 @@ const USAGE = `Usage: ashlr authority <command>
163
277
  rotate-provenance [--yes] Replace the provenance HMAC key
164
278
  setup [--dry-run [--json]] [--yes] [--source <checkout>]
165
279
  Guided Phase 0: custody, trust root, App, token, canary, rulesets, first grant, daemon
280
+ resident start Install / restart the resident daemon under the active grant (you confirm here)
281
+ resident stop Boot the resident daemon out and remove its plist (Stop is separate)
282
+ resident status [--json] Resident admission, service state and plist drift
166
283
 
167
284
  Lowering authority never asks. Anything that raises it asks first (or takes --yes).`;
168
285
  // ---------------------------------------------------------------------------
@@ -211,6 +328,8 @@ export async function runAuthorityCli(args, injected) {
211
328
  return await cmdRotateProvenance(parsed, deps);
212
329
  case 'setup':
213
330
  return await cmdSetup(parsed, deps);
331
+ case 'resident':
332
+ return await cmdResident(rest[0], parsed, deps);
214
333
  default:
215
334
  deps.err(`Unknown authority command: ${command}`);
216
335
  deps.out(USAGE);
@@ -249,12 +368,48 @@ async function cmdStatus(parsed, deps) {
249
368
  const merging = status.policy.repos.filter((repo) => repo.stage === 'merge').map((repo) => repo.nameWithOwner);
250
369
  deps.out(`Policy: ${status.policy.repos.length} repos (${merging.length} merging), engines ${status.policy.engines.join(', ')}, caps ${status.policy.merge.maxFiles}/${status.policy.merge.maxLines}`);
251
370
  }
371
+ for (const line of await enforcementStatusLines(deps))
372
+ deps.out(line);
252
373
  deps.out(`Ledger: ${status.ledger.state}${status.ledger.head ? `, ${status.ledger.head.seq + 1} entries, head ${status.ledger.head.hash.slice(0, 12)}` : ''}${status.ledger.reason ? ` — ${status.ledger.reason}` : ''}`);
253
374
  const c = status.custody;
254
375
  const yn = (v) => (v === null ? 'unknown' : v ? 'yes' : 'no');
255
376
  deps.out(`Custody: installed ${yn(c.installed)}, key ${yn(c.keyInitialized)}, GitHub App ${yn(c.githubApp)}, Claude token ${yn(c.claudeToken)}`);
256
377
  return 0;
257
378
  }
379
+ /**
380
+ * `status`: does GitHub still enforce what the installed grant says? Only the
381
+ * grant's `server` repos are read (local ones need nothing from GitHub). A
382
+ * mismatch is REPORTED, never fixed here — the signed grant is only ever
383
+ * changed by a re-approval, which switches such a repo to local enforcement.
384
+ */
385
+ async function enforcementStatusLines(deps) {
386
+ const { readInstalledGrant } = await import('../core/authority/standing-grant.js');
387
+ const installed = readInstalledGrant();
388
+ if (installed.state !== 'ok')
389
+ return [];
390
+ const grant = installed.envelope.payload;
391
+ const server = grant.repos.filter((repo) => repo.enforcement === 'server');
392
+ if (server.length === 0)
393
+ return [];
394
+ const { enforcementMismatch, probeServerEnforcementAll } = await import('../core/authority/server-enforcement.js');
395
+ const probes = await probeServerEnforcementAll(server.map((repo) => repo.nameWithOwner), ghGet(deps));
396
+ const lines = [];
397
+ const unread = [];
398
+ let enforced = 0;
399
+ for (const repo of server) {
400
+ const probe = probes.get(repo.nameWithOwner.toLowerCase());
401
+ const mismatch = enforcementMismatch(repo, probe, grant.grantSeq);
402
+ if (mismatch)
403
+ lines.push(` ${mismatch}`);
404
+ else if (probe?.state === 'enforced')
405
+ enforced += 1;
406
+ else
407
+ unread.push(repo.nameWithOwner);
408
+ }
409
+ const head = `Enforcement: ${enforced}/${server.length} server-enforced repo(s) protected by GitHub` +
410
+ `${unread.length > 0 ? `; could not check ${shortList(unread)}` : ''}${lines.length > 0 ? ` — ${lines.length} mismatch${lines.length === 1 ? '' : 'es'}:` : ''}`;
411
+ return [head, ...lines];
412
+ }
258
413
  const SWITCHES = ['off', 'propose', 'autonomous'];
259
414
  async function cmdSwitch(to, deps) {
260
415
  if (!to || !SWITCHES.includes(to)) {
@@ -334,7 +489,7 @@ async function cmdRevoke(parsed, deps) {
334
489
  async function cmdDraft(parsed, deps) {
335
490
  const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
336
491
  const kind = parsed.flags.has('--new') ? 'new' : parsed.flags.has('--reapprove') ? 'reapprove' : 'auto';
337
- const draft = await buildStandingGrantDraft(kind);
492
+ const draft = await buildStandingGrantDraft(kind, Date.now(), { githubGet: ghGet(deps) });
338
493
  if (parsed.flags.has('--json')) {
339
494
  deps.out(JSON.stringify({ kind: draft.kind, digest: draft.digest, payload: draft.payload }, null, 2));
340
495
  return 0;
@@ -382,7 +537,7 @@ async function cmdGrant(parsed, deps, kind) {
382
537
  }
383
538
  else {
384
539
  const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
385
- payload = (await buildStandingGrantDraft(kind)).payload;
540
+ payload = (await buildStandingGrantDraft(kind, Date.now(), { githubGet: ghGet(deps) })).payload;
386
541
  }
387
542
  deps.out(kind === 'new' ? 'You are about to sign this standing grant:' : 'You are about to re-approve (continue) this standing grant:');
388
543
  for (const line of describeGrantScope(payload))
@@ -520,68 +675,266 @@ export function buildFleetRuleset(requiredChecks) {
520
675
  ],
521
676
  };
522
677
  }
523
- function ghJson(deps, args) {
524
- const result = deps.run('gh', args);
678
+ /** A `gh api <path>` GET through the CLI's runner (tests inject it; the Verse probe's runner is read-only). */
679
+ function ghGet(deps) {
680
+ return async (path) => deps.run('gh', ['api', path]);
681
+ }
682
+ async function ghJson(deps, args) {
683
+ const result = await deps.run('gh', args);
525
684
  if (result.status !== 0)
526
685
  throw new Error(`gh ${args.slice(0, 3).join(' ')} failed: ${result.stderr.trim().slice(0, 200)}`);
527
686
  return JSON.parse(result.stdout || 'null');
528
687
  }
529
688
  /** Check-run names on the default branch head: what CI actually reports today. */
530
- function discoverChecks(deps, repo) {
531
- const info = ghJson(deps, ['api', `repos/${repo}`]);
532
- const branch = info.default_branch ?? 'main';
533
- const runs = ghJson(deps, ['api', `repos/${repo}/commits/${encodeURIComponent(branch)}/check-runs?per_page=100`]);
689
+ /**
690
+ * 3.13: with `verifyAppId` (the repo is in the grant, the fleet can verify it
691
+ * and the ashlr-fleet App's id is known) the host-verified `ashlr/verify` is
692
+ * always required, pinned to the App — even when the default branch has no
693
+ * runs at all (Actions off), which is exactly the repo it exists for. A
694
+ * same-named run from another App never replaces the pin.
695
+ */
696
+ async function discoverChecks(deps, repo, branch, verifyAppId = null) {
697
+ const runs = await ghJson(deps, ['api', `repos/${repo}/commits/${encodeURIComponent(branch)}/check-runs?per_page=100`]);
534
698
  const found = [];
535
699
  for (const run of runs.check_runs ?? []) {
536
700
  if (typeof run.name !== 'string' || run.name.length === 0)
537
701
  continue;
702
+ if (verifyAppId !== null && run.name === ASHLR_VERIFY_CHECK)
703
+ continue;
538
704
  const appId = run.app?.id;
539
705
  found.push({ context: run.name, integrationId: typeof appId === 'number' && Number.isSafeInteger(appId) && appId > 0 ? appId : null });
540
706
  }
707
+ if (verifyAppId !== null)
708
+ found.push({ context: ASHLR_VERIFY_CHECK, integrationId: verifyAppId });
541
709
  return normalizeRequiredChecks(found);
542
710
  }
543
- async function protectPlans(parsed, deps) {
544
- let repos = parsed.values.get('--repo') ?? [];
545
- let enforcement = new Map();
546
- if (repos.length === 0) {
547
- const { readInstalledGrant } = await import('../core/authority/standing-grant.js');
548
- const installed = readInstalledGrant();
549
- let grantRepos = [];
550
- if (installed.state === 'ok')
551
- grantRepos = installed.envelope.payload.repos;
552
- else {
553
- const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
554
- grantRepos = (await buildStandingGrantDraft('new')).payload.repos;
711
+ /** The check the ashlr-fleet App posts on fleet PR heads (core/fleet/verify-check-run.ts ASHLR_VERIFY_CHECK_NAME). */
712
+ export const ASHLR_VERIFY_CHECK = 'ashlr/verify';
713
+ /** The ashlr-fleet App as GitHub describes it to your own gh auth (a `gh api` read); null = unreadable. */
714
+ export async function readFleetApp(deps) {
715
+ try {
716
+ const app = await ghJson(deps, ['api', `apps/${FLEET_APP_NAME}`]);
717
+ if (!app || typeof app.id !== 'number' || !Number.isSafeInteger(app.id) || app.id <= 0)
718
+ return null;
719
+ const slug = typeof app.slug === 'string' && /^[a-z0-9-]{1,100}$/.test(app.slug) ? app.slug : FLEET_APP_NAME;
720
+ const ownerLogin = typeof app.owner?.login === 'string' && /^[A-Za-z0-9-]{1,39}$/.test(app.owner.login) ? app.owner.login : '';
721
+ const checks = typeof app.permissions?.['checks'] === 'string' ? app.permissions['checks'] : null;
722
+ return { id: app.id, slug, ownerLogin, ownerIsOrg: app.owner?.type === 'Organization', checks };
723
+ }
724
+ catch {
725
+ return null;
726
+ }
727
+ }
728
+ /** PURE: where the App's permissions (and then each installation's acceptance) are changed. */
729
+ export function fleetAppPermissionUrls(app) {
730
+ const base = app.ownerIsOrg && app.ownerLogin
731
+ ? `https://github.com/organizations/${app.ownerLogin}/settings`
732
+ : 'https://github.com/settings';
733
+ return { permissions: `${base}/apps/${app.slug}/permissions`, installations: `${base}/installations` };
734
+ }
735
+ /** True when the fleet mirror of `repo` exists and the fleet detects at least one required verify command there. */
736
+ async function fleetCanVerify(repo) {
737
+ try {
738
+ const { mirrorPathFor } = await import('../core/fleet/mirrors.js');
739
+ const { detectVerifyCommands } = await import('../core/run/verify-commands.js');
740
+ const mirror = mirrorPathFor(repo);
741
+ if (!existsSync(mirror))
742
+ return false;
743
+ return detectVerifyCommands(mirror, 'merge').some((command) => command.required !== false);
744
+ }
745
+ catch {
746
+ return false;
747
+ }
748
+ }
749
+ /**
750
+ * PURE: `actual` (a ruleset as GitHub returns it) already says everything
751
+ * `desired` says. GitHub adds ids, links, timestamps and server-side rule
752
+ * defaults to what it returns, so extra object keys are ignored — but every
753
+ * field we set must match, and arrays must match one-to-one in any order, so
754
+ * an extra rule, an extra bypass actor or an extra required check is drift
755
+ * (re-applying would remove it). Anything unreadable is "not up to date".
756
+ */
757
+ export function rulesetMatches(desired, actual) {
758
+ if (Array.isArray(desired)) {
759
+ if (!Array.isArray(actual) || actual.length !== desired.length)
760
+ return false;
761
+ const unused = [...actual];
762
+ for (const wanted of desired) {
763
+ const at = unused.findIndex((candidate) => rulesetMatches(wanted, candidate));
764
+ if (at < 0)
765
+ return false;
766
+ unused.splice(at, 1);
767
+ }
768
+ return true;
769
+ }
770
+ if (desired !== null && typeof desired === 'object') {
771
+ if (actual === null || typeof actual !== 'object' || Array.isArray(actual))
772
+ return false;
773
+ const have = actual;
774
+ return Object.entries(desired).every(([key, value]) => rulesetMatches(value, have[key]));
775
+ }
776
+ return Object.is(desired, actual);
777
+ }
778
+ /**
779
+ * The repos the fleet works in: the installed grant's, or — before the first
780
+ * grant — the ones it would name (every enrolled checkout's GitHub repo plus
781
+ * the canary; authority-api draftRepos), whose enforcement is not known yet
782
+ * (null): the draft names `server` only where GitHub then enforces required
783
+ * checks (authority/server-enforcement.ts). Read-only: no draft is built and the ledger is not
784
+ * touched, so a dry run (and Verse's checklist) can ask it freely.
785
+ */
786
+ async function setupTargetRepos() {
787
+ const { readInstalledGrant, FLEET_CANARY_REPO } = await import('../core/authority/standing-grant.js');
788
+ const installed = readInstalledGrant();
789
+ if (installed.state === 'ok') {
790
+ return installed.envelope.payload.repos.map((repo) => ({ nameWithOwner: repo.nameWithOwner, enforcement: repo.enforcement }));
791
+ }
792
+ const byKey = new Map();
793
+ try {
794
+ const { readEnrollmentRegistry } = await import('../core/sandbox/policy.js');
795
+ const { repoIdentityOfPath } = await import('../core/fleet/repo-identity.js');
796
+ const registry = readEnrollmentRegistry();
797
+ if (registry.state === 'ready') {
798
+ for (const path of registry.repos) {
799
+ const nameWithOwner = repoIdentityOfPath(path);
800
+ if (nameWithOwner && !byKey.has(nameWithOwner.toLowerCase()))
801
+ byKey.set(nameWithOwner.toLowerCase(), { nameWithOwner, enforcement: null });
802
+ }
555
803
  }
556
- repos = grantRepos.map((r) => r.nameWithOwner);
557
- enforcement = new Map(grantRepos.map((r) => [r.nameWithOwner, r.enforcement]));
558
804
  }
805
+ catch {
806
+ // An unreadable registry leaves the canary alone on the list.
807
+ }
808
+ if (!byKey.has(FLEET_CANARY_REPO))
809
+ byKey.set(FLEET_CANARY_REPO, { nameWithOwner: FLEET_CANARY_REPO, enforcement: null });
810
+ return [...byKey.values()];
811
+ }
812
+ /** Read-only: what `protect --apply` would do for each repo (every gh call here is a GET). */
813
+ async function protectPlans(parsed, deps, given) {
814
+ const explicit = parsed.values.get('--repo') ?? [];
815
+ const byGrant = given !== undefined || explicit.length === 0;
816
+ const targets = given
817
+ ?? (explicit.length > 0 ? explicit.map((nameWithOwner) => ({ nameWithOwner, enforcement: null })) : await setupTargetRepos());
818
+ // Repos the grant covers (installed grant, or the would-be grant's own list):
819
+ // only those get the fleet's ashlr/verify requirement.
820
+ const grantMembers = new Set();
821
+ const { readInstalledGrant } = await import('../core/authority/standing-grant.js');
822
+ const installed = readInstalledGrant();
823
+ if (installed.state === 'ok')
824
+ for (const r of installed.envelope.payload.repos)
825
+ grantMembers.add(r.nameWithOwner.toLowerCase());
826
+ if (byGrant)
827
+ for (const r of targets)
828
+ grantMembers.add(r.nameWithOwner.toLowerCase());
829
+ const grantEnforcement = new Map();
830
+ if (installed.state === 'ok')
831
+ for (const r of installed.envelope.payload.repos)
832
+ grantEnforcement.set(r.nameWithOwner.toLowerCase(), r.enforcement);
833
+ const grantSeq = installed.state === 'ok' ? installed.envelope.payload.grantSeq : null;
834
+ const { classifyGithubReadFailure, describeGithubReadFailure, enforcementMismatch } = await import('../core/authority/server-enforcement.js');
835
+ const unreadable = (repo, error) => {
836
+ const message = error.message;
837
+ return { repo, skipped: `could not read it from GitHub (${describeGithubReadFailure(classifyGithubReadFailure({ message }))}): ${message}`, checks: [], ruleset: null, existingId: null, upToDate: false };
838
+ };
839
+ let fleetApp;
559
840
  const plans = [];
560
- for (const repo of repos) {
561
- if (enforcement.get(repo) === 'local') {
562
- plans.push({ repo, skipped: 'local enforcement (private free-plan repo: GitHub rulesets unavailable) — the daemon enforces its gates', checks: [], ruleset: null, existingId: null });
841
+ for (const { nameWithOwner: repo, enforcement } of targets) {
842
+ if (enforcement === 'local') {
843
+ plans.push({ repo, skipped: `local enforcement in the grant — GitHub protects nothing here; the daemon enforces its gates and G7 requires the App's host-verified ${ASHLR_VERIFY_CHECK}`, checks: [], ruleset: null, existingId: null, upToDate: false });
844
+ continue;
845
+ }
846
+ let info;
847
+ try {
848
+ info = await ghJson(deps, ['api', `repos/${repo}`]);
849
+ }
850
+ catch (error) {
851
+ plans.push(unreadable(repo, error));
852
+ continue;
853
+ }
854
+ // Rulesets before anything else: on GitHub Free a PRIVATE repo has neither
855
+ // rulesets nor classic branch protection (both answer 403 "Upgrade to
856
+ // GitHub Pro or make this repository public…", verified live), which is a
857
+ // plan limit — not a permission problem and not a network error — so it is
858
+ // reported as such, and only `local` enforcement can work there.
859
+ const listed = await deps.run('gh', ['api', `repos/${repo}/rulesets`]);
860
+ if (listed.status !== 0 && classifyGithubReadFailure({ message: listed.stderr }) === 'plan-unavailable') {
861
+ const kind = info?.private === false ? 'repo' : 'private repo';
862
+ const mismatch = grantEnforcement.get(repo.toLowerCase()) === 'server'
863
+ ? enforcementMismatch({ nameWithOwner: repo, enforcement: 'server' }, { nameWithOwner: repo, state: 'unavailable', private: info?.private ?? null, failure: 'plan-unavailable', detail: '' }, grantSeq)
864
+ : null;
865
+ plans.push({
866
+ repo,
867
+ skipped: mismatch ?? `rulesets unavailable on this plan for ${kind} ${repo} — using local enforcement with ${ASHLR_VERIFY_CHECK}`,
868
+ checks: [],
869
+ ruleset: null,
870
+ existingId: null,
871
+ upToDate: false,
872
+ unavailable: true,
873
+ mismatch,
874
+ });
563
875
  continue;
564
876
  }
565
877
  try {
566
- const info = ghJson(deps, ['api', `repos/${repo}`]);
567
- const required = discoverChecks(deps, repo);
878
+ if (listed.status !== 0)
879
+ throw new Error(`gh api repos/${repo}/rulesets failed: ${listed.stderr.trim().slice(0, 200)}`);
880
+ let verifyAppId = null;
881
+ let verifyNote = null;
882
+ if (grantMembers.has(repo.toLowerCase())) {
883
+ if (!(await fleetCanVerify(repo))) {
884
+ verifyNote = `${ASHLR_VERIFY_CHECK} not required: the fleet mirror has no verify command yet (rerun protect once it has synced)`;
885
+ }
886
+ else {
887
+ if (fleetApp === undefined)
888
+ fleetApp = await readFleetApp(deps);
889
+ if (fleetApp)
890
+ verifyAppId = fleetApp.id;
891
+ else
892
+ verifyNote = `${ASHLR_VERIFY_CHECK} not required: the ${FLEET_APP_NAME} App could not be read (\`gh api apps/${FLEET_APP_NAME}\`)`;
893
+ }
894
+ }
895
+ const required = await discoverChecks(deps, repo, info?.default_branch ?? 'main', verifyAppId);
568
896
  const checks = required.map((check) => check.context);
569
- const existing = ghJson(deps, ['api', `repos/${repo}/rulesets`]);
897
+ const existing = JSON.parse(listed.stdout || 'null');
570
898
  const match = Array.isArray(existing) ? existing.find((r) => r.name === FLEET_RULESET_NAME) : undefined;
899
+ const existingId = typeof match?.id === 'number' && Number.isSafeInteger(match.id) ? match.id : null;
900
+ const ruleset = buildFleetRuleset(required);
901
+ // Rerun-safe: a ruleset already on GitHub with this exact content is
902
+ // reported as in place instead of being PUT again.
903
+ const upToDate = existingId !== null && rulesetMatches(ruleset, await ghJson(deps, ['api', `repos/${repo}/rulesets/${existingId}`]));
571
904
  plans.push({
572
905
  repo,
573
- skipped: info.private === true && checks.length === 0 ? 'private repo with no CI checks — protect it by hand or keep it propose-only' : null,
906
+ skipped: info?.private === true && checks.length === 0 ? 'private repo with no CI checks — protect it by hand or keep it propose-only' : null,
574
907
  checks,
575
- ruleset: buildFleetRuleset(required),
576
- existingId: typeof match?.id === 'number' ? match.id : null,
908
+ ruleset,
909
+ existingId,
910
+ upToDate,
911
+ verifyNote,
577
912
  });
578
913
  }
579
914
  catch (error) {
580
- plans.push({ repo, skipped: `could not read it from GitHub: ${error.message}`, checks: [], ruleset: null, existingId: null });
915
+ plans.push(unreadable(repo, error));
581
916
  }
582
917
  }
583
918
  return plans;
584
919
  }
920
+ /** Create or update each plan's ruleset; the number that failed. */
921
+ async function applyProtectPlans(plans, deps) {
922
+ let failures = 0;
923
+ for (const plan of plans) {
924
+ const result = await deps.run('gh', [
925
+ 'api', '--method', plan.existingId === null ? 'POST' : 'PUT',
926
+ `repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`}`,
927
+ '--input', '-',
928
+ ], { input: JSON.stringify(plan.ruleset) });
929
+ if (result.status === 0)
930
+ deps.out(`${plan.repo}: ruleset ${plan.existingId === null ? 'created' : 'updated'}.`);
931
+ else {
932
+ failures += 1;
933
+ deps.err(`${plan.repo}: failed — ${result.stderr.trim().slice(0, 300)}`);
934
+ }
935
+ }
936
+ return failures;
937
+ }
585
938
  async function cmdProtect(parsed, deps) {
586
939
  const apply = parsed.flags.has('--apply');
587
940
  if (!apply && !parsed.flags.has('--print')) {
@@ -594,7 +947,17 @@ async function cmdProtect(parsed, deps) {
594
947
  deps.out(`${plan.repo}: skipped — ${plan.skipped}`);
595
948
  continue;
596
949
  }
597
- deps.out(`${plan.repo}: ${plan.existingId === null ? 'create' : `update ruleset ${plan.existingId}`}; required checks: ${plan.checks.length > 0 ? plan.checks.join(', ') : 'none found (the repo goes to the owner lane until it has CI)'}`);
950
+ const checks = plan.checks.length > 0 ? plan.checks.join(', ') : 'none found (the repo goes to the owner lane until it has CI)';
951
+ if (plan.upToDate) {
952
+ deps.out(`${plan.repo}: ruleset ${plan.existingId} already up to date; required checks: ${checks}`);
953
+ continue;
954
+ }
955
+ deps.out(`${plan.repo}: ${plan.existingId === null ? 'create' : `update ruleset ${plan.existingId}`}; required checks: ${checks}`);
956
+ if (plan.verifyNote)
957
+ deps.out(` note: ${plan.verifyNote}`);
958
+ else if (plan.checks.includes(ASHLR_VERIFY_CHECK)) {
959
+ deps.out(` note: ${ASHLR_VERIFY_CHECK} is posted only on fleet PRs; your own PRs need the admin bypass unless another required check covers them`);
960
+ }
598
961
  if (!apply) {
599
962
  deps.out(` gh api --method ${plan.existingId === null ? 'POST' : 'PUT'} repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`} --input - <<'JSON'`);
600
963
  deps.out(JSON.stringify(plan.ruleset, null, 2));
@@ -603,37 +966,27 @@ async function cmdProtect(parsed, deps) {
603
966
  }
604
967
  if (!apply)
605
968
  return 0;
606
- const actionable = plans.filter((plan) => !plan.skipped);
969
+ const actionable = plans.filter((plan) => !plan.skipped && !plan.upToDate);
607
970
  if (actionable.length === 0)
608
971
  return 0;
609
972
  if (!parsed.flags.has('--yes') && !(await deps.confirm(`Apply the ruleset to ${actionable.length} repo(s)?`))) {
610
973
  deps.out('Nothing applied.');
611
974
  return 1;
612
975
  }
613
- let failures = 0;
614
- for (const plan of actionable) {
615
- const result = deps.run('gh', [
616
- 'api', '--method', plan.existingId === null ? 'POST' : 'PUT',
617
- `repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`}`,
618
- '--input', '-',
619
- ], { input: JSON.stringify(plan.ruleset) });
620
- if (result.status === 0)
621
- deps.out(`${plan.repo}: ruleset ${plan.existingId === null ? 'created' : 'updated'}.`);
622
- else {
623
- failures += 1;
624
- deps.err(`${plan.repo}: failed — ${result.stderr.trim().slice(0, 300)}`);
625
- }
626
- }
627
- return failures === 0 ? 0 : 1;
976
+ return (await applyProtectPlans(actionable, deps)) === 0 ? 0 : 1;
628
977
  }
629
978
  // ---------------------------------------------------------------------------
630
979
  // github-app — App Manifest flow (addendum §3)
631
980
  // ---------------------------------------------------------------------------
632
981
  export const FLEET_APP_NAME = 'ashlr-fleet';
633
982
  /**
634
- * PURE: the App manifest. Contents + pull requests read/write, checks /
635
- * statuses / metadata read. No workflows, no administration — GitHub itself
636
- * then refuses CI-config and protection changes from the fleet.
983
+ * PURE: the App manifest. Contents + pull requests read/write, checks
984
+ * read/write (3.13: the App posts the host-verified `ashlr/verify` check run
985
+ * on fleet PR heads — core/fleet/verify-check-run.ts), statuses / metadata
986
+ * read. No workflows, no administration — GitHub itself then refuses
987
+ * CI-config and protection changes from the fleet. An App created before 3.13
988
+ * has `checks: read`; `ashlr authority setup` detects that and prints where
989
+ * to raise it (fleetAppPermissionUrls).
637
990
  */
638
991
  export function buildGithubAppManifest(redirectUrl) {
639
992
  return {
@@ -642,7 +995,7 @@ export function buildGithubAppManifest(redirectUrl) {
642
995
  hook_attributes: { url: 'https://github.com/ashlrai/ashlr-hub', active: false },
643
996
  redirect_url: redirectUrl,
644
997
  public: false,
645
- default_permissions: { contents: 'write', pull_requests: 'write', checks: 'read', statuses: 'read', metadata: 'read' },
998
+ default_permissions: { contents: 'write', pull_requests: 'write', checks: 'write', statuses: 'read', metadata: 'read' },
646
999
  default_events: [],
647
1000
  };
648
1001
  }
@@ -729,26 +1082,165 @@ async function cmdGithubApp(parsed, deps) {
729
1082
  const result = await runGithubAppFlow(deps, { org });
730
1083
  deps.out(`Created ${result.slug} (App id ${result.appId}); its private key is in custody and was never written to disk.`);
731
1084
  deps.out(`Install it on the enrolled repos: ${result.installUrl}`);
1085
+ deps.out(`It can post the host-verified ${ASHLR_VERIFY_CHECK} check (checks: write). An ${FLEET_APP_NAME} App created before 3.13 cannot: \`ashlr authority setup\` prints the exact settings link to raise it.`);
732
1086
  return 0;
733
1087
  }
734
- // ---------------------------------------------------------------------------
735
- // rotate-provenance
736
- // ---------------------------------------------------------------------------
1088
+ /** Where Mason installs the App (and picks its repos). */
1089
+ export const FLEET_APP_INSTALL_URL = `https://github.com/apps/${FLEET_APP_NAME}/installations/new`;
1090
+ /** Bounded wait: a probe that does not answer in time is "unknown", never a hang. */
1091
+ async function withTimeout(work, ms, what) {
1092
+ let timer;
1093
+ try {
1094
+ return await Promise.race([
1095
+ work,
1096
+ new Promise((_, reject) => {
1097
+ timer = setTimeout(() => reject(new Error(`${what} did not answer within ${Math.round(ms / 1000)} s`)), ms);
1098
+ }),
1099
+ ]);
1100
+ }
1101
+ finally {
1102
+ if (timer)
1103
+ clearTimeout(timer);
1104
+ }
1105
+ }
1106
+ const APP_PROBE_TIMEOUT_MS = 20_000;
1107
+ /**
1108
+ * Where the ashlr-fleet App is installed — read-only, in two passes:
1109
+ *
1110
+ * 1. per owner, `gh api orgs/<owner>/installations` with Mason's own gh auth
1111
+ * (a GET; needs admin:org). An installation on "All repositories" settles
1112
+ * every repo of that owner; no installation at all settles them as missing.
1113
+ * 2. what that cannot settle (a user account, "Only select repositories", a
1114
+ * gh token without admin:org) is asked of the App itself: custody's
1115
+ * gh-token looks the repo up AS the App (GET /repos/<r>/installation under
1116
+ * the App's JWT), and a 404 there means "not installed". The one-hour token
1117
+ * the helper mints on success stays in this process's memory, is never
1118
+ * printed, and changes nothing on GitHub.
1119
+ */
1120
+ export async function probeAppInstallation(deps, repos) {
1121
+ const result = { installed: [], missing: [], unknown: [] };
1122
+ const byOwner = new Map();
1123
+ for (const repo of repos) {
1124
+ const owner = repo.split('/')[0];
1125
+ byOwner.set(owner, [...(byOwner.get(owner) ?? []), repo]);
1126
+ }
1127
+ const unsettled = [];
1128
+ for (const [owner, ownerRepos] of byOwner) {
1129
+ let listed = null;
1130
+ try {
1131
+ const read = await ghJson(deps, ['api', `orgs/${owner}/installations?per_page=100`]);
1132
+ if (read && Array.isArray(read.installations))
1133
+ listed = read.installations;
1134
+ }
1135
+ catch {
1136
+ listed = null;
1137
+ }
1138
+ const app = listed?.find((entry) => entry.app_slug === FLEET_APP_NAME);
1139
+ if (listed && !app)
1140
+ result.missing.push(...ownerRepos);
1141
+ else if (app?.repository_selection === 'all')
1142
+ result.installed.push(...ownerRepos);
1143
+ else
1144
+ unsettled.push(...ownerRepos);
1145
+ }
1146
+ // A few at a time: each lookup is two HTTPS calls inside the helper.
1147
+ const queue = [...unsettled];
1148
+ const worker = async () => {
1149
+ for (let repo = queue.shift(); repo !== undefined; repo = queue.shift()) {
1150
+ try {
1151
+ await withTimeout(deps.custody.githubToken(repo), APP_PROBE_TIMEOUT_MS, 'ashlr-custody gh-token');
1152
+ result.installed.push(repo);
1153
+ }
1154
+ catch (error) {
1155
+ const code = error.code;
1156
+ const message = error.message ?? String(error);
1157
+ if (code === 'github' && /not installed/u.test(message))
1158
+ result.missing.push(repo);
1159
+ else
1160
+ result.unknown.push({ repo, reason: message.slice(0, 160) });
1161
+ }
1162
+ }
1163
+ };
1164
+ await Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker));
1165
+ // Stable order for the checklist line, whatever order the lookups finished in.
1166
+ const order = new Map(repos.map((repo, i) => [repo, i]));
1167
+ const byInput = (a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0);
1168
+ result.installed.sort(byInput);
1169
+ result.missing.sort(byInput);
1170
+ result.unknown.sort((a, b) => byInput(a.repo, b.repo));
1171
+ return result;
1172
+ }
1173
+ /** "a, b and 3 more" — a list short enough for one checklist line. */
1174
+ function shortList(items, max = 3) {
1175
+ if (items.length <= max)
1176
+ return items.join(', ');
1177
+ return `${items.slice(0, max).join(', ')} and ${items.length - max} more`;
1178
+ }
1179
+ function provenanceRotationRecordPath(keyPath) {
1180
+ return `${keyPath}.rotation.json`;
1181
+ }
1182
+ function sha256Hex(bytes) {
1183
+ return createHash('sha256').update(bytes).digest('hex');
1184
+ }
1185
+ function readRotationRecord(keyPath) {
1186
+ try {
1187
+ const raw = JSON.parse(readFileSync(provenanceRotationRecordPath(keyPath), 'utf8'));
1188
+ if (raw.v !== 1 || typeof raw.rotatedAt !== 'string' || typeof raw.keySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(raw.keySha256))
1189
+ return null;
1190
+ return { v: 1, rotatedAt: raw.rotatedAt, keySha256: raw.keySha256 };
1191
+ }
1192
+ catch {
1193
+ return null;
1194
+ }
1195
+ }
1196
+ /** Read-only: never creates, repairs or moves the key. */
1197
+ export async function provenanceKeyState() {
1198
+ const { provenanceKeyPath, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
1199
+ let path;
1200
+ let key;
1201
+ try {
1202
+ path = provenanceKeyPath();
1203
+ key = loadExistingProvenanceKeyReadOnly();
1204
+ }
1205
+ catch (error) {
1206
+ return { state: 'unhealthy', reason: error.message.replace(homedir(), '~') };
1207
+ }
1208
+ if (!key)
1209
+ return { state: 'missing' };
1210
+ const record = readRotationRecord(path);
1211
+ // The record names the exact key it made: a key swapped in afterwards is not "already rotated".
1212
+ if (record && record.keySha256 === sha256Hex(key))
1213
+ return { state: 'rotated', rotatedAt: record.rotatedAt };
1214
+ return { state: 'unrotated' };
1215
+ }
737
1216
  /**
738
1217
  * Move the provenance HMAC key aside and create a new one. Every pending
739
1218
  * provenance signature stops verifying (by design: agents could read the old
740
1219
  * key while confinement was off), so pending proposals are re-verified.
1220
+ * Records which key it made (provenanceKeyState reads it) so setup can tell
1221
+ * a key it already rotated from the burned one.
741
1222
  */
742
1223
  export async function rotateProvenanceKey() {
743
- const { provenanceKeyPath, loadOrCreateKey } = await import('../core/foundry/provenance.js');
1224
+ const { provenanceKeyPath, loadOrCreateKey, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
744
1225
  const path = provenanceKeyPath();
745
1226
  let retiredAs = null;
746
1227
  if (existsSync(path)) {
747
1228
  retiredAs = `${path}.retired-${new Date().toISOString().replace(/[:.]/g, '-')}`;
748
1229
  renameSync(path, retiredAs);
749
1230
  }
750
- loadOrCreateKey();
751
- return { retiredAs };
1231
+ const created = loadOrCreateKey();
1232
+ // loadOrCreateKey falls back to a process-lifetime key when it cannot write
1233
+ // one; that is not a rotation — say so instead of recording it.
1234
+ const stored = loadExistingProvenanceKeyReadOnly();
1235
+ if (!stored || !stored.equals(created))
1236
+ throw new Error('the new provenance key could not be stored in ~/.ashlr/foundry');
1237
+ const rotatedAt = new Date().toISOString();
1238
+ const record = { v: 1, rotatedAt, keySha256: sha256Hex(stored) };
1239
+ const target = provenanceRotationRecordPath(path);
1240
+ const tmp = `${target}.${process.pid}.tmp`;
1241
+ writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
1242
+ renameSync(tmp, target);
1243
+ return { retiredAs, rotatedAt };
752
1244
  }
753
1245
  async function cmdRotateProvenance(parsed, deps) {
754
1246
  if (!parsed.flags.has('--yes') && !(await deps.confirm('Replace the provenance HMAC key? Pending proposals will need re-verification.'))) {
@@ -840,12 +1332,41 @@ const SETUP_STEP_NEEDS = Object.freeze({
840
1332
  'standing-grant': ['touch-id'],
841
1333
  'autonomy-switch': [],
842
1334
  'daemon-service': ['terminal'],
843
- 'resident-runtime': [],
1335
+ 'resident-runtime': ['terminal'],
844
1336
  });
845
- // The packaged runtime still has no resident-start broker or production
846
- // authority roots. This is a source/release gate, not a missing setup action.
847
- // Keep the checklist blocked until a reviewed resident consumer can prove it.
848
- const RESIDENT_RUNTIME_BLOCK = 'Resident activation is unavailable in this build: service install/restart authority, compiled daemon and conductor roots, and the native resident-start broker are absent. Preparing the other steps does not start autonomous work.';
1337
+ /** The one command that walks every step and resumes where the last run stopped. */
1338
+ export const AUTHORITY_SETUP_COMMAND = 'ashlr authority setup';
1339
+ /** The only command that installs or restarts the resident service (you, at a terminal, under the grant). */
1340
+ const RESIDENT_START_COMMAND = 'ashlr authority resident start';
1341
+ /**
1342
+ * The command that moves each step on while it is not in place. Setup is
1343
+ * rerun-safe, so for everything it performs itself the answer is setup; the
1344
+ * rest is what only Mason runs (sudo, the build, the grant, resident start).
1345
+ * A row may name its own (the resident step names what its admission asks
1346
+ * for, or none when a prerequisite outside this Mac's commands is missing).
1347
+ */
1348
+ const SETUP_STEP_COMMAND = Object.freeze({
1349
+ 'custody-helper': 'sudo scripts/install-custody.sh',
1350
+ 'host-binding': 'sudo scripts/install-custody.sh',
1351
+ 'signing-key': AUTHORITY_SETUP_COMMAND,
1352
+ 'trust-root': AUTHORITY_SETUP_COMMAND,
1353
+ deploy: 'npm run build',
1354
+ 'github-app': AUTHORITY_SETUP_COMMAND,
1355
+ 'claude-token': AUTHORITY_SETUP_COMMAND,
1356
+ 'canary-repo': AUTHORITY_SETUP_COMMAND,
1357
+ rulesets: AUTHORITY_SETUP_COMMAND,
1358
+ 'old-activation-state': AUTHORITY_SETUP_COMMAND,
1359
+ 'provenance-key': AUTHORITY_SETUP_COMMAND,
1360
+ 'standing-grant': 'ashlr authority grant',
1361
+ 'autonomy-switch': 'ashlr authority switch autonomous',
1362
+ 'daemon-service': RESIDENT_START_COMMAND,
1363
+ 'resident-runtime': RESIDENT_START_COMMAND,
1364
+ });
1365
+ // Every exit before the grant is known to be active (the first unmet live
1366
+ // prerequisite, or a dry run past one) reports the resident step with this:
1367
+ // a resident service is admitted only under an active standing grant
1368
+ // (docs/RESIDENT-RUNTIME.md), so without one it is genuinely blocked.
1369
+ const RESIDENT_NEEDS_GRANT = 'blocked until a grant is active (`ashlr authority grant`, Touch ID); then `ashlr authority resident start` in your own terminal installs the resident daemon under it';
849
1370
  function setupStepId(step) {
850
1371
  return step.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
851
1372
  }
@@ -854,7 +1375,17 @@ export function authoritySetupReport(report, dryRun) {
854
1375
  const count = (status) => report.filter((r) => r.status === status).length;
855
1376
  const steps = report.map((r) => {
856
1377
  const id = setupStepId(r.step);
857
- return { id, step: r.step, status: r.status, detail: r.detail, needs: SETUP_STEP_NEEDS[id] ?? [] };
1378
+ // A step in place needs nothing; one that is not names what moves it on.
1379
+ const open = r.status !== 'done' && r.status !== 'already';
1380
+ return {
1381
+ id,
1382
+ step: r.step,
1383
+ status: r.status,
1384
+ detail: r.detail,
1385
+ needs: SETUP_STEP_NEEDS[id] ?? [],
1386
+ command: open ? (r.command !== undefined ? r.command : SETUP_STEP_COMMAND[id] ?? null) : null,
1387
+ link: open ? r.link ?? null : null,
1388
+ };
858
1389
  });
859
1390
  const open = steps.find((s) => s.status !== 'done' && s.status !== 'already');
860
1391
  return {
@@ -868,7 +1399,6 @@ export function authoritySetupReport(report, dryRun) {
868
1399
  }
869
1400
  async function cmdSetup(parsed, deps) {
870
1401
  const dryRun = parsed.flags.has('--dry-run');
871
- const yes = parsed.flags.has('--yes');
872
1402
  const json = parsed.flags.has('--json');
873
1403
  if (json && !dryRun) {
874
1404
  // A real run asks questions and prints what gh / signing did; only the
@@ -876,24 +1406,119 @@ async function cmdSetup(parsed, deps) {
876
1406
  deps.err('setup --json needs --dry-run');
877
1407
  return 2;
878
1408
  }
1409
+ const report = await runSetup(parsed, deps, { dryRun, yes: parsed.flags.has('--yes'), print: !json });
1410
+ return finish(report, deps, dryRun, json);
1411
+ }
1412
+ // ---------------------------------------------------------------------------
1413
+ // The read-only checklist (Verse: GET /api/verse/authority/setup)
1414
+ // ---------------------------------------------------------------------------
1415
+ /** Per `gh api` GET in the read-only probe. */
1416
+ const READ_ONLY_GH_TIMEOUT_MS = 10_000;
1417
+ /** All `gh api` reads of one read-only probe together. */
1418
+ const READ_ONLY_PROBE_BUDGET_MS = 30_000;
1419
+ const GH_API_PATH_RE = /^[A-Za-z0-9][A-Za-z0-9._~/?=&%-]{0,400}$/u;
1420
+ /**
1421
+ * The read-only probe's runner: `gh api <path>` — a GET, since no field,
1422
+ * method or input flag is ever passed — run asynchronously (the Verse server
1423
+ * must not block its event loop) and killed after READ_ONLY_GH_TIMEOUT_MS.
1424
+ * Anything else (git, `gh pr`, `gh repo create`, `--method`) is refused
1425
+ * without running, so a dry-run path that ever grew a write could not reach it.
1426
+ */
1427
+ /** PURE: `gh api <path>` with nothing else — the only call the read-only probe runs. */
1428
+ export function isReadOnlyGhApiCall(bin, args) {
1429
+ return bin === 'gh' && args.length === 2 && args[0] === 'api' && GH_API_PATH_RE.test(args[1] ?? '');
1430
+ }
1431
+ export function readOnlyGhRun(bin, args) {
1432
+ if (!isReadOnlyGhApiCall(bin, args)) {
1433
+ return Promise.resolve({ status: 1, stdout: '', stderr: 'refused: the read-only setup probe only runs `gh api <path>` reads' });
1434
+ }
1435
+ return new Promise((done) => {
1436
+ execFile('gh', ['api', args[1]], {
1437
+ encoding: 'utf8',
1438
+ timeout: READ_ONLY_GH_TIMEOUT_MS,
1439
+ maxBuffer: 8 * 1024 * 1024,
1440
+ env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' },
1441
+ }, (error, stdout, stderr) => {
1442
+ const code = error?.code;
1443
+ done({
1444
+ status: error ? (typeof code === 'number' ? code : 1) : 0,
1445
+ stdout: stdout ?? '',
1446
+ stderr: stderr || (error ? error.message : ''),
1447
+ });
1448
+ });
1449
+ });
1450
+ }
1451
+ function refusedInProbe(what) {
1452
+ throw new Error(`${what} is not available to the read-only setup probe`);
1453
+ }
1454
+ /**
1455
+ * `ashlr authority setup --dry-run --json` as a function: the same planning
1456
+ * path (cmdSetup's steps with --dry-run), with dependencies that cannot ask,
1457
+ * prompt, open a browser, reach the network directly or run anything but a
1458
+ * bounded `gh api` GET. Custody is only asked read-only questions (status,
1459
+ * host binding, public key, and whether the App can see a repo).
1460
+ */
1461
+ export async function planAuthoritySetup(injected) {
1462
+ // The whole probe gets READ_ONLY_PROBE_BUDGET_MS of gh time: past it, a read
1463
+ // is answered "not read" at once (the step then says what it could not
1464
+ // check) instead of queueing more ten-second waits behind a slow network.
1465
+ const deadline = Date.now() + READ_ONLY_PROBE_BUDGET_MS;
1466
+ const budgeted = (bin, args) => (Date.now() > deadline
1467
+ ? Promise.resolve({ status: 1, stdout: '', stderr: 'not read: the setup probe ran out of time' })
1468
+ : readOnlyGhRun(bin, args));
1469
+ const deps = {
1470
+ out: () => undefined,
1471
+ err: () => undefined,
1472
+ confirm: async () => false,
1473
+ readSecret: async () => refusedInProbe('a secret prompt'),
1474
+ run: budgeted,
1475
+ openBrowser: () => refusedInProbe('a browser'),
1476
+ fetch: (async () => refusedInProbe('the network')),
1477
+ custody: injected?.custody ?? await import('../core/authority/custody-client.js'),
1478
+ daemonService: () => realDaemonService(true),
1479
+ // The resident step only reads (admission verdict, service + plist
1480
+ // observation); the effects are refused here, never reached by setup.
1481
+ resident: {
1482
+ admission: realResidentDeps.admission,
1483
+ observe: realResidentDeps.observe,
1484
+ operatorRefusal: async () => 'the read-only setup probe is not the operator at a terminal',
1485
+ start: async () => refusedInProbe('resident start'),
1486
+ stop: async () => refusedInProbe('resident stop'),
1487
+ },
1488
+ ...(injected ?? {}),
1489
+ };
1490
+ const parsed = { positional: ['setup'], flags: new Set(['--dry-run', '--json']), values: new Map() };
1491
+ return authoritySetupReport(await runSetup(parsed, deps, { dryRun: true, yes: false, print: false }), true);
1492
+ }
1493
+ /**
1494
+ * Every step of setup, in order. RERUN-SAFE: Mason runs it several times, so
1495
+ * each step first checks whether it is already in place and says so
1496
+ * ('already') instead of doing it again — the provenance key is rotated once,
1497
+ * a ruleset GitHub already holds is not PUT again, the App counts only when it
1498
+ * is installed, an open trust-root PR is found rather than reopened. A step
1499
+ * that throws is recorded as failed and the run still ends with its summary.
1500
+ */
1501
+ async function runSetup(parsed, deps, opts) {
1502
+ const { dryRun, yes } = opts;
879
1503
  const report = [];
880
- const note = (step, status, detail) => {
881
- report.push({ step, status, detail });
882
- if (json)
1504
+ const note = (step, status, detail, extra = {}) => {
1505
+ report.push({ step, status, detail, ...extra });
1506
+ if (!opts.print)
883
1507
  return;
884
1508
  // 'skipped' gets its own glyph: a ✓ on a step a dry run did not perform read as done.
885
1509
  const glyph = status === 'failed' ? '✗' : status === 'blocked' ? '×' : status === 'waiting-on-you' ? '…' : status === 'skipped' ? '·' : '✓';
886
1510
  deps.out(`${glyph} ${step}: ${detail}`);
887
1511
  };
888
1512
  const done = () => {
889
- // Every exit, including the first unmet live prerequisite, must report
890
- // this independent source/release block exactly once.
1513
+ // Every exit, including the first unmet live prerequisite, reports the
1514
+ // resident step exactly once — it is never silently complete.
891
1515
  if (!report.some((row) => row.step === 'resident runtime')) {
892
- note('resident runtime', 'blocked', RESIDENT_RUNTIME_BLOCK);
1516
+ note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
893
1517
  }
894
- return finish(report, deps, dryRun, json);
1518
+ return report;
895
1519
  };
896
1520
  const ask = async (question) => !dryRun && (yes || deps.confirm(question));
1521
+ const failure = (error) => (error?.message ?? String(error)).replace(homedir(), '~').slice(0, 300);
897
1522
  // --dry-run PLANS: the CHANGELOG and docs/AUTHORITY.md promise it "prints
898
1523
  // every step first" (3.10 review c20). A real run must stop at an unmet
899
1524
  // prerequisite; a dry run notes it and keeps describing the remaining
@@ -907,256 +1532,561 @@ async function cmdSetup(parsed, deps) {
907
1532
  planning = true;
908
1533
  return false;
909
1534
  };
910
- // 1. Custody helper (installing it needs sudo — Mason runs that himself).
911
- let custody = null;
1535
+ // Which step is running: an exception nobody anticipated becomes its
1536
+ // failure (with the summary still printed), never a bare abort.
1537
+ let current = 'custody helper';
912
1538
  try {
913
- custody = await deps.custody.custodyStatus();
914
- }
915
- catch (error) {
916
- note('custody helper', 'failed', `status unavailable (${error.message})`);
917
- }
918
- if (!custody || !custody.installed) {
919
- note('custody helper', 'waiting-on-you', 'run `sudo scripts/install-custody.sh` from the ashlr-hub checkout, then run setup again');
920
- if (stopHere())
921
- return done();
922
- }
923
- else {
924
- note('custody helper', 'already', `installed${custody.version ? ` (${custody.version})` : ''}`);
925
- }
926
- // 1b. The helper and this CLI must agree on which Mac this is: the helper
927
- // derives StandingGrantV1.hostBinding as sha256(uppercase IOPlatformUUID)
928
- // and so does surface.ts. A disagreement would make every grant fail
929
- // "signed for a different Mac" only AFTER Mason's Touch ID — catch it first.
930
- if (planning) {
931
- note('host binding', 'skipped', 'would check that ashlr-custody and this release agree on which Mac this is');
932
- }
933
- else {
1539
+ // 1. Custody helper (installing it needs sudo — Mason runs that himself).
1540
+ let custody = null;
934
1541
  try {
935
- const { currentHostBinding } = await import('../core/authority/surface.js');
936
- const ours = currentHostBinding();
937
- const helpers = await deps.custody.custodyHostBinding();
938
- if (ours === null)
939
- note('host binding', 'failed', "this Mac's IOPlatformUUID could not be read, so no grant can be bound to it");
940
- else if (ours !== helpers)
941
- note('host binding', 'failed', 'ashlr-custody and this release disagree on the host binding — reinstall the helper from this checkout');
942
- else
943
- note('host binding', 'already', `this Mac is ${ours.slice(0, 12)}…`);
1542
+ custody = await deps.custody.custodyStatus();
944
1543
  }
945
1544
  catch (error) {
946
- note('host binding', 'failed', `could not compare with ashlr-custody (${error.message})`);
947
- }
948
- }
949
- if (report.some((r) => r.step === 'host binding' && r.status === 'failed') && stopHere())
950
- return done();
951
- // 2. Secure Enclave key (Touch ID). The helper is not on PATH, so every
952
- // message names it by its installed path.
953
- let keyId = custody?.keyId ?? null;
954
- let publicKeyPem = null;
955
- if (planning || !custody) {
956
- note('signing key', 'skipped', 'would create the Secure Enclave signing key (Touch ID) unless it already exists');
957
- }
958
- else if (!custody.keyInitialized) {
959
- if (!(await ask('Create the Secure Enclave signing key now (Touch ID)?'))) {
960
- note('signing key', dryRun ? 'skipped' : 'waiting-on-you', dryRun
961
- ? `would create it (Touch ID) — or run \`${CUSTODY_HELPER_PATH} init\``
962
- : `create it with \`${CUSTODY_HELPER_PATH} init\` or rerun setup`);
1545
+ note('custody helper', 'failed', `status unavailable (${failure(error)})`);
1546
+ }
1547
+ if (!custody || !custody.installed) {
1548
+ note('custody helper', 'waiting-on-you', 'run `sudo scripts/install-custody.sh` from the ashlr-hub checkout, then run setup again');
963
1549
  if (stopHere())
964
1550
  return done();
965
1551
  }
966
1552
  else {
967
- const info = await deps.custody.custodyInit();
968
- keyId = info.keyId;
969
- publicKeyPem = info.publicKeyPem;
970
- note('signing key', 'done', `created ${info.keyId}`);
1553
+ note('custody helper', 'already', `installed${custody.version ? ` (${custody.version})` : ''}`);
971
1554
  }
972
- }
973
- else {
974
- note('signing key', 'already', `key ${keyId ?? '(id unknown)'}`);
975
- }
976
- // 3. Trust root compiled in (Mason's own PR).
977
- const { STANDING_GRANT_TRUST_ROOTS } = await import('../core/authority/trust-roots.js');
978
- // A key made on an earlier run whose id status did not report: read its
979
- // public half back (`ashlr-custody pubkey`, no Touch ID) instead of asking
980
- // Mason to copy it by hand.
981
- const isTrusted = (id) => id !== null && STANDING_GRANT_TRUST_ROOTS.some((root) => root.keyId === id);
982
- // No kickstart here: a daemon that is disabled or not loaded (the usual
983
- // state before the first grant) has nothing to kickstart, and the last step
984
- // (daemon service) says how to start it once the grant is in force.
985
- const DEPLOY_DETAIL = 'after you merge that PR: `npm run build`, install the release as you normally do (docs/RELEASING-LOCALLY.md), then rerun setup (restart a running daemon with `launchctl kickstart -k gui/$(id -u)/ai.ashlr.daemon`)';
986
- if (planning) {
987
- note('trust root', 'skipped', 'would check that your signing key is compiled into this release, and if not open a PR adding it to trust-roots.ts for you to review and merge');
988
- note('deploy', 'skipped', `would wait for you: ${DEPLOY_DETAIL}`);
989
- }
990
- else {
991
- let trustRootFailed = false;
992
- if (!isTrusted(keyId) && !publicKeyPem) {
1555
+ // 1b. The helper and this CLI must agree on which Mac this is: the helper
1556
+ // derives StandingGrantV1.hostBinding as sha256(uppercase IOPlatformUUID)
1557
+ // and so does surface.ts. A disagreement would make every grant fail
1558
+ // "signed for a different Mac" only AFTER Mason's Touch ID — catch it first.
1559
+ current = 'host binding';
1560
+ if (planning) {
1561
+ note('host binding', 'skipped', 'would check that ashlr-custody and this release agree on which Mac this is');
1562
+ }
1563
+ else {
993
1564
  try {
994
- const info = await deps.custody.custodyPublicKey();
995
- keyId = info.keyId;
996
- publicKeyPem = info.publicKeyPem;
1565
+ const { currentHostBinding } = await import('../core/authority/surface.js');
1566
+ const ours = currentHostBinding();
1567
+ const helpers = await deps.custody.custodyHostBinding();
1568
+ if (ours === null)
1569
+ note('host binding', 'failed', "this Mac's IOPlatformUUID could not be read, so no grant can be bound to it");
1570
+ else if (ours !== helpers)
1571
+ note('host binding', 'failed', 'ashlr-custody and this release disagree on the host binding — reinstall the helper from this checkout');
1572
+ else
1573
+ note('host binding', 'already', `this Mac is ${ours.slice(0, 12)}…`);
997
1574
  }
998
1575
  catch (error) {
999
- note('trust root', 'failed', `could not read the custody public key (${error.message})`);
1000
- trustRootFailed = true;
1576
+ note('host binding', 'failed', `could not compare with ashlr-custody (${failure(error)})`);
1001
1577
  }
1002
1578
  }
1003
- if (trustRootFailed) {
1004
- if (stopHere())
1005
- return done();
1579
+ if (report.some((r) => r.step === 'host binding' && r.status === 'failed') && stopHere())
1580
+ return done();
1581
+ // 2. Secure Enclave key (Touch ID). The helper is not on PATH, so every
1582
+ // message names it by its installed path.
1583
+ current = 'signing key';
1584
+ let keyId = custody?.keyId ?? null;
1585
+ let publicKeyPem = null;
1586
+ if (planning || !custody) {
1587
+ note('signing key', 'skipped', 'would create the Secure Enclave signing key (Touch ID) unless it already exists');
1588
+ }
1589
+ else if (!custody.keyInitialized) {
1590
+ if (!(await ask('Create the Secure Enclave signing key now (Touch ID)?'))) {
1591
+ note('signing key', dryRun ? 'skipped' : 'waiting-on-you', dryRun
1592
+ ? `would create it (Touch ID) — or run \`${CUSTODY_HELPER_PATH} init\``
1593
+ : `create it with \`${CUSTODY_HELPER_PATH} init\` or rerun setup`);
1594
+ if (stopHere())
1595
+ return done();
1596
+ }
1597
+ else {
1598
+ try {
1599
+ const info = await deps.custody.custodyInit();
1600
+ keyId = info.keyId;
1601
+ publicKeyPem = info.publicKeyPem;
1602
+ note('signing key', 'done', `created ${info.keyId}`);
1603
+ }
1604
+ catch (error) {
1605
+ note('signing key', 'failed', `not created (${failure(error)}) — rerun setup, or run \`${CUSTODY_HELPER_PATH} init\``);
1606
+ if (stopHere())
1607
+ return done();
1608
+ }
1609
+ }
1610
+ }
1611
+ else {
1612
+ note('signing key', 'already', `key ${keyId ?? '(id unknown)'}`);
1006
1613
  }
1007
- else if (isTrusted(keyId)) {
1008
- note('trust root', 'already', `${keyId} is compiled into this release`);
1009
- note('deploy', 'already', 'this release carries your trust root');
1614
+ // 3. Trust root compiled in (Mason's own PR).
1615
+ current = 'trust root';
1616
+ const { STANDING_GRANT_TRUST_ROOTS } = await import('../core/authority/trust-roots.js');
1617
+ // A key made on an earlier run whose id status did not report: read its
1618
+ // public half back (`ashlr-custody pubkey`, no Touch ID) instead of asking
1619
+ // Mason to copy it by hand.
1620
+ const isTrusted = (id) => id !== null && STANDING_GRANT_TRUST_ROOTS.some((root) => root.keyId === id);
1621
+ // No kickstart here: a daemon that is disabled or not loaded (the usual
1622
+ // state before the first grant) has nothing to kickstart, and the last step
1623
+ // (daemon service) says how to start it once the grant is in force.
1624
+ const DEPLOY_DETAIL = 'after you merge that PR: `npm run build`, install the release as you normally do (docs/RELEASING-LOCALLY.md), then rerun setup (restart a running daemon with `launchctl kickstart -k gui/$(id -u)/ai.ashlr.daemon`)';
1625
+ if (planning) {
1626
+ note('trust root', 'skipped', 'would check that your signing key is compiled into this release, and if not open a PR adding it to trust-roots.ts for you to review and merge');
1627
+ note('deploy', 'skipped', `would wait for you: ${DEPLOY_DETAIL}`);
1010
1628
  }
1011
1629
  else {
1012
- const root = { keyId: keyId, alg: 'ES256', publicKeyPem: publicKeyPem };
1013
- const invalid = validateCustodyRoot(root, deps.custody.keyIdForPublicKeyPem);
1014
- if (invalid) {
1015
- note('trust root', 'failed', invalid);
1630
+ let trustRootFailed = false;
1631
+ if (!isTrusted(keyId) && !publicKeyPem) {
1632
+ try {
1633
+ const info = await deps.custody.custodyPublicKey();
1634
+ keyId = info.keyId;
1635
+ publicKeyPem = info.publicKeyPem;
1636
+ }
1637
+ catch (error) {
1638
+ note('trust root', 'failed', `could not read the custody public key (${failure(error)})`);
1639
+ trustRootFailed = true;
1640
+ }
1641
+ }
1642
+ if (trustRootFailed) {
1643
+ if (stopHere())
1644
+ return done();
1645
+ }
1646
+ else if (isTrusted(keyId)) {
1647
+ note('trust root', 'already', `${keyId} is compiled into this release`);
1648
+ note('deploy', 'already', 'this release carries your trust root');
1016
1649
  }
1017
1650
  else {
1018
- let pr;
1651
+ const root = { keyId: keyId, alg: 'ES256', publicKeyPem: publicKeyPem };
1652
+ const invalid = validateCustodyRoot(root, deps.custody.keyIdForPublicKeyPem);
1653
+ if (invalid) {
1654
+ note('trust root', 'failed', invalid);
1655
+ }
1656
+ else {
1657
+ let pr;
1658
+ try {
1659
+ // A dry run looks too, through `gh api` reads only (the
1660
+ // Verse checklist is this path): an open PR, a merged key or a
1661
+ // pushed branch is reported instead of "add this root yourself".
1662
+ pr = (dryRun ? await probeTrustRootReadOnly(deps, root) : null) ?? await openTrustRootPr(parsed, deps, root, ask, !dryRun);
1663
+ }
1664
+ catch (error) {
1665
+ pr = { status: 'failed', detail: failure(error) };
1666
+ }
1667
+ note('trust root', pr.status, pr.detail, {
1668
+ ...(pr.link ? { link: pr.link } : {}),
1669
+ ...(pr.command !== undefined ? { command: pr.command } : {}),
1670
+ });
1671
+ note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
1672
+ }
1673
+ if (stopHere())
1674
+ return done();
1675
+ }
1676
+ }
1677
+ // The canary's existence decides whether the App and ruleset checks
1678
+ // include it yet (setup creates it at step 6). One read-only GET.
1679
+ const canaryRepo = 'ashlrai/fleet-canary';
1680
+ let canaryExists = planning ? false : (await deps.run('gh', ['api', `repos/${canaryRepo}`])).status === 0;
1681
+ // 4. GitHub App: its key in custody is half of it — it also has to be
1682
+ // INSTALLED on the repos the fleet works in, or every mint fails.
1683
+ current = 'GitHub App';
1684
+ if (!custody?.githubApp) {
1685
+ if (await ask(`Create the ${FLEET_APP_NAME} GitHub App now (one browser page)?`)) {
1019
1686
  try {
1020
- pr = await openTrustRootPr(parsed, deps, root, ask, !dryRun);
1687
+ const app = await runGithubAppFlow(deps, { org: one(parsed, '--org') ?? 'ashlrai' });
1688
+ // A new App is installed nowhere yet.
1689
+ note('GitHub App', 'waiting-on-you', `created ${app.slug} (its key is in custody); now install it on the enrolled repos: ${app.installUrl} — then rerun setup`, { link: app.installUrl });
1021
1690
  }
1022
1691
  catch (error) {
1023
- pr = { status: 'failed', detail: error.message };
1692
+ note('GitHub App', 'failed', `not created (${failure(error)}) — rerun setup, or run \`ashlr authority github-app\``);
1024
1693
  }
1025
- note('trust root', pr.status, pr.detail);
1026
- note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
1027
1694
  }
1028
- if (stopHere())
1029
- return done();
1695
+ else
1696
+ note('GitHub App', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority github-app`');
1697
+ }
1698
+ else if (planning) {
1699
+ note('GitHub App', 'skipped', `the ${FLEET_APP_NAME} key is in custody; would check the App is installed on the enrolled repos`);
1700
+ }
1701
+ else {
1702
+ const targets = (await setupTargetRepos()).map((repo) => repo.nameWithOwner)
1703
+ .filter((repo) => canaryExists || repo.toLowerCase() !== canaryRepo);
1704
+ const probe = await probeAppInstallation(deps, targets);
1705
+ const later = canaryExists ? '' : ` (${canaryRepo} needs it too once it exists)`;
1706
+ if (probe.missing.length > 0) {
1707
+ note('GitHub App', 'waiting-on-you', `the ${FLEET_APP_NAME} key is in custody, but the App is not installed on ${shortList(probe.missing)} — install it: ${FLEET_APP_INSTALL_URL}, then rerun setup${later}`, { link: FLEET_APP_INSTALL_URL });
1708
+ }
1709
+ else if (probe.unknown.length > 0) {
1710
+ note('GitHub App', 'waiting-on-you', `the ${FLEET_APP_NAME} key is in custody, but its installation on ${shortList(probe.unknown.map((u) => u.repo))} could not be confirmed (${probe.unknown[0].reason}) — check ${FLEET_APP_INSTALL_URL}, then rerun setup`, { link: FLEET_APP_INSTALL_URL });
1711
+ }
1712
+ else {
1713
+ const where = probe.installed.length === 0 ? 'no enrolled repo needs it yet' : probe.installed.length === 1 ? `installed on ${probe.installed[0]}` : `installed on all ${probe.installed.length} enrolled repos`;
1714
+ // 3.13: it must also be able to post ashlr/verify (checks: write); an
1715
+ // App created before 3.13 has checks: read and must be raised by hand.
1716
+ const app = await readFleetApp(deps);
1717
+ if (app && app.checks !== 'write') {
1718
+ const urls = fleetAppPermissionUrls(app);
1719
+ note('GitHub App', 'waiting-on-you', `the ${FLEET_APP_NAME} key is in custody and the App is ${where}, but it cannot post the ${ASHLR_VERIFY_CHECK} check (Checks: ${app.checks ?? 'no access'}): `
1720
+ + `set "Checks: Read and write" at ${urls.permissions}, accept the new permission on each installation at ${urls.installations}, then rerun setup`, { link: urls.permissions });
1721
+ }
1722
+ else {
1723
+ note('GitHub App', 'already', app
1724
+ ? `the ${FLEET_APP_NAME} key is in custody; ${where}${later}; the App can post ${ASHLR_VERIFY_CHECK} (checks: write)`
1725
+ : `the ${FLEET_APP_NAME} key is in custody; ${where}${later} (its checks permission was not read; it needs "Checks: Read and write" for ${ASHLR_VERIFY_CHECK})`);
1726
+ }
1727
+ }
1728
+ }
1729
+ // 5. Claude token for restricted judge / Leader calls.
1730
+ current = 'Claude token';
1731
+ if (custody?.claudeToken)
1732
+ note('Claude token', 'already', 'stored in custody');
1733
+ else if (await ask('Store a Claude token now (run `claude setup-token` in another terminal and paste it)?')) {
1734
+ try {
1735
+ const token = (await deps.readSecret('Paste the token (hidden): ')).trim();
1736
+ if (!token)
1737
+ note('Claude token', 'failed', 'nothing was pasted');
1738
+ else {
1739
+ await deps.custody.storeClaudeToken(token);
1740
+ note('Claude token', 'done', 'stored in custody (never written to disk here)');
1741
+ }
1742
+ }
1743
+ catch (error) {
1744
+ note('Claude token', 'failed', `not stored (${failure(error)}) — run \`claude setup-token\`, then rerun setup`);
1745
+ }
1746
+ }
1747
+ else
1748
+ note('Claude token', dryRun ? 'skipped' : 'waiting-on-you', 'run `claude setup-token`, then rerun setup');
1749
+ // 6. Canary repo (the App cannot write workflows, so this uses your own gh
1750
+ // auth). Before the rulesets: the grant's server-enforced repos include the
1751
+ // canary, and a ruleset cannot be applied to a repo that does not exist yet.
1752
+ current = 'canary repo';
1753
+ if (canaryExists)
1754
+ note('canary repo', 'already', `${canaryRepo} exists`);
1755
+ else if (await ask(`Create the public repo ${canaryRepo} with its CI workflow (your gh auth)?`)) {
1756
+ const created = await deps.run('gh', ['repo', 'create', canaryRepo, '--public', '--add-readme', '--description', 'ashlr fleet canary (revert drills)']);
1757
+ const workflow = created.status === 0
1758
+ ? await deps.run('gh', ['api', '--method', 'PUT', `repos/${canaryRepo}/contents/.github/workflows/ci.yml`, '--input', '-'], {
1759
+ input: JSON.stringify({ message: 'ci: canary workflow', content: Buffer.from(CANARY_WORKFLOW, 'utf8').toString('base64') }),
1760
+ })
1761
+ : null;
1762
+ canaryExists = created.status === 0;
1763
+ note('canary repo', created.status === 0 && workflow?.status === 0 ? 'done' : 'failed', created.status === 0 ? (workflow?.status === 0 ? 'created with its CI workflow' : 'created, but the workflow push failed') : created.stderr.trim().slice(0, 200));
1764
+ }
1765
+ else
1766
+ note('canary repo', dryRun ? 'skipped' : 'waiting-on-you', `create ${canaryRepo} with a CI workflow`);
1767
+ // 7. Rulesets — only where GitHub does not already hold exactly this one.
1768
+ current = 'rulesets';
1769
+ if (planning) {
1770
+ note('rulesets', 'skipped', 'would check the fleet ruleset on the server-enforced repos and apply it where it is missing or different');
1771
+ }
1772
+ else {
1773
+ const targets = (await setupTargetRepos()).filter((repo) => canaryExists || repo.nameWithOwner.toLowerCase() !== canaryRepo);
1774
+ const plans = await protectPlans(parsed, deps, targets);
1775
+ const stale = plans.filter((plan) => !plan.skipped && !plan.upToDate);
1776
+ const inPlace = plans.filter((plan) => !plan.skipped && plan.upToDate).length;
1777
+ const unreadable = plans.filter((plan) => plan.skipped?.startsWith('could not read')).map((plan) => plan.repo);
1778
+ const unavailable = plans.filter((plan) => plan.unavailable === true);
1779
+ const mismatches = unavailable.map((plan) => plan.mismatch).filter((m) => typeof m === 'string');
1780
+ const byHand = plans.filter((plan) => plan.skipped && !plan.unavailable && !plan.skipped.startsWith('could not read')).length;
1781
+ const unread = unreadable.length > 0 ? `; could not read ${shortList(unreadable)} from GitHub` : '';
1782
+ // A plan limit is not a read failure: say which repos only local enforcement can protect.
1783
+ const noRulesets = unavailable.length > 0
1784
+ ? `; rulesets unavailable on this plan for private ${shortList(unavailable.map((plan) => plan.repo))} — ${mismatches.length > 0 ? mismatches.join('; ') : `using local enforcement with ${ASHLR_VERIFY_CHECK}`}`
1785
+ : '';
1786
+ const left = `${byHand > 0 ? `; ${byHand} left to you (local enforcement or no CI)` : ''}${noRulesets}`;
1787
+ const inPlaceText = inPlace > 0 ? `the fleet ruleset is in place on ${inPlace === 1 ? '1 repo' : `${inPlace} repos`}` : 'no repo has the fleet ruleset';
1788
+ if (plans.length === 0) {
1789
+ // Nothing to look at yet — never "in place on 0 repos".
1790
+ if (canaryExists)
1791
+ note('rulesets', 'already', 'no server-enforced repo needs one');
1792
+ else
1793
+ note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${canaryRepo} gets the fleet ruleset once it exists`);
1794
+ }
1795
+ else if (stale.length > 0) {
1796
+ const where = shortList(stale.map((plan) => plan.repo));
1797
+ if (await ask(`Apply the fleet ruleset to ${stale.length} repo(s) where it is missing or different (${where})?`)) {
1798
+ const failures = await applyProtectPlans(stale, deps);
1799
+ note('rulesets', failures > 0 ? 'failed' : unreadable.length > 0 || mismatches.length > 0 ? 'waiting-on-you' : 'done', failures === 0
1800
+ ? `applied to ${where}${inPlace > 0 ? `; ${inPlace} already in place` : ''}${unread}${left}`
1801
+ : `${failures} of ${stale.length} failed (see above)${unread}${noRulesets}`);
1802
+ }
1803
+ else {
1804
+ note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${stale.length} repo(s) need the fleet ruleset (${where})${unread}${noRulesets} — run \`ashlr authority protect --print\`, then \`--apply\``);
1805
+ }
1806
+ }
1807
+ else if (unreadable.length > 0) {
1808
+ note('rulesets', 'waiting-on-you', `could not read ${shortList(unreadable)} from GitHub — check \`gh auth status\`, then rerun setup${inPlace > 0 ? ` (${inPlace} already in place)` : ''}${noRulesets}`);
1809
+ }
1810
+ else if (mismatches.length > 0) {
1811
+ // The signed grant is never rewritten here: only a re-approval (Touch ID) switches the repo.
1812
+ note('rulesets', 'waiting-on-you', `${inPlaceText}${left}`, { command: 'ashlr authority re-approve' });
1813
+ }
1814
+ else {
1815
+ note('rulesets', 'already', `${inPlaceText}${left}`);
1816
+ }
1817
+ }
1818
+ // 8. Retire the burned activation directory.
1819
+ current = 'old activation state';
1820
+ const activation = join(homedir(), '.ashlr', 'activation');
1821
+ if (!existsSync(activation))
1822
+ note('old activation state', 'already', 'nothing to retire');
1823
+ else if (await ask('Move ~/.ashlr/activation (the burned key) out of ~/.ashlr so no code path can reach it?')) {
1824
+ try {
1825
+ const target = join(homedir(), `ashlr-activation-retired-${new Date().toISOString().replace(/[:.]/g, '-')}`);
1826
+ renameSync(activation, target);
1827
+ note('old activation state', 'done', `moved to ${target.replace(homedir(), '~')} — move it offline, then delete it`);
1828
+ }
1829
+ catch (error) {
1830
+ note('old activation state', 'failed', `not moved (${failure(error)})`);
1831
+ }
1832
+ }
1833
+ else
1834
+ note('old activation state', dryRun ? 'skipped' : 'waiting-on-you', 'archive ~/.ashlr/activation offline, then delete it');
1835
+ // 9. Provenance key — rotated ONCE. The burned key is whichever one no
1836
+ // rotation recorded; a key a rotation made (and recorded) is left alone,
1837
+ // because rotating again would invalidate every pending proposal for nothing.
1838
+ current = 'provenance key';
1839
+ const provenance = await provenanceKeyState();
1840
+ if (provenance.state === 'rotated') {
1841
+ note('provenance key', 'already', `rotated ${provenance.rotatedAt.slice(0, 10)}; not rotated again, so pending proposals keep verifying`);
1842
+ }
1843
+ else if (await ask(provenance.state === 'missing'
1844
+ ? 'Create the provenance HMAC key now?'
1845
+ : 'Rotate the provenance HMAC key (agents could read the old one)?')) {
1846
+ try {
1847
+ const rotated = await rotateProvenanceKey();
1848
+ note('provenance key', 'done', rotated.retiredAs ? 'rotated; the old key was moved aside' : 'created');
1849
+ }
1850
+ catch (error) {
1851
+ note('provenance key', 'failed', `not rotated (${failure(error)})`);
1852
+ }
1030
1853
  }
1031
- }
1032
- // 4. GitHub App.
1033
- if (custody?.githubApp)
1034
- note('GitHub App', 'already', 'the ashlr-fleet key is in custody');
1035
- else if (await ask(`Create the ${FLEET_APP_NAME} GitHub App now (one browser page)?`)) {
1036
- const app = await runGithubAppFlow(deps, { org: one(parsed, '--org') ?? 'ashlrai' });
1037
- note('GitHub App', 'done', `created ${app.slug}; install it on the repos: ${app.installUrl}`);
1038
- }
1039
- else
1040
- note('GitHub App', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority github-app`');
1041
- // 5. Claude token for restricted judge / Leader calls.
1042
- if (custody?.claudeToken)
1043
- note('Claude token', 'already', 'stored in custody');
1044
- else if (await ask('Store a Claude token now (run `claude setup-token` in another terminal and paste it)?')) {
1045
- const token = (await deps.readSecret('Paste the token (hidden): ')).trim();
1046
- if (!token)
1047
- note('Claude token', 'failed', 'nothing was pasted');
1048
1854
  else {
1049
- await deps.custody.storeClaudeToken(token);
1050
- note('Claude token', 'done', 'stored in custody (never written to disk here)');
1855
+ const why = provenance.state === 'unhealthy' ? `the current key is unusable (${provenance.reason}); ` : '';
1856
+ note('provenance key', dryRun ? 'skipped' : 'waiting-on-you', `${why}run \`ashlr authority rotate-provenance\``);
1857
+ }
1858
+ // 10. First grant (Touch ID), 11. the switch, 12. the daemon service,
1859
+ // 13. resident admission (docs/RESIDENT-RUNTIME.md). Setup itself never
1860
+ // touches launchd: it names the one command that does.
1861
+ const DAEMON_DETAIL = 'would check that the ai.ashlr.daemon service is loaded and running (each tick re-verifies the grant)';
1862
+ if (planning) {
1863
+ note('standing grant', 'skipped', 'would sign the first standing grant (Touch ID)');
1864
+ note('autonomy switch', 'skipped', 'would offer to set the autonomy switch to Autonomous (the ladder starts in shadow)');
1865
+ note('daemon service', 'skipped', DAEMON_DETAIL);
1866
+ return done();
1867
+ }
1868
+ current = 'standing grant';
1869
+ const { evaluateStandingAuthority, displaySurfaceTarget } = await import('../core/authority/effective-config.js');
1870
+ const evaluation = evaluateStandingAuthority({ mode: 'cached', surface: displaySurfaceTarget() });
1871
+ let grantActive = evaluation.grantState === 'active';
1872
+ if (grantActive) {
1873
+ note('standing grant', 'already', `grant #${evaluation.grant?.grantSeq ?? '?'} is active`);
1874
+ }
1875
+ else if (await ask('Sign the first standing grant now (Touch ID)?')) {
1876
+ try {
1877
+ const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
1878
+ const draft = await buildStandingGrantDraft('auto', Date.now(), { githubGet: ghGet(deps) });
1879
+ for (const line of draft.summary)
1880
+ deps.out(` ${line}`);
1881
+ const signed = await signAndInstall(draft.payload, deps);
1882
+ grantActive = signed.ok;
1883
+ note('standing grant', signed.ok ? 'done' : 'failed', signed.ok ? `grant #${signed.grant.grantSeq} installed, starting at ${signed.grant.rollout.stages[0].id}` : signed.reason);
1884
+ }
1885
+ catch (error) {
1886
+ note('standing grant', 'failed', `no grant was drafted (${failure(error)}) — rerun setup, or run \`ashlr authority grant\``);
1887
+ }
1888
+ }
1889
+ else
1890
+ note('standing grant', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority grant`');
1891
+ // Resumable: a grant signed on an earlier run (or with `authority grant`)
1892
+ // still gets the switch offered — it used to be offered only in the run
1893
+ // that signed. Raising stays capped by the grant (requestAutonomySwitch).
1894
+ current = 'autonomy switch';
1895
+ if (!grantActive) {
1896
+ note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', 'needs an active standing grant first');
1897
+ }
1898
+ else if (evaluation.switch === 'autonomous') {
1899
+ note('autonomy switch', 'already', 'Autonomous');
1900
+ }
1901
+ else if (await ask('Set the autonomy switch to Autonomous (the ladder starts in shadow)?')) {
1902
+ const { requestAutonomySwitch } = await import('../core/authority/effective-config.js');
1903
+ const switched = requestAutonomySwitch('autonomous', 'mason', 'ashlr authority setup');
1904
+ note('autonomy switch', switched.ok ? 'done' : 'failed', switched.ok ? 'Autonomous' : switched.reason);
1905
+ }
1906
+ else
1907
+ note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', `it is ${evaluation.switch} — run \`ashlr authority switch autonomous\``);
1908
+ // 12. Read-only service observation. Only `resident start` (Mason, in his
1909
+ // own terminal, under the active grant) installs or restarts the service.
1910
+ current = 'daemon service';
1911
+ let service;
1912
+ try {
1913
+ service = await deps.daemonService();
1914
+ }
1915
+ catch {
1916
+ service = 'unknown';
1917
+ }
1918
+ const START = `\`${RESIDENT_START_COMMAND}\``;
1919
+ const after = grantActive ? `run ${START}` : `after the standing grant, run ${START}`;
1920
+ // Before the grant, the command that moves this step on is the grant.
1921
+ const serviceCommand = grantActive ? {} : { command: 'ashlr authority grant' };
1922
+ if (service === 'running')
1923
+ note('daemon service', 'already', 'ai.ashlr.daemon is running (each tick re-verifies the grant)');
1924
+ else if (service === 'absent')
1925
+ note('daemon service', 'waiting-on-you', `no ai.ashlr.daemon service is installed — ${after} to install it`, serviceCommand);
1926
+ else if (service === 'loaded')
1927
+ note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is loaded but stopped — ${after} to restart it`, serviceCommand);
1928
+ else if (service === 'not-loaded')
1929
+ note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is not loaded — ${after} to load it`, serviceCommand);
1930
+ else
1931
+ note('daemon service', 'waiting-on-you', `ai.ashlr.daemon state is unknown — \`ashlr authority resident status\` shows why; ${after} to repair it`, serviceCommand);
1932
+ // 13. Resident admission: the grant, this release and the service together.
1933
+ current = 'resident runtime';
1934
+ if (!grantActive) {
1935
+ note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
1936
+ }
1937
+ else {
1938
+ const resident = await residentSetupRow(deps, service);
1939
+ note('resident runtime', resident.status, resident.detail, { command: resident.command });
1051
1940
  }
1052
1941
  }
1053
- else
1054
- note('Claude token', dryRun ? 'skipped' : 'waiting-on-you', 'run `claude setup-token`, then rerun setup');
1055
- // 6. Canary repo (the App cannot write workflows, so this uses your own gh
1056
- // auth). Before the rulesets: the grant's server-enforced repos include the
1057
- // canary, and a ruleset cannot be applied to a repo that does not exist yet.
1058
- const canary = planning ? null : deps.run('gh', ['api', 'repos/ashlrai/fleet-canary']);
1059
- if (canary?.status === 0)
1060
- note('canary repo', 'already', 'ashlrai/fleet-canary exists');
1061
- else if (await ask('Create the public repo ashlrai/fleet-canary with its CI workflow (your gh auth)?')) {
1062
- const created = deps.run('gh', ['repo', 'create', 'ashlrai/fleet-canary', '--public', '--add-readme', '--description', 'ashlr fleet canary (revert drills)']);
1063
- const workflow = created.status === 0
1064
- ? deps.run('gh', ['api', '--method', 'PUT', 'repos/ashlrai/fleet-canary/contents/.github/workflows/ci.yml', '--input', '-'], {
1065
- input: JSON.stringify({ message: 'ci: canary workflow', content: Buffer.from(CANARY_WORKFLOW, 'utf8').toString('base64') }),
1066
- })
1067
- : null;
1068
- note('canary repo', created.status === 0 && workflow?.status === 0 ? 'done' : 'failed', created.status === 0 ? (workflow?.status === 0 ? 'created with its CI workflow' : 'created, but the workflow push failed') : created.stderr.trim().slice(0, 200));
1942
+ catch (error) {
1943
+ // Nothing above is expected to throw; if something does, it is this
1944
+ // step's failure and the run still ends with the summary.
1945
+ note(report.some((row) => row.step === current) ? 'setup' : current, 'failed', `stopped: ${failure(error)}`);
1069
1946
  }
1070
- else
1071
- note('canary repo', dryRun ? 'skipped' : 'waiting-on-you', 'create ashlrai/fleet-canary with a CI workflow');
1072
- // 7. Rulesets.
1073
- if (await ask('Apply the fleet rulesets to the server-enforced repos now?')) {
1074
- const code = await cmdProtect({ ...parsed, flags: new Set([...parsed.flags, '--apply', '--yes']) }, deps);
1075
- note('rulesets', code === 0 ? 'done' : 'failed', code === 0 ? 'applied (see above)' : 'some repos failed (see above)');
1947
+ return done();
1948
+ }
1949
+ /**
1950
+ * Setup's resident step (docs/RESIDENT-RUNTIME.md §d):
1951
+ * already admitted AND ai.ashlr.daemon running AND its plist is what
1952
+ * `resident start` would write from config now;
1953
+ * waiting-on-you the exact command that gets there (start / clear-stop / switch);
1954
+ * blocked a prerequisite is missing (no active grant, not a compiled
1955
+ * release, dirty build, no confinement, not macOS).
1956
+ * Read-only: never mints, never touches launchd.
1957
+ */
1958
+ async function residentSetupRow(deps, service) {
1959
+ let admission;
1960
+ try {
1961
+ admission = await deps.resident.admission();
1076
1962
  }
1077
- else
1078
- note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority protect --print`, then `--apply`');
1079
- // 8. Retire the burned activation directory.
1080
- const activation = join(homedir(), '.ashlr', 'activation');
1081
- if (!existsSync(activation))
1082
- note('old activation state', 'already', 'nothing to retire');
1083
- else if (await ask('Move ~/.ashlr/activation (the burned key) out of ~/.ashlr so no code path can reach it?')) {
1084
- const target = join(homedir(), `ashlr-activation-retired-${new Date().toISOString().replace(/[:.]/g, '-')}`);
1085
- renameSync(activation, target);
1086
- note('old activation state', 'done', `moved to ${target.replace(homedir(), '~')} — move it offline, then delete it`);
1963
+ catch (error) {
1964
+ return { status: 'failed', detail: `resident admission could not be evaluated (${error.message})` };
1087
1965
  }
1088
- else
1089
- note('old activation state', dryRun ? 'skipped' : 'waiting-on-you', 'archive ~/.ashlr/activation offline, then delete it');
1090
- // 9. Provenance key.
1091
- if (await ask('Rotate the provenance HMAC key (agents could read the old one)?')) {
1092
- const rotated = await rotateProvenanceKey();
1093
- note('provenance key', 'done', rotated.retiredAs ? 'rotated; the old key was moved aside' : 'created');
1966
+ if (!admission.ok) {
1967
+ const fix = admission.command ? ` — run \`${admission.command}\`` : '';
1968
+ // The checklist's command is exactly what the admission asks for (none when it names none).
1969
+ return { status: admission.status === 'waiting-on-you' ? 'waiting-on-you' : 'blocked', detail: `${admission.reason}${fix}`, command: admission.command ?? null };
1094
1970
  }
1095
- else
1096
- note('provenance key', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority rotate-provenance`');
1097
- // 10. First grant (Touch ID), 11. the switch, 12. the daemon service,
1098
- // 13. production resident admission. A running legacy service is not proof
1099
- // that its execution gate or native release boundary is available.
1100
- const DAEMON_DETAIL = 'would check that the ai.ashlr.daemon service is loaded and running (each tick re-verifies the grant)';
1101
- if (planning) {
1102
- note('standing grant', 'skipped', 'would sign the first standing grant (Touch ID)');
1103
- note('autonomy switch', 'skipped', 'would offer to set the autonomy switch to Autonomous (the ladder starts in shadow)');
1104
- note('daemon service', 'skipped', DAEMON_DETAIL);
1105
- return done();
1106
- }
1107
- const { evaluateStandingAuthority, displaySurfaceTarget } = await import('../core/authority/effective-config.js');
1108
- const evaluation = evaluateStandingAuthority({ mode: 'cached', surface: displaySurfaceTarget() });
1109
- let grantActive = evaluation.grantState === 'active';
1110
- if (grantActive) {
1111
- note('standing grant', 'already', `grant #${evaluation.grant?.grantSeq ?? '?'} is active`);
1112
- }
1113
- else if (await ask('Sign the first standing grant now (Touch ID)?')) {
1114
- const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
1115
- const draft = await buildStandingGrantDraft('auto');
1116
- for (const line of draft.summary)
1117
- deps.out(` ${line}`);
1118
- const signed = await signAndInstall(draft.payload, deps);
1119
- grantActive = signed.ok;
1120
- note('standing grant', signed.ok ? 'done' : 'failed', signed.ok ? `grant #${signed.grant.grantSeq} installed, starting at ${signed.grant.rollout.stages[0].id}` : signed.reason);
1971
+ let observed;
1972
+ try {
1973
+ observed = await deps.resident.observe();
1121
1974
  }
1122
- else
1123
- note('standing grant', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority grant`');
1124
- // Resumable: a grant signed on an earlier run (or with `authority grant`)
1125
- // still gets the switch offered — it used to be offered only in the run
1126
- // that signed. Raising stays capped by the grant (requestAutonomySwitch).
1127
- if (!grantActive) {
1128
- note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', 'needs an active standing grant first');
1975
+ catch (error) {
1976
+ return { status: 'failed', detail: `the resident service could not be observed (${error.message})` };
1129
1977
  }
1130
- else if (evaluation.switch === 'autonomous') {
1131
- note('autonomy switch', 'already', 'Autonomous');
1978
+ if (observed.problem)
1979
+ return { status: 'blocked', detail: observed.problem, command: null };
1980
+ const start = '`ashlr authority resident start`';
1981
+ if (service !== 'running') {
1982
+ return { status: 'waiting-on-you', detail: `admitted under grant #${admission.grantSeq ?? '?'} but the resident daemon is not running — run ${start} in your own terminal` };
1132
1983
  }
1133
- else if (await ask('Set the autonomy switch to Autonomous (the ladder starts in shadow)?')) {
1134
- const { requestAutonomySwitch } = await import('../core/authority/effective-config.js');
1135
- const switched = requestAutonomySwitch('autonomous', 'mason', 'ashlr authority setup');
1136
- note('autonomy switch', switched.ok ? 'done' : 'failed', switched.ok ? 'Autonomous' : switched.reason);
1984
+ if (observed.plist === 'drifted') {
1985
+ const budget = observed.installedBudgetUsd !== observed.expectedBudgetUsd
1986
+ ? ` (installed budget $${observed.installedBudgetUsd ?? '?'}/day, config says $${observed.expectedBudgetUsd ?? '?'}/day)`
1987
+ : '';
1988
+ return { status: 'waiting-on-you', detail: `the running service's plist differs from config${budget} — run ${start} to regenerate and restart it` };
1137
1989
  }
1138
- else
1139
- note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', `it is ${evaluation.switch} — run \`ashlr authority switch autonomous\``);
1140
- // 12. Read-only service observation. Manual launchctl advice would imply
1141
- // that this build can admit a resident start, which its production gate denies.
1142
- let service;
1143
- try {
1144
- service = await deps.daemonService();
1990
+ if (observed.plist !== 'current') {
1991
+ return { status: 'waiting-on-you', detail: `the running service's plist could not be matched (${observed.plist}) — run ${start} to regenerate it` };
1145
1992
  }
1146
- catch {
1147
- service = 'unknown';
1148
- }
1149
- if (service === 'running')
1150
- note('daemon service', 'already', 'ai.ashlr.daemon is running; resident execution still requires the separate runtime admission below');
1151
- else if (service === 'absent')
1152
- note('daemon service', 'waiting-on-you', 'no ai.ashlr.daemon service is installed; this build cannot install or start one');
1153
- else if (service === 'loaded')
1154
- note('daemon service', 'waiting-on-you', 'ai.ashlr.daemon is loaded but stopped; this build cannot restart it');
1155
- else if (service === 'not-loaded')
1156
- note('daemon service', 'waiting-on-you', 'ai.ashlr.daemon is not loaded; this build cannot start it');
1157
- else
1158
- note('daemon service', 'waiting-on-you', 'ai.ashlr.daemon state is unknown; this build cannot start or repair it');
1159
- return done();
1993
+ return { status: 'already', detail: `the resident daemon is running under grant #${admission.grantSeq ?? '?'} (until ${admission.expiresAt ?? '?'}), from clean release ${admission.revision?.slice(0, 12) ?? '?'}` };
1994
+ }
1995
+ // ---------------------------------------------------------------------------
1996
+ // resident start | stop | status
1997
+ // ---------------------------------------------------------------------------
1998
+ function describeAdmission(admission) {
1999
+ if (admission.ok)
2000
+ return `admitted — ${admission.reason}`;
2001
+ return `${admission.status} — ${admission.reason}${admission.command ? ` (run \`${admission.command}\`)` : ''}`;
2002
+ }
2003
+ async function cmdResident(sub, parsed, deps) {
2004
+ switch (sub) {
2005
+ case 'status':
2006
+ return cmdResidentStatus(parsed, deps);
2007
+ case 'start':
2008
+ return cmdResidentStart(parsed, deps);
2009
+ case 'stop':
2010
+ return cmdResidentStop(deps);
2011
+ default:
2012
+ deps.err('Usage: ashlr authority resident <start|stop|status [--json]>');
2013
+ return 2;
2014
+ }
2015
+ }
2016
+ async function cmdResidentStatus(parsed, deps) {
2017
+ const admission = await deps.resident.admission();
2018
+ const observed = await deps.resident.observe();
2019
+ if (parsed.flags.has('--json')) {
2020
+ deps.out(JSON.stringify({ schema: 'ashlr.resident-status.v1', admission, service: observed }, null, 2));
2021
+ return 0;
2022
+ }
2023
+ deps.out(`Resident admission: ${describeAdmission(admission)}`);
2024
+ deps.out(`Service: ai.ashlr.daemon ${observed.state}${observed.plistPath ? ` (${observed.plistPath.replace(homedir(), '~')})` : ''}`);
2025
+ const budget = `installed $${observed.installedBudgetUsd ?? '—'}/day, config $${observed.expectedBudgetUsd ?? '?'}/day`;
2026
+ deps.out(`Plist: ${observed.plist}${observed.plist === 'drifted' ? ' — `ashlr authority resident start` regenerates it' : ''} (budget: ${budget})`);
2027
+ if (observed.problem)
2028
+ deps.out(` ${observed.problem}`);
2029
+ return 0;
2030
+ }
2031
+ /**
2032
+ * `resident start` — the only command that installs or restarts the resident
2033
+ * service. Order: refuse a non-operator context; show the verdict; stop if
2034
+ * not admitted; no-op when already running from the current plist; show
2035
+ * exactly what will happen and ask (no --yes: the operator confirms here);
2036
+ * then the real start, which re-verifies everything itself (mint).
2037
+ */
2038
+ async function cmdResidentStart(parsed, deps) {
2039
+ if (parsed.flags.has('--yes')) {
2040
+ deps.err('resident start takes no --yes: confirm it yourself at the prompt.');
2041
+ return 2;
2042
+ }
2043
+ const refusal = await deps.resident.operatorRefusal();
2044
+ if (refusal) {
2045
+ deps.err(`resident start refused: ${refusal}`);
2046
+ return 1;
2047
+ }
2048
+ const admission = await deps.resident.admission();
2049
+ if (!admission.ok) {
2050
+ deps.err(`resident start refused: ${describeAdmission(admission)}`);
2051
+ return 1;
2052
+ }
2053
+ const observed = await deps.resident.observe();
2054
+ if (observed.problem) {
2055
+ deps.err(`resident start refused: ${observed.problem}`);
2056
+ return 1;
2057
+ }
2058
+ if (observed.state === 'running' && observed.plist === 'current') {
2059
+ deps.out(`ai.ashlr.daemon is already running under grant #${admission.grantSeq ?? '?'} from the current plist; nothing to do.`);
2060
+ return 0;
2061
+ }
2062
+ const action = observed.state === 'running' ? 'regenerate the plist from config and restart' : observed.plist === 'absent' ? 'install and start' : 'regenerate, load and start';
2063
+ deps.out(`Resident daemon under grant #${admission.grantSeq ?? '?'} (expires ${admission.expiresAt ?? '?'}):`);
2064
+ deps.out(` release ${admission.revision ?? '?'} (${admission.packageRoot?.replace(homedir(), '~') ?? '?'})`);
2065
+ deps.out(` service ai.ashlr.daemon → ${observed.plistPath?.replace(homedir(), '~') ?? '?'} (${observed.state}, plist ${observed.plist})`);
2066
+ deps.out(` budget $${observed.expectedBudgetUsd ?? '?'}/day from config daemon.dailyBudgetUsd${observed.installedBudgetUsd !== null && observed.installedBudgetUsd !== observed.expectedBudgetUsd ? ` (installed: $${observed.installedBudgetUsd})` : ''}`);
2067
+ deps.out(' launchd enable + bootstrap (RunAtLoad); every tick re-verifies the grant, Stop and the switch');
2068
+ if (!(await deps.confirm(`${action[0].toUpperCase()}${action.slice(1)} ai.ashlr.daemon now?`))) {
2069
+ deps.out('Nothing was changed.');
2070
+ return 1;
2071
+ }
2072
+ const result = await deps.resident.start();
2073
+ if (!result.ok) {
2074
+ deps.err(`resident start failed: ${result.reason}`);
2075
+ return 1;
2076
+ }
2077
+ deps.out(`✓ ${result.detail}`);
2078
+ deps.out('Stop anytime: `ashlr authority stop` halts agents (the service stays up, idle); `ashlr authority resident stop` removes the service.');
2079
+ return 0;
2080
+ }
2081
+ /** Lowering: never asks, never needs the grant. */
2082
+ async function cmdResidentStop(deps) {
2083
+ const result = await deps.resident.stop();
2084
+ if (!result.ok) {
2085
+ deps.err(`resident stop failed: ${result.reason}`);
2086
+ return 1;
2087
+ }
2088
+ deps.out(`${result.detail}. Stop (~/.ashlr/KILL) and the grant are unchanged.`);
2089
+ return 0;
1160
2090
  }
1161
2091
  function finish(report, deps, dryRun = false, json = false) {
1162
2092
  const result = authoritySetupReport(report, dryRun);
@@ -1166,10 +2096,54 @@ function finish(report, deps, dryRun = false, json = false) {
1166
2096
  }
1167
2097
  else {
1168
2098
  const planned = dryRun ? `, ${summary.planned} planned (dry run: nothing was asked or changed)` : '';
1169
- deps.out(`\nSetup: ${summary.done} done, ${summary.already} already in place, ${summary.waitingOnYou} waiting on you, ${summary.blocked} blocked by this build, ${summary.failed} failed${planned}.`);
2099
+ deps.out(`\nSetup: ${summary.done} done, ${summary.already} already in place, ${summary.waitingOnYou} waiting on you, ${summary.blocked} blocked on a prerequisite, ${summary.failed} failed${planned}.`);
1170
2100
  }
1171
2101
  return summary.failed > 0 ? 1 : 0;
1172
2102
  }
2103
+ const HUB_REPO = 'ashlrai/ashlr-hub';
2104
+ /**
2105
+ * The dry run's look at the trust-root PR, through `gh api <path>` GETs only
2106
+ * (each passes isReadOnlyGhApiCall, so the Verse probe can run it): the PR an
2107
+ * earlier run opened, the key already merged on the default branch, or a
2108
+ * branch an earlier run pushed without a PR. Same order as the live run's
2109
+ * probes. null = none of these (or GitHub could not be read): the step then
2110
+ * reads as it always has.
2111
+ */
2112
+ async function probeTrustRootReadOnly(deps, root) {
2113
+ const branch = `authority/trust-root-${root.keyId}`;
2114
+ const read = async (path) => {
2115
+ const result = await deps.run('gh', ['api', path]);
2116
+ if (result.status !== 0)
2117
+ return null;
2118
+ try {
2119
+ return JSON.parse(result.stdout || 'null');
2120
+ }
2121
+ catch {
2122
+ return null;
2123
+ }
2124
+ };
2125
+ const open = await read(`repos/${HUB_REPO}/pulls?head=${HUB_REPO.split('/')[0]}%3A${encodeURIComponent(branch)}&state=open&per_page=1`);
2126
+ const url = Array.isArray(open) ? open[0]?.html_url : undefined;
2127
+ if (typeof url === 'string' && /^https:\/\/github\.com\/[^\s]+\/pull\/\d+$/u.test(url)) {
2128
+ return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, link: url, command: null };
2129
+ }
2130
+ const repo = await read(`repos/${HUB_REPO}`);
2131
+ const base = typeof repo?.default_branch === 'string' && /^[A-Za-z0-9._/-]{1,100}$/u.test(repo.default_branch) ? repo.default_branch : 'master';
2132
+ const file = await read(`repos/${HUB_REPO}/contents/src/core/authority/trust-roots.ts?ref=${encodeURIComponent(base)}`);
2133
+ if (file && file.encoding === 'base64' && typeof file.content === 'string'
2134
+ && Buffer.from(file.content, 'base64').toString('utf8').includes(`keyId: '${root.keyId}'`)) {
2135
+ return {
2136
+ status: 'waiting-on-you',
2137
+ detail: `${root.keyId} is merged on ${base}, but this release was built before it — install a release built after it`,
2138
+ command: 'npm run build',
2139
+ };
2140
+ }
2141
+ const pushed = await read(`repos/${HUB_REPO}/git/ref/heads/${branch.split('/').map(encodeURIComponent).join('/')}`);
2142
+ if (pushed && pushed.ref === `refs/heads/${branch}`) {
2143
+ return { status: 'waiting-on-you', detail: `${branch} was pushed by an earlier run but has no PR yet — rerun setup to open it` };
2144
+ }
2145
+ return null;
2146
+ }
1173
2147
  /**
1174
2148
  * Open Mason's trust-root PR from a throwaway worktree (his checkout is never
1175
2149
  * touched). Resumable: a rerun finds the PR it already opened, or the key
@@ -1180,73 +2154,76 @@ async function openTrustRootPr(parsed, deps, root, ask, probe) {
1180
2154
  if (!source)
1181
2155
  return { status: 'waiting-on-you', detail: 'pass --source <your ashlr-hub checkout> so setup can open the trust-root PR' };
1182
2156
  const branch = `authority/trust-root-${root.keyId}`;
1183
- const base = () => {
1184
- const result = deps.run('gh', ['repo', 'view', 'ashlrai/ashlr-hub', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name']);
2157
+ const base = async () => {
2158
+ const result = await deps.run('gh', ['repo', 'view', 'ashlrai/ashlr-hub', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name']);
1185
2159
  return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : 'master';
1186
2160
  };
2161
+ const prLink = (text) => /https:\/\/github\.com\/\S+\/pull\/\d+/u.exec(text)?.[0];
1187
2162
  let baseBranch = null;
1188
2163
  let pushedAlready = false;
1189
2164
  if (probe) {
1190
2165
  // Read-only probes (a dry run never makes them).
1191
- const open = deps.run('gh', ['pr', 'list', '--repo', 'ashlrai/ashlr-hub', '--head', branch, '--state', 'open', '--json', 'url', '--jq', '.[0].url // ""']);
2166
+ const open = await deps.run('gh', ['pr', 'list', '--repo', 'ashlrai/ashlr-hub', '--head', branch, '--state', 'open', '--json', 'url', '--jq', '.[0].url // ""']);
1192
2167
  if (open.status === 0 && open.stdout.trim()) {
1193
- return { status: 'waiting-on-you', detail: `${open.stdout.trim()} is already open — review and merge it yourself` };
2168
+ const url = open.stdout.trim();
2169
+ return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
1194
2170
  }
1195
- baseBranch = base();
1196
- const fetched = deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
2171
+ baseBranch = await base();
2172
+ const fetched = await deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
1197
2173
  const onBase = fetched.status === 0
1198
- ? deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
2174
+ ? await deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
1199
2175
  : null;
1200
2176
  if (onBase?.status === 0 && onBase.stdout.includes(`keyId: '${root.keyId}'`)) {
1201
2177
  return { status: 'waiting-on-you', detail: `${root.keyId} is already on ${baseBranch}, but this release was built before it` };
1202
2178
  }
1203
2179
  // An earlier run pushed the branch but `gh pr create` failed: open the PR from it.
1204
- const remote = deps.run('git', ['-C', source, 'ls-remote', '--exit-code', '--heads', 'origin', branch]);
2180
+ const remote = await deps.run('git', ['-C', source, 'ls-remote', '--exit-code', '--heads', 'origin', branch]);
1205
2181
  pushedAlready = remote.status === 0 && remote.stdout.trim() !== '';
1206
2182
  }
1207
2183
  if (!(await ask(`Open a PR in ${source.replace(homedir(), '~')} adding key ${root.keyId} to trust-roots.ts?`))) {
1208
2184
  return { status: 'waiting-on-you', detail: `add this root yourself: ${JSON.stringify({ keyId: root.keyId, alg: 'ES256' })}` };
1209
2185
  }
1210
- baseBranch ??= base();
1211
- const createPr = (cwd) => {
1212
- const pr = deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
2186
+ baseBranch ??= await base();
2187
+ const createPr = async (cwd) => {
2188
+ const pr = await deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
1213
2189
  '--title', `authority: trust custody key ${root.keyId}`,
1214
2190
  '--body', `Adds the Secure Enclave custody key \`${root.keyId}\` to STANDING_GRANT_TRUST_ROOTS (opened by \`ashlr authority setup\`). Tier-1 path: review it yourself.`], cwd ? { cwd } : {});
1215
2191
  if (pr.status !== 0)
1216
2192
  return { status: 'failed', detail: `pushed ${branch}, but the PR could not be opened: ${pr.stderr.trim().slice(0, 200)}` };
1217
- return { status: 'waiting-on-you', detail: `opened ${pr.stdout.trim()} — review and merge it yourself` };
2193
+ const url = pr.stdout.trim();
2194
+ return { status: 'waiting-on-you', detail: `opened ${url} — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
1218
2195
  };
1219
2196
  if (pushedAlready)
1220
2197
  return createPr();
1221
2198
  const worktree = mkdtempSync(join(tmpdir(), 'ashlr-trust-root-'));
1222
2199
  rmSync(worktree, { recursive: true, force: true });
1223
- const step = (bin, args, cwd) => {
1224
- const result = deps.run(bin, args, cwd ? { cwd } : {});
2200
+ const step = async (bin, args, cwd) => {
2201
+ const result = await deps.run(bin, args, cwd ? { cwd } : {});
1225
2202
  if (result.status !== 0)
1226
2203
  throw new Error(`${bin} ${args[0] === '-C' ? args[2] : args[0]} failed: ${result.stderr.trim().slice(0, 200)}`);
1227
2204
  };
1228
2205
  let worktreeAdded = false;
1229
2206
  try {
1230
- step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
2207
+ await step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
1231
2208
  // -B: a local branch left by an earlier run that stopped before the PR
1232
2209
  // (push or `gh pr create` failed) is reset instead of blocking every rerun.
1233
- step('git', ['-C', source, 'worktree', 'add', '-B', branch, worktree, `origin/${baseBranch}`]);
2210
+ await step('git', ['-C', source, 'worktree', 'add', '-B', branch, worktree, `origin/${baseBranch}`]);
1234
2211
  worktreeAdded = true;
1235
2212
  const file = join(worktree, 'src', 'core', 'authority', 'trust-roots.ts');
1236
2213
  const updated = renderTrustRootsWithKey(readFileSync(file, 'utf8'), root);
1237
2214
  if (!updated)
1238
2215
  return { status: 'failed', detail: `trust-roots.ts on ${baseBranch} already lists a different root — add the key by hand` };
1239
2216
  writeFileSync(file, updated);
1240
- step('git', ['add', 'src/core/authority/trust-roots.ts'], worktree);
1241
- step('git', ['commit', '-m', `authority: trust custody key ${root.keyId}`], worktree);
1242
- step('git', ['push', '-u', 'origin', branch], worktree);
1243
- return createPr(worktree);
2217
+ await step('git', ['add', 'src/core/authority/trust-roots.ts'], worktree);
2218
+ await step('git', ['commit', '-m', `authority: trust custody key ${root.keyId}`], worktree);
2219
+ await step('git', ['push', '-u', 'origin', branch], worktree);
2220
+ return await createPr(worktree);
1244
2221
  }
1245
2222
  finally {
1246
- deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
2223
+ await deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
1247
2224
  // The pushed branch lives on origin; the local copy only blocks a rerun.
1248
2225
  if (worktreeAdded)
1249
- deps.run('git', ['-C', source, 'branch', '-D', branch]);
2226
+ await deps.run('git', ['-C', source, 'branch', '-D', branch]);
1250
2227
  }
1251
2228
  }
1252
2229
  /** The enrolled checkout of ashlr-hub itself, if any. */