@ashlr/hub 3.11.5 → 3.13.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 (408) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +29 -16
  3. package/dist/authority-surface.json +91 -45
  4. package/dist/build-identity.json +2 -2
  5. package/dist/cli/authority.d.ts +145 -33
  6. package/dist/cli/authority.js +1196 -313
  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/core/authority/protected-paths.js +11 -0
  11. package/dist/core/authority/protected-paths.js.map +1 -1
  12. package/dist/core/authority/resident.d.ts +101 -0
  13. package/dist/core/authority/resident.js +246 -0
  14. package/dist/core/authority/resident.js.map +1 -0
  15. package/dist/core/authority/trust-roots.js +7 -1
  16. package/dist/core/authority/trust-roots.js.map +1 -1
  17. package/dist/core/authority/types.d.ts +43 -0
  18. package/dist/core/authority/types.js +6 -0
  19. package/dist/core/authority/types.js.map +1 -1
  20. package/dist/core/cloud/budget.js +11 -1
  21. package/dist/core/cloud/budget.js.map +1 -1
  22. package/dist/core/cloud/cloud-api.d.ts +25 -2
  23. package/dist/core/cloud/cloud-api.js +139 -5
  24. package/dist/core/cloud/cloud-api.js.map +1 -1
  25. package/dist/core/cloud/pr-actions.d.ts +93 -0
  26. package/dist/core/cloud/pr-actions.js +436 -0
  27. package/dist/core/cloud/pr-actions.js.map +1 -0
  28. package/dist/core/cloud/pr-preview.d.ts +128 -0
  29. package/dist/core/cloud/pr-preview.js +228 -0
  30. package/dist/core/cloud/pr-preview.js.map +1 -0
  31. package/dist/core/cloud/store.d.ts +2 -0
  32. package/dist/core/cloud/store.js +28 -0
  33. package/dist/core/cloud/store.js.map +1 -1
  34. package/dist/core/cloud/timeline-api.d.ts +24 -0
  35. package/dist/core/cloud/timeline-api.js +46 -0
  36. package/dist/core/cloud/timeline-api.js.map +1 -0
  37. package/dist/core/cloud/timeline-types.d.ts +62 -0
  38. package/dist/core/cloud/timeline-types.js +36 -0
  39. package/dist/core/cloud/timeline-types.js.map +1 -0
  40. package/dist/core/cloud/timeline.d.ts +102 -0
  41. package/dist/core/cloud/timeline.js +919 -0
  42. package/dist/core/cloud/timeline.js.map +1 -0
  43. package/dist/core/cloud/tracker.js +67 -0
  44. package/dist/core/cloud/tracker.js.map +1 -1
  45. package/dist/core/cloud/types.d.ts +38 -0
  46. package/dist/core/cloud/types.js +1 -1
  47. package/dist/core/cloud/types.js.map +1 -1
  48. package/dist/core/daemon/service-install-authority.d.ts +10 -2
  49. package/dist/core/daemon/service-install-authority.js +14 -5
  50. package/dist/core/daemon/service-install-authority.js.map +1 -1
  51. package/dist/core/daemon/service.d.ts +15 -0
  52. package/dist/core/daemon/service.js +26 -0
  53. package/dist/core/daemon/service.js.map +1 -1
  54. package/dist/core/fleet/cloud-intake.d.ts +95 -0
  55. package/dist/core/fleet/cloud-intake.js +557 -0
  56. package/dist/core/fleet/cloud-intake.js.map +1 -0
  57. package/dist/core/fleet/dispatch-router.d.ts +13 -0
  58. package/dist/core/fleet/dispatch-router.js +32 -8
  59. package/dist/core/fleet/dispatch-router.js.map +1 -1
  60. package/dist/core/fleet/fleet-merge-state.d.ts +10 -0
  61. package/dist/core/fleet/fleet-merge-state.js.map +1 -1
  62. package/dist/core/fleet/host-merge.d.ts +5 -0
  63. package/dist/core/fleet/host-merge.js +11 -0
  64. package/dist/core/fleet/host-merge.js.map +1 -1
  65. package/dist/core/fleet/merge-gates.d.ts +8 -0
  66. package/dist/core/fleet/merge-gates.js +25 -5
  67. package/dist/core/fleet/merge-gates.js.map +1 -1
  68. package/dist/core/fleet/standing-merge-pass.js +30 -0
  69. package/dist/core/fleet/standing-merge-pass.js.map +1 -1
  70. package/dist/core/fleet/tick-hooks-live.d.ts +9 -0
  71. package/dist/core/fleet/tick-hooks-live.js +44 -5
  72. package/dist/core/fleet/tick-hooks-live.js.map +1 -1
  73. package/dist/core/fleet/verify-check-run.d.ts +90 -0
  74. package/dist/core/fleet/verify-check-run.js +274 -0
  75. package/dist/core/fleet/verify-check-run.js.map +1 -0
  76. package/dist/core/routing/budget-api.d.ts +11 -0
  77. package/dist/core/routing/budget-api.js +64 -8
  78. package/dist/core/routing/budget-api.js.map +1 -1
  79. package/dist/core/routing/budget-store.d.ts +22 -1
  80. package/dist/core/routing/budget-store.js +22 -8
  81. package/dist/core/routing/budget-store.js.map +1 -1
  82. package/dist/core/universe/builtins/preparation/manifest.json +1 -1
  83. package/dist/core/universe/builtins/preparation/preparation-bridge.mjs +8 -2
  84. package/dist/core/universe/builtins/preparation/preparation-verification-fixtures.mjs +7 -1
  85. package/dist/core/verse/account-health.js +9 -3
  86. package/dist/core/verse/account-health.js.map +1 -1
  87. package/dist/core/verse/activity-api.d.ts +5 -1
  88. package/dist/core/verse/activity-api.js +62 -14
  89. package/dist/core/verse/activity-api.js.map +1 -1
  90. package/dist/core/verse/authority-api.d.ts +9 -1
  91. package/dist/core/verse/authority-api.js +82 -2
  92. package/dist/core/verse/authority-api.js.map +1 -1
  93. package/dist/core/verse/seat-readiness.d.ts +25 -0
  94. package/dist/core/verse/seat-readiness.js +43 -6
  95. package/dist/core/verse/seat-readiness.js.map +1 -1
  96. package/dist/core/verse/verse-api.d.ts +3 -1
  97. package/dist/core/verse/verse-api.js +6 -0
  98. package/dist/core/verse/verse-api.js.map +1 -1
  99. package/dist/core/web/public/next/assets/{ActionMenu-DffwPklw.js → ActionMenu-Dwjk6rUQ.js} +2 -2
  100. package/dist/core/web/public/next/assets/{ActionMenu-DffwPklw.js.map → ActionMenu-Dwjk6rUQ.js.map} +1 -1
  101. package/dist/core/web/public/next/assets/Advanced-cVYTAsfB.js +2 -0
  102. package/dist/core/web/public/next/assets/{Advanced-CxzE6ekM.js.map → Advanced-cVYTAsfB.js.map} +1 -1
  103. package/dist/core/web/public/next/assets/App-IsaWDsdF.js +7 -0
  104. package/dist/core/web/public/next/assets/{App-DFlp-vdv.js.map → App-IsaWDsdF.js.map} +1 -1
  105. package/dist/core/web/public/next/assets/AppsSection-DDqeSrty.js +2 -0
  106. package/dist/core/web/public/next/assets/{AppsSection-Ct2O605-.js.map → AppsSection-DDqeSrty.js.map} +1 -1
  107. package/dist/core/web/public/next/assets/{AreaTrend-o-GLl2Is.js → AreaTrend-C32rUaxm.js} +2 -2
  108. package/dist/core/web/public/next/assets/{AreaTrend-o-GLl2Is.js.map → AreaTrend-C32rUaxm.js.map} +1 -1
  109. package/dist/core/web/public/next/assets/AutonomyOffState-CmSSTHVe.css +1 -0
  110. package/dist/core/web/public/next/assets/AutonomyOffState-CpFYS3tQ.js +3 -0
  111. package/dist/core/web/public/next/assets/AutonomyOffState-CpFYS3tQ.js.map +1 -0
  112. package/dist/core/web/public/next/assets/AutonomySection-Dwn8UtOy.js +2 -0
  113. package/dist/core/web/public/next/assets/{AutonomySection-BTg7yVa1.js.map → AutonomySection-Dwn8UtOy.js.map} +1 -1
  114. package/dist/core/web/public/next/assets/{BranchBar-BjPFtGsj.js → BranchBar-ISZ7xUCj.js} +2 -2
  115. package/dist/core/web/public/next/assets/{BranchBar-BjPFtGsj.js.map → BranchBar-ISZ7xUCj.js.map} +1 -1
  116. package/dist/core/web/public/next/assets/BudgetControl-BARVp-En.js +2 -0
  117. package/dist/core/web/public/next/assets/BudgetControl-BARVp-En.js.map +1 -0
  118. package/dist/core/web/public/next/assets/{Button-OjVJSrFq.js → Button-DtE0rlWD.js} +2 -2
  119. package/dist/core/web/public/next/assets/{Button-OjVJSrFq.js.map → Button-DtE0rlWD.js.map} +1 -1
  120. package/dist/core/web/public/next/assets/{CapacityStrip-Bx4c3dxD.js → CapacityStrip-DKbKA_Aa.js} +2 -2
  121. package/dist/core/web/public/next/assets/{CapacityStrip-Bx4c3dxD.js.map → CapacityStrip-DKbKA_Aa.js.map} +1 -1
  122. package/dist/core/web/public/next/assets/{ChartFrame-B_w2Siqg.js → ChartFrame-DK_BDXCt.js} +2 -2
  123. package/dist/core/web/public/next/assets/{ChartFrame-B_w2Siqg.js.map → ChartFrame-DK_BDXCt.js.map} +1 -1
  124. package/dist/core/web/public/next/assets/ChatSection-C9myY4ei.js +3 -0
  125. package/dist/core/web/public/next/assets/ChatSection-C9myY4ei.js.map +1 -0
  126. package/dist/core/web/public/next/assets/{CloudBudgetForm-5Gf4iAok.js → CloudBudgetForm-CvjK4Oh6.js} +2 -2
  127. package/dist/core/web/public/next/assets/{CloudBudgetForm-5Gf4iAok.js.map → CloudBudgetForm-CvjK4Oh6.js.map} +1 -1
  128. package/dist/core/web/public/next/assets/{CommandPalette-iTpk6I8r.js → CommandPalette-DzPsth0u.js} +2 -2
  129. package/dist/core/web/public/next/assets/{CommandPalette-iTpk6I8r.js.map → CommandPalette-DzPsth0u.js.map} +1 -1
  130. package/dist/core/web/public/next/assets/CommandSection-Brp73iox.js +3 -0
  131. package/dist/core/web/public/next/assets/CommandSection-Brp73iox.js.map +1 -0
  132. package/dist/core/web/public/next/assets/{ConfirmDialog-_S9iE-cJ.js → ConfirmDialog-CLUhzaBj.js} +2 -2
  133. package/dist/core/web/public/next/assets/{ConfirmDialog-_S9iE-cJ.js.map → ConfirmDialog-CLUhzaBj.js.map} +1 -1
  134. package/dist/core/web/public/next/assets/{DeleteChatDialog-KUYUygiA.js → DeleteChatDialog-C_eDhV8b.js} +2 -2
  135. package/dist/core/web/public/next/assets/{DeleteChatDialog-KUYUygiA.js.map → DeleteChatDialog-C_eDhV8b.js.map} +1 -1
  136. package/dist/core/web/public/next/assets/{Dialog-BuIaG6HF.js → Dialog-BRd_B9OE.js} +2 -2
  137. package/dist/core/web/public/next/assets/{Dialog-BuIaG6HF.js.map → Dialog-BRd_B9OE.js.map} +1 -1
  138. package/dist/core/web/public/next/assets/{DiffPane-De4IQt82.js → DiffPane-Cpr9soB2.js} +2 -2
  139. package/dist/core/web/public/next/assets/{DiffPane-De4IQt82.js.map → DiffPane-Cpr9soB2.js.map} +1 -1
  140. package/dist/core/web/public/next/assets/{DiffViewer-B75t6Ile.js → DiffViewer-CNwd0W3Q.js} +2 -2
  141. package/dist/core/web/public/next/assets/{DiffViewer-B75t6Ile.js.map → DiffViewer-CNwd0W3Q.js.map} +1 -1
  142. package/dist/core/web/public/next/assets/DockHost-BcEq2qaS.js +2 -0
  143. package/dist/core/web/public/next/assets/{DockHost-BPqyhtCP.js.map → DockHost-BcEq2qaS.js.map} +1 -1
  144. package/dist/core/web/public/next/assets/{EmptyState-DXkT4vhW.js → EmptyState-DsN1H6MC.js} +2 -2
  145. package/dist/core/web/public/next/assets/{EmptyState-DXkT4vhW.js.map → EmptyState-DsN1H6MC.js.map} +1 -1
  146. package/dist/core/web/public/next/assets/{Epistemic-CfqFe_aP.js → Epistemic-fycQTBRO.js} +2 -2
  147. package/dist/core/web/public/next/assets/{Epistemic-CfqFe_aP.js.map → Epistemic-fycQTBRO.js.map} +1 -1
  148. package/dist/core/web/public/next/assets/EvidenceTimeline-PBDAiSuG.css +1 -0
  149. package/dist/core/web/public/next/assets/EvidenceTimeline-sCCAdnBK.js +2 -0
  150. package/dist/core/web/public/next/assets/EvidenceTimeline-sCCAdnBK.js.map +1 -0
  151. package/dist/core/web/public/next/assets/{FleetSection-B7f-XBiX.js → FleetSection-BG8iGraG.js} +3 -3
  152. package/dist/core/web/public/next/assets/{FleetSection-B7f-XBiX.js.map → FleetSection-BG8iGraG.js.map} +1 -1
  153. package/dist/core/web/public/next/assets/{Funnel-pXYySwXQ.js → Funnel-B88_wpA4.js} +2 -2
  154. package/dist/core/web/public/next/assets/{Funnel-pXYySwXQ.js.map → Funnel-B88_wpA4.js.map} +1 -1
  155. package/dist/core/web/public/next/assets/{GearTray-gj6lT7ba.js → GearTray-CpDt0xwj.js} +2 -2
  156. package/dist/core/web/public/next/assets/{GearTray-gj6lT7ba.js.map → GearTray-CpDt0xwj.js.map} +1 -1
  157. package/dist/core/web/public/next/assets/{GrowthSection-D8e9m9Wy.js → GrowthSection-BvZezOsX.js} +2 -2
  158. package/dist/core/web/public/next/assets/{GrowthSection-D8e9m9Wy.js.map → GrowthSection-BvZezOsX.js.map} +1 -1
  159. package/dist/core/web/public/next/assets/{HandoffDialog-VfKxKhxF.js → HandoffDialog-B1G6gQyk.js} +2 -2
  160. package/dist/core/web/public/next/assets/{HandoffDialog-VfKxKhxF.js.map → HandoffDialog-B1G6gQyk.js.map} +1 -1
  161. package/dist/core/web/public/next/assets/{Input-u1YR_Ogl.js → Input-CI0yBHmn.js} +2 -2
  162. package/dist/core/web/public/next/assets/{Input-u1YR_Ogl.js.map → Input-CI0yBHmn.js.map} +1 -1
  163. package/dist/core/web/public/next/assets/{LeaderCard-BDELBrnx.js → LeaderCard-BxxiEg4e.js} +2 -2
  164. package/dist/core/web/public/next/assets/{LeaderCard-BDELBrnx.js.map → LeaderCard-BxxiEg4e.js.map} +1 -1
  165. package/dist/core/web/public/next/assets/{MatrixHeatmap-NKLF2srR.js → MatrixHeatmap-iPFEU2Tv.js} +2 -2
  166. package/dist/core/web/public/next/assets/{MatrixHeatmap-NKLF2srR.js.map → MatrixHeatmap-iPFEU2Tv.js.map} +1 -1
  167. package/dist/core/web/public/next/assets/McpSection-nuXer4K-.js +2 -0
  168. package/dist/core/web/public/next/assets/{McpSection-D1tDvlRa.js.map → McpSection-nuXer4K-.js.map} +1 -1
  169. package/dist/core/web/public/next/assets/{MessageMarkdown-DlfLEJxu.js → MessageMarkdown-BCe4TsXk.js} +3 -3
  170. package/dist/core/web/public/next/assets/{MessageMarkdown-DlfLEJxu.js.map → MessageMarkdown-BCe4TsXk.js.map} +1 -1
  171. package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-BYZzI115.js → MessageMarkdownRenderer-DL3LU5kL.js} +2 -2
  172. package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-BYZzI115.js.map → MessageMarkdownRenderer-DL3LU5kL.js.map} +1 -1
  173. package/dist/core/web/public/next/assets/{Meter-BSVhU43Q.js → Meter-b69LM0mU.js} +2 -2
  174. package/dist/core/web/public/next/assets/{Meter-BSVhU43Q.js.map → Meter-b69LM0mU.js.map} +1 -1
  175. package/dist/core/web/public/next/assets/{MindSection-lqV2e_Sq.js → MindSection-Dc5v5rH3.js} +2 -2
  176. package/dist/core/web/public/next/assets/{MindSection-lqV2e_Sq.js.map → MindSection-Dc5v5rH3.js.map} +1 -1
  177. package/dist/core/web/public/next/assets/{MutationTokenDialog-CLGP72g6.js → MutationTokenDialog-Bm34BNVV.js} +2 -2
  178. package/dist/core/web/public/next/assets/{MutationTokenDialog-CLGP72g6.js.map → MutationTokenDialog-Bm34BNVV.js.map} +1 -1
  179. package/dist/core/web/public/next/assets/NeedsYouDrawer-N1cWJjpz.js +3 -0
  180. package/dist/core/web/public/next/assets/NeedsYouDrawer-N1cWJjpz.js.map +1 -0
  181. package/dist/core/web/public/next/assets/NeedsYouList-CH6Mq-E_.css +1 -0
  182. package/dist/core/web/public/next/assets/NeedsYouList-D5Kk549g.js +2 -0
  183. package/dist/core/web/public/next/assets/{NeedsYouList-eBOlJr0b.js.map → NeedsYouList-D5Kk549g.js.map} +1 -1
  184. package/dist/core/web/public/next/assets/{NewChatDialog-DoYTH26M.js → NewChatDialog-BqFTWgHU.js} +2 -2
  185. package/dist/core/web/public/next/assets/{NewChatDialog-DoYTH26M.js.map → NewChatDialog-BqFTWgHU.js.map} +1 -1
  186. package/dist/core/web/public/next/assets/OnboardingFlow-DcDsguAM.js +3 -0
  187. package/dist/core/web/public/next/assets/OnboardingFlow-DcDsguAM.js.map +1 -0
  188. package/dist/core/web/public/next/assets/OnboardingGate-BZafM1Vf.js +3 -0
  189. package/dist/core/web/public/next/assets/{OnboardingGate-DrDEVBqw.js.map → OnboardingGate-BZafM1Vf.js.map} +1 -1
  190. package/dist/core/web/public/next/assets/{PreviewPane-ULN673zm.js → PreviewPane-zT3gK9TD.js} +2 -2
  191. package/dist/core/web/public/next/assets/{PreviewPane-ULN673zm.js.map → PreviewPane-zT3gK9TD.js.map} +1 -1
  192. package/dist/core/web/public/next/assets/{ProviderLogo-8WaUjTlN.js → ProviderLogo-AwHn2sKm.js} +2 -2
  193. package/dist/core/web/public/next/assets/{ProviderLogo-8WaUjTlN.js.map → ProviderLogo-AwHn2sKm.js.map} +1 -1
  194. package/dist/core/web/public/next/assets/{RailStatus-Ckw9nvYU.js → RailStatus-xHrovWvI.js} +2 -2
  195. package/dist/core/web/public/next/assets/{RailStatus-Ckw9nvYU.js.map → RailStatus-xHrovWvI.js.map} +1 -1
  196. package/dist/core/web/public/next/assets/{RefreshIndicator-DGubF4vR.js → RefreshIndicator-SEFZlMB2.js} +2 -2
  197. package/dist/core/web/public/next/assets/{RefreshIndicator-DGubF4vR.js.map → RefreshIndicator-SEFZlMB2.js.map} +1 -1
  198. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-BahEaY4d.js → ResourcePoolConsoleApp-CAxPQ4sZ.js} +3 -3
  199. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-BahEaY4d.js.map → ResourcePoolConsoleApp-CAxPQ4sZ.js.map} +1 -1
  200. package/dist/core/web/public/next/assets/ResourcesChrome-BnhnEdJe.js +3 -0
  201. package/dist/core/web/public/next/assets/{ResourcesChrome-C2ZrA7la.js.map → ResourcesChrome-BnhnEdJe.js.map} +1 -1
  202. package/dist/core/web/public/next/assets/{ResourcesDrawer-BC93fzwQ.js → ResourcesDrawer-BAFGodny.js} +2 -2
  203. package/dist/core/web/public/next/assets/{ResourcesDrawer-BC93fzwQ.js.map → ResourcesDrawer-BAFGodny.js.map} +1 -1
  204. package/dist/core/web/public/next/assets/{RouteErrorBoundary-BvhHNpE_.js → RouteErrorBoundary-C8WGiixc.js} +2 -2
  205. package/dist/core/web/public/next/assets/{RouteErrorBoundary-BvhHNpE_.js.map → RouteErrorBoundary-C8WGiixc.js.map} +1 -1
  206. package/dist/core/web/public/next/assets/{RunInCloudAction-Dywfnw0C.js → RunInCloudAction-1-IPLYhf.js} +2 -2
  207. package/dist/core/web/public/next/assets/{RunInCloudAction-Dywfnw0C.js.map → RunInCloudAction-1-IPLYhf.js.map} +1 -1
  208. package/dist/core/web/public/next/assets/{SeatSelector-CH7hTgwy.js → SeatSelector-DpwNiX8F.js} +2 -2
  209. package/dist/core/web/public/next/assets/{SeatSelector-CH7hTgwy.js.map → SeatSelector-DpwNiX8F.js.map} +1 -1
  210. package/dist/core/web/public/next/assets/{Segmented-Wynbg0F5.js → Segmented-Wt1ATod9.js} +2 -2
  211. package/dist/core/web/public/next/assets/{Segmented-Wynbg0F5.js.map → Segmented-Wt1ATod9.js.map} +1 -1
  212. package/dist/core/web/public/next/assets/{Select-DMyLCkId.js → Select-By-V9lMc.js} +2 -2
  213. package/dist/core/web/public/next/assets/{Select-DMyLCkId.js.map → Select-By-V9lMc.js.map} +1 -1
  214. package/dist/core/web/public/next/assets/{SessionGate-8RtQFWeZ.js → SessionGate-WROS02tK.js} +2 -2
  215. package/dist/core/web/public/next/assets/{SessionGate-8RtQFWeZ.js.map → SessionGate-WROS02tK.js.map} +1 -1
  216. package/dist/core/web/public/next/assets/{SessionInsightChip-zBJbKv2L.js → SessionInsightChip-DNUcruwp.js} +2 -2
  217. package/dist/core/web/public/next/assets/{SessionInsightChip-zBJbKv2L.js.map → SessionInsightChip-DNUcruwp.js.map} +1 -1
  218. package/dist/core/web/public/next/assets/{SessionSearch-DfJIcxxM.js → SessionSearch-UHtt7a8f.js} +2 -2
  219. package/dist/core/web/public/next/assets/{SessionSearch-DfJIcxxM.js.map → SessionSearch-UHtt7a8f.js.map} +1 -1
  220. package/dist/core/web/public/next/assets/{SettingsSection-Da9RYyH5.js → SettingsSection-Bf74l0Gj.js} +2 -2
  221. package/dist/core/web/public/next/assets/{SettingsSection-Da9RYyH5.js.map → SettingsSection-Bf74l0Gj.js.map} +1 -1
  222. package/dist/core/web/public/next/assets/SetupChecklist-BhZpFLsj.css +1 -0
  223. package/dist/core/web/public/next/assets/SetupChecklist-CzpEcfOl.js +2 -0
  224. package/dist/core/web/public/next/assets/SetupChecklist-CzpEcfOl.js.map +1 -0
  225. package/dist/core/web/public/next/assets/{Sheet-DhXyIDmq.js → Sheet-HkBOBMTT.js} +2 -2
  226. package/dist/core/web/public/next/assets/{Sheet-DhXyIDmq.js.map → Sheet-HkBOBMTT.js.map} +1 -1
  227. package/dist/core/web/public/next/assets/{ShortcutsOverlay-Bi5f-wUw.js → ShortcutsOverlay-DiigiT8H.js} +2 -2
  228. package/dist/core/web/public/next/assets/{ShortcutsOverlay-Bi5f-wUw.js.map → ShortcutsOverlay-DiigiT8H.js.map} +1 -1
  229. package/dist/core/web/public/next/assets/Sidebar-C9YtBYVZ.js +3 -0
  230. package/dist/core/web/public/next/assets/Sidebar-C9YtBYVZ.js.map +1 -0
  231. package/dist/core/web/public/next/assets/{Slider-Bd9YFWEV.js → Slider-DaEDws-b.js} +2 -2
  232. package/dist/core/web/public/next/assets/{Slider-Bd9YFWEV.js.map → Slider-DaEDws-b.js.map} +1 -1
  233. package/dist/core/web/public/next/assets/{StatTile-DScTd3c-.js → StatTile-CTX41O6y.js} +2 -2
  234. package/dist/core/web/public/next/assets/{StatTile-DScTd3c-.js.map → StatTile-CTX41O6y.js.map} +1 -1
  235. package/dist/core/web/public/next/assets/{StatusBadge-DQ1UtQAJ.js → StatusBadge-CXG3iNL4.js} +2 -2
  236. package/dist/core/web/public/next/assets/{StatusBadge-DQ1UtQAJ.js.map → StatusBadge-CXG3iNL4.js.map} +1 -1
  237. package/dist/core/web/public/next/assets/{StepBand-BB3IJxhr.js → StepBand-jJTqYIdX.js} +2 -2
  238. package/dist/core/web/public/next/assets/{StepBand-BB3IJxhr.js.map → StepBand-jJTqYIdX.js.map} +1 -1
  239. package/dist/core/web/public/next/assets/{Surface-DWMtQe3Q.js → Surface-5bNA08tK.js} +2 -2
  240. package/dist/core/web/public/next/assets/{Surface-DWMtQe3Q.js.map → Surface-5bNA08tK.js.map} +1 -1
  241. package/dist/core/web/public/next/assets/{Swimlane-DcENqtdp.js → Swimlane-LrslsGxw.js} +2 -2
  242. package/dist/core/web/public/next/assets/{Swimlane-DcENqtdp.js.map → Swimlane-LrslsGxw.js.map} +1 -1
  243. package/dist/core/web/public/next/assets/{Switch-CenChEYf.js → Switch-BmCzfFnu.js} +2 -2
  244. package/dist/core/web/public/next/assets/{Switch-CenChEYf.js.map → Switch-BmCzfFnu.js.map} +1 -1
  245. package/dist/core/web/public/next/assets/{Tag-D4k5E3u0.js → Tag-WSUIaMVy.js} +2 -2
  246. package/dist/core/web/public/next/assets/{Tag-D4k5E3u0.js.map → Tag-WSUIaMVy.js.map} +1 -1
  247. package/dist/core/web/public/next/assets/{TerminalPane-DCQTukcE.js → TerminalPane-BVFgYX6K.js} +5 -5
  248. package/dist/core/web/public/next/assets/{TerminalPane-DCQTukcE.js.map → TerminalPane-BVFgYX6K.js.map} +1 -1
  249. package/dist/core/web/public/next/assets/{UniverseConsoleApp-DsuWT0pi.js → UniverseConsoleApp-BpphLqoz.js} +2 -2
  250. package/dist/core/web/public/next/assets/{UniverseConsoleApp-DsuWT0pi.js.map → UniverseConsoleApp-BpphLqoz.js.map} +1 -1
  251. package/dist/core/web/public/next/assets/{UniverseView-DyI5ApsA.js → UniverseView-DbpbSMwO.js} +4 -4
  252. package/dist/core/web/public/next/assets/{UniverseView-DyI5ApsA.js.map → UniverseView-DbpbSMwO.js.map} +1 -1
  253. package/dist/core/web/public/next/assets/{UsageSection-Imr28nwK.js → UsageSection-CUlaKtws.js} +3 -3
  254. package/dist/core/web/public/next/assets/{UsageSection-Imr28nwK.js.map → UsageSection-CUlaKtws.js.map} +1 -1
  255. package/dist/core/web/public/next/assets/VerseConsoleApp-DXDv1aOi.js +3 -0
  256. package/dist/core/web/public/next/assets/VerseConsoleApp-DXDv1aOi.js.map +1 -0
  257. package/dist/core/web/public/next/assets/{Workspace-CpxBO98E.js → Workspace-RXy0pqp6.js} +5 -5
  258. package/dist/core/web/public/next/assets/{Workspace-CpxBO98E.js.map → Workspace-RXy0pqp6.js.map} +1 -1
  259. package/dist/core/web/public/next/assets/{actions-DE8NoWV_.js → actions-BZTidCXZ.js} +2 -2
  260. package/dist/core/web/public/next/assets/{actions-DE8NoWV_.js.map → actions-BZTidCXZ.js.map} +1 -1
  261. package/dist/core/web/public/next/assets/{apps-model-CFGCUu9p.js → apps-model-CHO133qt.js} +2 -2
  262. package/dist/core/web/public/next/assets/{apps-model-CFGCUu9p.js.map → apps-model-CHO133qt.js.map} +1 -1
  263. package/dist/core/web/public/next/assets/{authority-model-CmnfJzmM.js → authority-model-8NeAEwAF.js} +2 -2
  264. package/dist/core/web/public/next/assets/{authority-model-CmnfJzmM.js.map → authority-model-8NeAEwAF.js.map} +1 -1
  265. package/dist/core/web/public/next/assets/{budget-queries-DjrPRFwl.js → budget-queries-DFF-3ICg.js} +2 -2
  266. package/dist/core/web/public/next/assets/{budget-queries-DjrPRFwl.js.map → budget-queries-DFF-3ICg.js.map} +1 -1
  267. package/dist/core/web/public/next/assets/{capacity-strip-model-C7cdQnuK.js → capacity-strip-model-5ViUgUlP.js} +2 -2
  268. package/dist/core/web/public/next/assets/{capacity-strip-model-C7cdQnuK.js.map → capacity-strip-model-5ViUgUlP.js.map} +1 -1
  269. package/dist/core/web/public/next/assets/{charts-DUD_jprE.js → charts-CzxWrZn7.js} +2 -2
  270. package/dist/core/web/public/next/assets/{charts-DUD_jprE.js.map → charts-CzxWrZn7.js.map} +1 -1
  271. package/dist/core/web/public/next/assets/cloud-model-CfRydq8N.js +2 -0
  272. package/dist/core/web/public/next/assets/{cloud-model-DoxBMDvx.js.map → cloud-model-CfRydq8N.js.map} +1 -1
  273. package/dist/core/web/public/next/assets/{cloud-queries-CTGvO-k-.js → cloud-queries-DmptbjS8.js} +2 -2
  274. package/dist/core/web/public/next/assets/{cloud-queries-CTGvO-k-.js.map → cloud-queries-DmptbjS8.js.map} +1 -1
  275. package/dist/core/web/public/next/assets/{command-catalog-CmrHNFzh.js → command-catalog-ffCAijVY.js} +2 -2
  276. package/dist/core/web/public/next/assets/{command-catalog-CmrHNFzh.js.map → command-catalog-ffCAijVY.js.map} +1 -1
  277. package/dist/core/web/public/next/assets/{context-model-2Owp2jYF.js → context-model-Bb15chFt.js} +2 -2
  278. package/dist/core/web/public/next/assets/{context-model-2Owp2jYF.js.map → context-model-Bb15chFt.js.map} +1 -1
  279. package/dist/core/web/public/next/assets/{context-model-9xywbXxX.js → context-model-CDeyP0cA.js} +2 -2
  280. package/dist/core/web/public/next/assets/{context-model-9xywbXxX.js.map → context-model-CDeyP0cA.js.map} +1 -1
  281. package/dist/core/web/public/next/assets/{context-queries-wvcRnV-I.js → context-queries-_9bFWb9v.js} +2 -2
  282. package/dist/core/web/public/next/assets/{context-queries-wvcRnV-I.js.map → context-queries-_9bFWb9v.js.map} +1 -1
  283. package/dist/core/web/public/next/assets/{control-queries-DB6Kh7UN.js → control-queries-CXtmZZH6.js} +2 -2
  284. package/dist/core/web/public/next/assets/{control-queries-DB6Kh7UN.js.map → control-queries-CXtmZZH6.js.map} +1 -1
  285. package/dist/core/web/public/next/assets/copy-setup-BIp0iaiZ.js +2 -0
  286. package/dist/core/web/public/next/assets/copy-setup-BIp0iaiZ.js.map +1 -0
  287. package/dist/core/web/public/next/assets/{dock-icons-BBUGl4tE.js → dock-icons-Cc43W7r4.js} +2 -2
  288. package/dist/core/web/public/next/assets/{dock-icons-BBUGl4tE.js.map → dock-icons-Cc43W7r4.js.map} +1 -1
  289. package/dist/core/web/public/next/assets/{fleet-model-BRfRWkZl.js → fleet-model-DOK8ZtEt.js} +2 -2
  290. package/dist/core/web/public/next/assets/{fleet-model-BRfRWkZl.js.map → fleet-model-DOK8ZtEt.js.map} +1 -1
  291. package/dist/core/web/public/next/assets/{fleet-queries-tU6UEXN2.js → fleet-queries-BeEAqD6y.js} +2 -2
  292. package/dist/core/web/public/next/assets/{fleet-queries-tU6UEXN2.js.map → fleet-queries-BeEAqD6y.js.map} +1 -1
  293. package/dist/core/web/public/next/assets/{focus-trap-BGOceFvj.js → focus-trap-ChZ5EFfM.js} +2 -2
  294. package/dist/core/web/public/next/assets/{focus-trap-BGOceFvj.js.map → focus-trap-ChZ5EFfM.js.map} +1 -1
  295. package/dist/core/web/public/next/assets/format-BmH2yWL6.js +2 -0
  296. package/dist/core/web/public/next/assets/format-BmH2yWL6.js.map +1 -0
  297. package/dist/core/web/public/next/assets/{git-model-53iRLInY.js → git-model-BFwAVbxF.js} +2 -2
  298. package/dist/core/web/public/next/assets/{git-model-53iRLInY.js.map → git-model-BFwAVbxF.js.map} +1 -1
  299. package/dist/core/web/public/next/assets/global-y_exIyAP.js +2 -0
  300. package/dist/core/web/public/next/assets/{global-CXHHMz9o.js.map → global-y_exIyAP.js.map} +1 -1
  301. package/dist/core/web/public/next/assets/{guarded-runners-B8zFVBVZ.js → guarded-runners-Cd4POBtI.js} +2 -2
  302. package/dist/core/web/public/next/assets/{guarded-runners-B8zFVBVZ.js.map → guarded-runners-Cd4POBtI.js.map} +1 -1
  303. package/dist/core/web/public/next/assets/{icons-D4s-jlxX.js → icons-CygCEFWs.js} +2 -2
  304. package/dist/core/web/public/next/assets/{icons-D4s-jlxX.js.map → icons-CygCEFWs.js.map} +1 -1
  305. package/dist/core/web/public/next/assets/{index-BG177cPN.js → index-CyK31YRm.js} +3 -3
  306. package/dist/core/web/public/next/assets/{index-BG177cPN.js.map → index-CyK31YRm.js.map} +1 -1
  307. package/dist/core/web/public/next/assets/{live-model-CEf01wrW.js → live-model-D7hilYFk.js} +2 -2
  308. package/dist/core/web/public/next/assets/live-model-D7hilYFk.js.map +1 -0
  309. package/dist/core/web/public/next/assets/{mind.module-D_xk6HIQ.js → mind.module-B15k0Y9q.js} +2 -2
  310. package/dist/core/web/public/next/assets/{mind.module-D_xk6HIQ.js.map → mind.module-B15k0Y9q.js.map} +1 -1
  311. package/dist/core/web/public/next/assets/mutations-CWlxEMEm.js +2 -0
  312. package/dist/core/web/public/next/assets/{mutations-CbTSofVH.js.map → mutations-CWlxEMEm.js.map} +1 -1
  313. package/dist/core/web/public/next/assets/{nav-CmZcbPFJ.js → nav-mimleI0R.js} +2 -2
  314. package/dist/core/web/public/next/assets/{nav-CmZcbPFJ.js.map → nav-mimleI0R.js.map} +1 -1
  315. package/dist/core/web/public/next/assets/{overnight-queries-CR0b_j-V.js → overnight-queries-DJ4mZKSj.js} +2 -2
  316. package/dist/core/web/public/next/assets/{overnight-queries-CR0b_j-V.js.map → overnight-queries-DJ4mZKSj.js.map} +1 -1
  317. package/dist/core/web/public/next/assets/primitives-DMijqcu0.js +1 -0
  318. package/dist/core/web/public/next/assets/{queries-rvRsAGW2.js → queries-Bw1McHTj.js} +2 -2
  319. package/dist/core/web/public/next/assets/{queries-rvRsAGW2.js.map → queries-Bw1McHTj.js.map} +1 -1
  320. package/dist/core/web/public/next/assets/{reasoning-pref-DMPQkujg.js → reasoning-pref-KkhI_5-j.js} +2 -2
  321. package/dist/core/web/public/next/assets/{reasoning-pref-DMPQkujg.js.map → reasoning-pref-KkhI_5-j.js.map} +1 -1
  322. package/dist/core/web/public/next/assets/{reveal-anchor-BKk2wZZZ.js → reveal-anchor-CCCx_kfT.js} +2 -2
  323. package/dist/core/web/public/next/assets/{reveal-anchor-BKk2wZZZ.js.map → reveal-anchor-CCCx_kfT.js.map} +1 -1
  324. package/dist/core/web/public/next/assets/{seat-subscription-CLn2ReSZ.js → seat-subscription-BhUUz7VL.js} +2 -2
  325. package/dist/core/web/public/next/assets/{seat-subscription-CLn2ReSZ.js.map → seat-subscription-BhUUz7VL.js.map} +1 -1
  326. package/dist/core/web/public/next/assets/{sidebar-model-CjQLvdag.js → sidebar-model-Dcd437cf.js} +2 -2
  327. package/dist/core/web/public/next/assets/{sidebar-model-CjQLvdag.js.map → sidebar-model-Dcd437cf.js.map} +1 -1
  328. package/dist/core/web/public/next/assets/{slots-DRxNosZg.js → slots-BL_J5mmV.js} +3 -3
  329. package/dist/core/web/public/next/assets/{slots-DRxNosZg.js.map → slots-BL_J5mmV.js.map} +1 -1
  330. package/dist/core/web/public/next/assets/{surface-data-DshUC_TU.js → surface-data-C3UAQ4QX.js} +2 -2
  331. package/dist/core/web/public/next/assets/{surface-data-DshUC_TU.js.map → surface-data-C3UAQ4QX.js.map} +1 -1
  332. package/dist/core/web/public/next/assets/surface-prefetch-BXKjPsV5.js +2 -0
  333. package/dist/core/web/public/next/assets/{surface-prefetch-DO8r4ttQ.js.map → surface-prefetch-BXKjPsV5.js.map} +1 -1
  334. package/dist/core/web/public/next/assets/{tasks-model-BKPFZAmZ.js → tasks-model-CzHvclVm.js} +2 -2
  335. package/dist/core/web/public/next/assets/{tasks-model-BKPFZAmZ.js.map → tasks-model-CzHvclVm.js.map} +1 -1
  336. package/dist/core/web/public/next/assets/{terminal-icons-DoeQkP3y.js → terminal-icons-BwgQTQP0.js} +2 -2
  337. package/dist/core/web/public/next/assets/{terminal-icons-DoeQkP3y.js.map → terminal-icons-BwgQTQP0.js.map} +1 -1
  338. package/dist/core/web/public/next/assets/types-IYKrYLku.js +2 -0
  339. package/dist/core/web/public/next/assets/types-IYKrYLku.js.map +1 -0
  340. package/dist/core/web/public/next/assets/{usage-queries-MnUeYzwp.js → usage-queries-pXxvy54x.js} +2 -2
  341. package/dist/core/web/public/next/assets/{usage-queries-MnUeYzwp.js.map → usage-queries-pXxvy54x.js.map} +1 -1
  342. package/dist/core/web/public/next/assets/{use-guarded-action-P12MfNwU.js → use-guarded-action-EgVWjM5c.js} +2 -2
  343. package/dist/core/web/public/next/assets/{use-guarded-action-P12MfNwU.js.map → use-guarded-action-EgVWjM5c.js.map} +1 -1
  344. package/dist/core/web/public/next/assets/{use-ticker-DIvAZ4Ey.js → use-ticker-BoqFxRFG.js} +2 -2
  345. package/dist/core/web/public/next/assets/{use-ticker-DIvAZ4Ey.js.map → use-ticker-BoqFxRFG.js.map} +1 -1
  346. package/dist/core/web/public/next/assets/{use-token-gate-DfEja7Wr.js → use-token-gate-DSNIIpI6.js} +2 -2
  347. package/dist/core/web/public/next/assets/{use-token-gate-DfEja7Wr.js.map → use-token-gate-DSNIIpI6.js.map} +1 -1
  348. package/dist/core/web/public/next/assets/{useOnboarding-CkOHadqC.js → useOnboarding-Bk0VnkxQ.js} +2 -2
  349. package/dist/core/web/public/next/assets/{useOnboarding-CkOHadqC.js.map → useOnboarding-Bk0VnkxQ.js.map} +1 -1
  350. package/dist/core/web/public/next/assets/{useSeatsRefresh-BDlbDlIU.js → useSeatsRefresh-q7Id9RcW.js} +2 -2
  351. package/dist/core/web/public/next/assets/{useSeatsRefresh-BDlbDlIU.js.map → useSeatsRefresh-q7Id9RcW.js.map} +1 -1
  352. package/dist/core/web/public/next/assets/{useVerseTranscript-BGtA55k2.js → useVerseTranscript-CqH51rt9.js} +2 -2
  353. package/dist/core/web/public/next/assets/{useVerseTranscript-BGtA55k2.js.map → useVerseTranscript-CqH51rt9.js.map} +1 -1
  354. package/dist/core/web/public/next/assets/{verse-icons-wOet9JZy.js → verse-icons-DsG1dOQx.js} +2 -2
  355. package/dist/core/web/public/next/assets/{verse-icons-wOet9JZy.js.map → verse-icons-DsG1dOQx.js.map} +1 -1
  356. package/dist/core/web/public/next/assets/{verse-model-CSzzdyu2.js → verse-model-JTuvRBVK.js} +2 -2
  357. package/dist/core/web/public/next/assets/{verse-model-CSzzdyu2.js.map → verse-model-JTuvRBVK.js.map} +1 -1
  358. package/dist/core/web/public/next/assets/{warmup-CecO_Nk0.js → warmup-Bv1RumVq.js} +3 -3
  359. package/dist/core/web/public/next/assets/{warmup-CecO_Nk0.js.map → warmup-Bv1RumVq.js.map} +1 -1
  360. package/dist/core/web/public/next/assets/why-seat-model-Cau8qG6O.js +2 -0
  361. package/dist/core/web/public/next/assets/why-seat-model-Cau8qG6O.js.map +1 -0
  362. package/dist/core/web/public/next/assets/{workspace-model-BdIVybKr.js → workspace-model-D-SLfads.js} +2 -2
  363. package/dist/core/web/public/next/assets/{workspace-model-BdIVybKr.js.map → workspace-model-D-SLfads.js.map} +1 -1
  364. package/dist/core/web/public/next/index.html +1 -1
  365. package/dist/release-dependency-inventory.json +1 -1
  366. package/docs/AUTONOMY-GAP.md +1 -1
  367. package/package.json +1 -1
  368. package/dist/core/web/public/next/assets/Advanced-CxzE6ekM.js +0 -2
  369. package/dist/core/web/public/next/assets/App-DFlp-vdv.js +0 -7
  370. package/dist/core/web/public/next/assets/AppsSection-Ct2O605-.js +0 -2
  371. package/dist/core/web/public/next/assets/AutonomyOffState-CXhXpsim.js +0 -2
  372. package/dist/core/web/public/next/assets/AutonomyOffState-CXhXpsim.js.map +0 -1
  373. package/dist/core/web/public/next/assets/AutonomyOffState-DmbE71vI.css +0 -1
  374. package/dist/core/web/public/next/assets/AutonomySection-BTg7yVa1.js +0 -2
  375. package/dist/core/web/public/next/assets/BudgetControl-ng6swoZp.js +0 -2
  376. package/dist/core/web/public/next/assets/BudgetControl-ng6swoZp.js.map +0 -1
  377. package/dist/core/web/public/next/assets/ChatSection-Bisj9R6E.js +0 -3
  378. package/dist/core/web/public/next/assets/ChatSection-Bisj9R6E.js.map +0 -1
  379. package/dist/core/web/public/next/assets/CommandSection-D8ZihzvE.js +0 -2
  380. package/dist/core/web/public/next/assets/CommandSection-D8ZihzvE.js.map +0 -1
  381. package/dist/core/web/public/next/assets/DockHost-BPqyhtCP.js +0 -2
  382. package/dist/core/web/public/next/assets/McpSection-D1tDvlRa.js +0 -2
  383. package/dist/core/web/public/next/assets/NeedsYouDrawer-D992XTOn.js +0 -2
  384. package/dist/core/web/public/next/assets/NeedsYouDrawer-D992XTOn.js.map +0 -1
  385. package/dist/core/web/public/next/assets/NeedsYouList-CruOt4JT.css +0 -1
  386. package/dist/core/web/public/next/assets/NeedsYouList-eBOlJr0b.js +0 -2
  387. package/dist/core/web/public/next/assets/OnboardingFlow-D3yfITK1.js +0 -2
  388. package/dist/core/web/public/next/assets/OnboardingFlow-D3yfITK1.js.map +0 -1
  389. package/dist/core/web/public/next/assets/OnboardingGate-DrDEVBqw.js +0 -3
  390. package/dist/core/web/public/next/assets/ResourcesChrome-C2ZrA7la.js +0 -3
  391. package/dist/core/web/public/next/assets/Sidebar-BuO3sViG.js +0 -3
  392. package/dist/core/web/public/next/assets/Sidebar-BuO3sViG.js.map +0 -1
  393. package/dist/core/web/public/next/assets/VerseConsoleApp-DeuBEvAO.js +0 -3
  394. package/dist/core/web/public/next/assets/VerseConsoleApp-DeuBEvAO.js.map +0 -1
  395. package/dist/core/web/public/next/assets/cloud-model-DoxBMDvx.js +0 -2
  396. package/dist/core/web/public/next/assets/copy-setup-CJQhFl-4.js +0 -2
  397. package/dist/core/web/public/next/assets/copy-setup-CJQhFl-4.js.map +0 -1
  398. package/dist/core/web/public/next/assets/format-DEAaVF6X.js +0 -2
  399. package/dist/core/web/public/next/assets/format-DEAaVF6X.js.map +0 -1
  400. package/dist/core/web/public/next/assets/global-CXHHMz9o.js +0 -2
  401. package/dist/core/web/public/next/assets/live-model-CEf01wrW.js.map +0 -1
  402. package/dist/core/web/public/next/assets/mutations-CbTSofVH.js +0 -2
  403. package/dist/core/web/public/next/assets/primitives-DNI-uJBd.js +0 -1
  404. package/dist/core/web/public/next/assets/surface-prefetch-DO8r4ttQ.js +0 -2
  405. package/dist/core/web/public/next/assets/types-D5XOdyw8.js +0 -2
  406. package/dist/core/web/public/next/assets/types-D5XOdyw8.js.map +0 -1
  407. package/dist/core/web/public/next/assets/why-seat-model-D68ZA7G8.js +0 -2
  408. package/dist/core/web/public/next/assets/why-seat-model-D68ZA7G8.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);
@@ -520,68 +639,219 @@ export function buildFleetRuleset(requiredChecks) {
520
639
  ],
521
640
  };
522
641
  }
523
- function ghJson(deps, args) {
524
- const result = deps.run('gh', args);
642
+ async function ghJson(deps, args) {
643
+ const result = await deps.run('gh', args);
525
644
  if (result.status !== 0)
526
645
  throw new Error(`gh ${args.slice(0, 3).join(' ')} failed: ${result.stderr.trim().slice(0, 200)}`);
527
646
  return JSON.parse(result.stdout || 'null');
528
647
  }
529
648
  /** 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`]);
649
+ /**
650
+ * 3.13: with `verifyAppId` (the repo is in the grant, the fleet can verify it
651
+ * and the ashlr-fleet App's id is known) the host-verified `ashlr/verify` is
652
+ * always required, pinned to the App — even when the default branch has no
653
+ * runs at all (Actions off), which is exactly the repo it exists for. A
654
+ * same-named run from another App never replaces the pin.
655
+ */
656
+ async function discoverChecks(deps, repo, branch, verifyAppId = null) {
657
+ const runs = await ghJson(deps, ['api', `repos/${repo}/commits/${encodeURIComponent(branch)}/check-runs?per_page=100`]);
534
658
  const found = [];
535
659
  for (const run of runs.check_runs ?? []) {
536
660
  if (typeof run.name !== 'string' || run.name.length === 0)
537
661
  continue;
662
+ if (verifyAppId !== null && run.name === ASHLR_VERIFY_CHECK)
663
+ continue;
538
664
  const appId = run.app?.id;
539
665
  found.push({ context: run.name, integrationId: typeof appId === 'number' && Number.isSafeInteger(appId) && appId > 0 ? appId : null });
540
666
  }
667
+ if (verifyAppId !== null)
668
+ found.push({ context: ASHLR_VERIFY_CHECK, integrationId: verifyAppId });
541
669
  return normalizeRequiredChecks(found);
542
670
  }
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;
671
+ /** The check the ashlr-fleet App posts on fleet PR heads (core/fleet/verify-check-run.ts ASHLR_VERIFY_CHECK_NAME). */
672
+ export const ASHLR_VERIFY_CHECK = 'ashlr/verify';
673
+ /** The ashlr-fleet App as GitHub describes it to your own gh auth (a `gh api` read); null = unreadable. */
674
+ export async function readFleetApp(deps) {
675
+ try {
676
+ const app = await ghJson(deps, ['api', `apps/${FLEET_APP_NAME}`]);
677
+ if (!app || typeof app.id !== 'number' || !Number.isSafeInteger(app.id) || app.id <= 0)
678
+ return null;
679
+ const slug = typeof app.slug === 'string' && /^[a-z0-9-]{1,100}$/.test(app.slug) ? app.slug : FLEET_APP_NAME;
680
+ const ownerLogin = typeof app.owner?.login === 'string' && /^[A-Za-z0-9-]{1,39}$/.test(app.owner.login) ? app.owner.login : '';
681
+ const checks = typeof app.permissions?.['checks'] === 'string' ? app.permissions['checks'] : null;
682
+ return { id: app.id, slug, ownerLogin, ownerIsOrg: app.owner?.type === 'Organization', checks };
683
+ }
684
+ catch {
685
+ return null;
686
+ }
687
+ }
688
+ /** PURE: where the App's permissions (and then each installation's acceptance) are changed. */
689
+ export function fleetAppPermissionUrls(app) {
690
+ const base = app.ownerIsOrg && app.ownerLogin
691
+ ? `https://github.com/organizations/${app.ownerLogin}/settings`
692
+ : 'https://github.com/settings';
693
+ return { permissions: `${base}/apps/${app.slug}/permissions`, installations: `${base}/installations` };
694
+ }
695
+ /** True when the fleet mirror of `repo` exists and the fleet detects at least one required verify command there. */
696
+ async function fleetCanVerify(repo) {
697
+ try {
698
+ const { mirrorPathFor } = await import('../core/fleet/mirrors.js');
699
+ const { detectVerifyCommands } = await import('../core/run/verify-commands.js');
700
+ const mirror = mirrorPathFor(repo);
701
+ if (!existsSync(mirror))
702
+ return false;
703
+ return detectVerifyCommands(mirror, 'merge').some((command) => command.required !== false);
704
+ }
705
+ catch {
706
+ return false;
707
+ }
708
+ }
709
+ /**
710
+ * PURE: `actual` (a ruleset as GitHub returns it) already says everything
711
+ * `desired` says. GitHub adds ids, links, timestamps and server-side rule
712
+ * defaults to what it returns, so extra object keys are ignored — but every
713
+ * field we set must match, and arrays must match one-to-one in any order, so
714
+ * an extra rule, an extra bypass actor or an extra required check is drift
715
+ * (re-applying would remove it). Anything unreadable is "not up to date".
716
+ */
717
+ export function rulesetMatches(desired, actual) {
718
+ if (Array.isArray(desired)) {
719
+ if (!Array.isArray(actual) || actual.length !== desired.length)
720
+ return false;
721
+ const unused = [...actual];
722
+ for (const wanted of desired) {
723
+ const at = unused.findIndex((candidate) => rulesetMatches(wanted, candidate));
724
+ if (at < 0)
725
+ return false;
726
+ unused.splice(at, 1);
555
727
  }
556
- repos = grantRepos.map((r) => r.nameWithOwner);
557
- enforcement = new Map(grantRepos.map((r) => [r.nameWithOwner, r.enforcement]));
728
+ return true;
729
+ }
730
+ if (desired !== null && typeof desired === 'object') {
731
+ if (actual === null || typeof actual !== 'object' || Array.isArray(actual))
732
+ return false;
733
+ const have = actual;
734
+ return Object.entries(desired).every(([key, value]) => rulesetMatches(value, have[key]));
558
735
  }
736
+ return Object.is(desired, actual);
737
+ }
738
+ /**
739
+ * The repos the fleet works in: the installed grant's, or — before the first
740
+ * grant — the ones it would name (every enrolled checkout's GitHub repo plus
741
+ * the canary; authority-api draftRepos), which are server-enforced until a
742
+ * grant says otherwise. Read-only: no draft is built and the ledger is not
743
+ * touched, so a dry run (and Verse's checklist) can ask it freely.
744
+ */
745
+ async function setupTargetRepos() {
746
+ const { readInstalledGrant, FLEET_CANARY_REPO } = await import('../core/authority/standing-grant.js');
747
+ const installed = readInstalledGrant();
748
+ if (installed.state === 'ok') {
749
+ return installed.envelope.payload.repos.map((repo) => ({ nameWithOwner: repo.nameWithOwner, enforcement: repo.enforcement }));
750
+ }
751
+ const byKey = new Map();
752
+ try {
753
+ const { readEnrollmentRegistry } = await import('../core/sandbox/policy.js');
754
+ const { repoIdentityOfPath } = await import('../core/fleet/repo-identity.js');
755
+ const registry = readEnrollmentRegistry();
756
+ if (registry.state === 'ready') {
757
+ for (const path of registry.repos) {
758
+ const nameWithOwner = repoIdentityOfPath(path);
759
+ if (nameWithOwner && !byKey.has(nameWithOwner.toLowerCase()))
760
+ byKey.set(nameWithOwner.toLowerCase(), { nameWithOwner, enforcement: 'server' });
761
+ }
762
+ }
763
+ }
764
+ catch {
765
+ // An unreadable registry leaves the canary alone on the list.
766
+ }
767
+ if (!byKey.has(FLEET_CANARY_REPO))
768
+ byKey.set(FLEET_CANARY_REPO, { nameWithOwner: FLEET_CANARY_REPO, enforcement: 'server' });
769
+ return [...byKey.values()];
770
+ }
771
+ /** Read-only: what `protect --apply` would do for each repo (every gh call here is a GET). */
772
+ async function protectPlans(parsed, deps, given) {
773
+ const explicit = parsed.values.get('--repo') ?? [];
774
+ const byGrant = given !== undefined || explicit.length === 0;
775
+ const targets = given
776
+ ?? (explicit.length > 0 ? explicit.map((nameWithOwner) => ({ nameWithOwner, enforcement: null })) : await setupTargetRepos());
777
+ // Repos the grant covers (installed grant, or the would-be grant's own list):
778
+ // only those get the fleet's ashlr/verify requirement.
779
+ const grantMembers = new Set();
780
+ const { readInstalledGrant } = await import('../core/authority/standing-grant.js');
781
+ const installed = readInstalledGrant();
782
+ if (installed.state === 'ok')
783
+ for (const r of installed.envelope.payload.repos)
784
+ grantMembers.add(r.nameWithOwner.toLowerCase());
785
+ if (byGrant)
786
+ for (const r of targets)
787
+ grantMembers.add(r.nameWithOwner.toLowerCase());
788
+ let fleetApp;
559
789
  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 });
790
+ for (const { nameWithOwner: repo, enforcement } of targets) {
791
+ if (enforcement === 'local') {
792
+ plans.push({ repo, skipped: 'local enforcement (private free-plan repo: GitHub rulesets unavailable) — the daemon enforces its gates', checks: [], ruleset: null, existingId: null, upToDate: false });
563
793
  continue;
564
794
  }
565
795
  try {
566
- const info = ghJson(deps, ['api', `repos/${repo}`]);
567
- const required = discoverChecks(deps, repo);
796
+ const info = await ghJson(deps, ['api', `repos/${repo}`]);
797
+ let verifyAppId = null;
798
+ let verifyNote = null;
799
+ if (grantMembers.has(repo.toLowerCase())) {
800
+ if (!(await fleetCanVerify(repo))) {
801
+ verifyNote = `${ASHLR_VERIFY_CHECK} not required: the fleet mirror has no verify command yet (rerun protect once it has synced)`;
802
+ }
803
+ else {
804
+ if (fleetApp === undefined)
805
+ fleetApp = await readFleetApp(deps);
806
+ if (fleetApp)
807
+ verifyAppId = fleetApp.id;
808
+ else
809
+ verifyNote = `${ASHLR_VERIFY_CHECK} not required: the ${FLEET_APP_NAME} App could not be read (\`gh api apps/${FLEET_APP_NAME}\`)`;
810
+ }
811
+ }
812
+ const required = await discoverChecks(deps, repo, info?.default_branch ?? 'main', verifyAppId);
568
813
  const checks = required.map((check) => check.context);
569
- const existing = ghJson(deps, ['api', `repos/${repo}/rulesets`]);
814
+ const existing = await ghJson(deps, ['api', `repos/${repo}/rulesets`]);
570
815
  const match = Array.isArray(existing) ? existing.find((r) => r.name === FLEET_RULESET_NAME) : undefined;
816
+ const existingId = typeof match?.id === 'number' && Number.isSafeInteger(match.id) ? match.id : null;
817
+ const ruleset = buildFleetRuleset(required);
818
+ // Rerun-safe: a ruleset already on GitHub with this exact content is
819
+ // reported as in place instead of being PUT again.
820
+ const upToDate = existingId !== null && rulesetMatches(ruleset, await ghJson(deps, ['api', `repos/${repo}/rulesets/${existingId}`]));
571
821
  plans.push({
572
822
  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,
823
+ skipped: info?.private === true && checks.length === 0 ? 'private repo with no CI checks — protect it by hand or keep it propose-only' : null,
574
824
  checks,
575
- ruleset: buildFleetRuleset(required),
576
- existingId: typeof match?.id === 'number' ? match.id : null,
825
+ ruleset,
826
+ existingId,
827
+ upToDate,
828
+ verifyNote,
577
829
  });
578
830
  }
579
831
  catch (error) {
580
- plans.push({ repo, skipped: `could not read it from GitHub: ${error.message}`, checks: [], ruleset: null, existingId: null });
832
+ plans.push({ repo, skipped: `could not read it from GitHub: ${error.message}`, checks: [], ruleset: null, existingId: null, upToDate: false });
581
833
  }
582
834
  }
583
835
  return plans;
584
836
  }
837
+ /** Create or update each plan's ruleset; the number that failed. */
838
+ async function applyProtectPlans(plans, deps) {
839
+ let failures = 0;
840
+ for (const plan of plans) {
841
+ const result = await deps.run('gh', [
842
+ 'api', '--method', plan.existingId === null ? 'POST' : 'PUT',
843
+ `repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`}`,
844
+ '--input', '-',
845
+ ], { input: JSON.stringify(plan.ruleset) });
846
+ if (result.status === 0)
847
+ deps.out(`${plan.repo}: ruleset ${plan.existingId === null ? 'created' : 'updated'}.`);
848
+ else {
849
+ failures += 1;
850
+ deps.err(`${plan.repo}: failed — ${result.stderr.trim().slice(0, 300)}`);
851
+ }
852
+ }
853
+ return failures;
854
+ }
585
855
  async function cmdProtect(parsed, deps) {
586
856
  const apply = parsed.flags.has('--apply');
587
857
  if (!apply && !parsed.flags.has('--print')) {
@@ -594,7 +864,17 @@ async function cmdProtect(parsed, deps) {
594
864
  deps.out(`${plan.repo}: skipped — ${plan.skipped}`);
595
865
  continue;
596
866
  }
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)'}`);
867
+ const checks = plan.checks.length > 0 ? plan.checks.join(', ') : 'none found (the repo goes to the owner lane until it has CI)';
868
+ if (plan.upToDate) {
869
+ deps.out(`${plan.repo}: ruleset ${plan.existingId} already up to date; required checks: ${checks}`);
870
+ continue;
871
+ }
872
+ deps.out(`${plan.repo}: ${plan.existingId === null ? 'create' : `update ruleset ${plan.existingId}`}; required checks: ${checks}`);
873
+ if (plan.verifyNote)
874
+ deps.out(` note: ${plan.verifyNote}`);
875
+ else if (plan.checks.includes(ASHLR_VERIFY_CHECK)) {
876
+ 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`);
877
+ }
598
878
  if (!apply) {
599
879
  deps.out(` gh api --method ${plan.existingId === null ? 'POST' : 'PUT'} repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`} --input - <<'JSON'`);
600
880
  deps.out(JSON.stringify(plan.ruleset, null, 2));
@@ -603,37 +883,27 @@ async function cmdProtect(parsed, deps) {
603
883
  }
604
884
  if (!apply)
605
885
  return 0;
606
- const actionable = plans.filter((plan) => !plan.skipped);
886
+ const actionable = plans.filter((plan) => !plan.skipped && !plan.upToDate);
607
887
  if (actionable.length === 0)
608
888
  return 0;
609
889
  if (!parsed.flags.has('--yes') && !(await deps.confirm(`Apply the ruleset to ${actionable.length} repo(s)?`))) {
610
890
  deps.out('Nothing applied.');
611
891
  return 1;
612
892
  }
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;
893
+ return (await applyProtectPlans(actionable, deps)) === 0 ? 0 : 1;
628
894
  }
629
895
  // ---------------------------------------------------------------------------
630
896
  // github-app — App Manifest flow (addendum §3)
631
897
  // ---------------------------------------------------------------------------
632
898
  export const FLEET_APP_NAME = 'ashlr-fleet';
633
899
  /**
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.
900
+ * PURE: the App manifest. Contents + pull requests read/write, checks
901
+ * read/write (3.13: the App posts the host-verified `ashlr/verify` check run
902
+ * on fleet PR heads — core/fleet/verify-check-run.ts), statuses / metadata
903
+ * read. No workflows, no administration — GitHub itself then refuses
904
+ * CI-config and protection changes from the fleet. An App created before 3.13
905
+ * has `checks: read`; `ashlr authority setup` detects that and prints where
906
+ * to raise it (fleetAppPermissionUrls).
637
907
  */
638
908
  export function buildGithubAppManifest(redirectUrl) {
639
909
  return {
@@ -642,7 +912,7 @@ export function buildGithubAppManifest(redirectUrl) {
642
912
  hook_attributes: { url: 'https://github.com/ashlrai/ashlr-hub', active: false },
643
913
  redirect_url: redirectUrl,
644
914
  public: false,
645
- default_permissions: { contents: 'write', pull_requests: 'write', checks: 'read', statuses: 'read', metadata: 'read' },
915
+ default_permissions: { contents: 'write', pull_requests: 'write', checks: 'write', statuses: 'read', metadata: 'read' },
646
916
  default_events: [],
647
917
  };
648
918
  }
@@ -729,26 +999,165 @@ async function cmdGithubApp(parsed, deps) {
729
999
  const result = await runGithubAppFlow(deps, { org });
730
1000
  deps.out(`Created ${result.slug} (App id ${result.appId}); its private key is in custody and was never written to disk.`);
731
1001
  deps.out(`Install it on the enrolled repos: ${result.installUrl}`);
1002
+ 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
1003
  return 0;
733
1004
  }
734
- // ---------------------------------------------------------------------------
735
- // rotate-provenance
736
- // ---------------------------------------------------------------------------
1005
+ /** Where Mason installs the App (and picks its repos). */
1006
+ export const FLEET_APP_INSTALL_URL = `https://github.com/apps/${FLEET_APP_NAME}/installations/new`;
1007
+ /** Bounded wait: a probe that does not answer in time is "unknown", never a hang. */
1008
+ async function withTimeout(work, ms, what) {
1009
+ let timer;
1010
+ try {
1011
+ return await Promise.race([
1012
+ work,
1013
+ new Promise((_, reject) => {
1014
+ timer = setTimeout(() => reject(new Error(`${what} did not answer within ${Math.round(ms / 1000)} s`)), ms);
1015
+ }),
1016
+ ]);
1017
+ }
1018
+ finally {
1019
+ if (timer)
1020
+ clearTimeout(timer);
1021
+ }
1022
+ }
1023
+ const APP_PROBE_TIMEOUT_MS = 20_000;
1024
+ /**
1025
+ * Where the ashlr-fleet App is installed — read-only, in two passes:
1026
+ *
1027
+ * 1. per owner, `gh api orgs/<owner>/installations` with Mason's own gh auth
1028
+ * (a GET; needs admin:org). An installation on "All repositories" settles
1029
+ * every repo of that owner; no installation at all settles them as missing.
1030
+ * 2. what that cannot settle (a user account, "Only select repositories", a
1031
+ * gh token without admin:org) is asked of the App itself: custody's
1032
+ * gh-token looks the repo up AS the App (GET /repos/<r>/installation under
1033
+ * the App's JWT), and a 404 there means "not installed". The one-hour token
1034
+ * the helper mints on success stays in this process's memory, is never
1035
+ * printed, and changes nothing on GitHub.
1036
+ */
1037
+ export async function probeAppInstallation(deps, repos) {
1038
+ const result = { installed: [], missing: [], unknown: [] };
1039
+ const byOwner = new Map();
1040
+ for (const repo of repos) {
1041
+ const owner = repo.split('/')[0];
1042
+ byOwner.set(owner, [...(byOwner.get(owner) ?? []), repo]);
1043
+ }
1044
+ const unsettled = [];
1045
+ for (const [owner, ownerRepos] of byOwner) {
1046
+ let listed = null;
1047
+ try {
1048
+ const read = await ghJson(deps, ['api', `orgs/${owner}/installations?per_page=100`]);
1049
+ if (read && Array.isArray(read.installations))
1050
+ listed = read.installations;
1051
+ }
1052
+ catch {
1053
+ listed = null;
1054
+ }
1055
+ const app = listed?.find((entry) => entry.app_slug === FLEET_APP_NAME);
1056
+ if (listed && !app)
1057
+ result.missing.push(...ownerRepos);
1058
+ else if (app?.repository_selection === 'all')
1059
+ result.installed.push(...ownerRepos);
1060
+ else
1061
+ unsettled.push(...ownerRepos);
1062
+ }
1063
+ // A few at a time: each lookup is two HTTPS calls inside the helper.
1064
+ const queue = [...unsettled];
1065
+ const worker = async () => {
1066
+ for (let repo = queue.shift(); repo !== undefined; repo = queue.shift()) {
1067
+ try {
1068
+ await withTimeout(deps.custody.githubToken(repo), APP_PROBE_TIMEOUT_MS, 'ashlr-custody gh-token');
1069
+ result.installed.push(repo);
1070
+ }
1071
+ catch (error) {
1072
+ const code = error.code;
1073
+ const message = error.message ?? String(error);
1074
+ if (code === 'github' && /not installed/u.test(message))
1075
+ result.missing.push(repo);
1076
+ else
1077
+ result.unknown.push({ repo, reason: message.slice(0, 160) });
1078
+ }
1079
+ }
1080
+ };
1081
+ await Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker));
1082
+ // Stable order for the checklist line, whatever order the lookups finished in.
1083
+ const order = new Map(repos.map((repo, i) => [repo, i]));
1084
+ const byInput = (a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0);
1085
+ result.installed.sort(byInput);
1086
+ result.missing.sort(byInput);
1087
+ result.unknown.sort((a, b) => byInput(a.repo, b.repo));
1088
+ return result;
1089
+ }
1090
+ /** "a, b and 3 more" — a list short enough for one checklist line. */
1091
+ function shortList(items, max = 3) {
1092
+ if (items.length <= max)
1093
+ return items.join(', ');
1094
+ return `${items.slice(0, max).join(', ')} and ${items.length - max} more`;
1095
+ }
1096
+ function provenanceRotationRecordPath(keyPath) {
1097
+ return `${keyPath}.rotation.json`;
1098
+ }
1099
+ function sha256Hex(bytes) {
1100
+ return createHash('sha256').update(bytes).digest('hex');
1101
+ }
1102
+ function readRotationRecord(keyPath) {
1103
+ try {
1104
+ const raw = JSON.parse(readFileSync(provenanceRotationRecordPath(keyPath), 'utf8'));
1105
+ if (raw.v !== 1 || typeof raw.rotatedAt !== 'string' || typeof raw.keySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(raw.keySha256))
1106
+ return null;
1107
+ return { v: 1, rotatedAt: raw.rotatedAt, keySha256: raw.keySha256 };
1108
+ }
1109
+ catch {
1110
+ return null;
1111
+ }
1112
+ }
1113
+ /** Read-only: never creates, repairs or moves the key. */
1114
+ export async function provenanceKeyState() {
1115
+ const { provenanceKeyPath, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
1116
+ let path;
1117
+ let key;
1118
+ try {
1119
+ path = provenanceKeyPath();
1120
+ key = loadExistingProvenanceKeyReadOnly();
1121
+ }
1122
+ catch (error) {
1123
+ return { state: 'unhealthy', reason: error.message.replace(homedir(), '~') };
1124
+ }
1125
+ if (!key)
1126
+ return { state: 'missing' };
1127
+ const record = readRotationRecord(path);
1128
+ // The record names the exact key it made: a key swapped in afterwards is not "already rotated".
1129
+ if (record && record.keySha256 === sha256Hex(key))
1130
+ return { state: 'rotated', rotatedAt: record.rotatedAt };
1131
+ return { state: 'unrotated' };
1132
+ }
737
1133
  /**
738
1134
  * Move the provenance HMAC key aside and create a new one. Every pending
739
1135
  * provenance signature stops verifying (by design: agents could read the old
740
1136
  * key while confinement was off), so pending proposals are re-verified.
1137
+ * Records which key it made (provenanceKeyState reads it) so setup can tell
1138
+ * a key it already rotated from the burned one.
741
1139
  */
742
1140
  export async function rotateProvenanceKey() {
743
- const { provenanceKeyPath, loadOrCreateKey } = await import('../core/foundry/provenance.js');
1141
+ const { provenanceKeyPath, loadOrCreateKey, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
744
1142
  const path = provenanceKeyPath();
745
1143
  let retiredAs = null;
746
1144
  if (existsSync(path)) {
747
1145
  retiredAs = `${path}.retired-${new Date().toISOString().replace(/[:.]/g, '-')}`;
748
1146
  renameSync(path, retiredAs);
749
1147
  }
750
- loadOrCreateKey();
751
- return { retiredAs };
1148
+ const created = loadOrCreateKey();
1149
+ // loadOrCreateKey falls back to a process-lifetime key when it cannot write
1150
+ // one; that is not a rotation — say so instead of recording it.
1151
+ const stored = loadExistingProvenanceKeyReadOnly();
1152
+ if (!stored || !stored.equals(created))
1153
+ throw new Error('the new provenance key could not be stored in ~/.ashlr/foundry');
1154
+ const rotatedAt = new Date().toISOString();
1155
+ const record = { v: 1, rotatedAt, keySha256: sha256Hex(stored) };
1156
+ const target = provenanceRotationRecordPath(path);
1157
+ const tmp = `${target}.${process.pid}.tmp`;
1158
+ writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
1159
+ renameSync(tmp, target);
1160
+ return { retiredAs, rotatedAt };
752
1161
  }
753
1162
  async function cmdRotateProvenance(parsed, deps) {
754
1163
  if (!parsed.flags.has('--yes') && !(await deps.confirm('Replace the provenance HMAC key? Pending proposals will need re-verification.'))) {
@@ -840,12 +1249,41 @@ const SETUP_STEP_NEEDS = Object.freeze({
840
1249
  'standing-grant': ['touch-id'],
841
1250
  'autonomy-switch': [],
842
1251
  'daemon-service': ['terminal'],
843
- 'resident-runtime': [],
1252
+ 'resident-runtime': ['terminal'],
844
1253
  });
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.';
1254
+ /** The one command that walks every step and resumes where the last run stopped. */
1255
+ export const AUTHORITY_SETUP_COMMAND = 'ashlr authority setup';
1256
+ /** The only command that installs or restarts the resident service (you, at a terminal, under the grant). */
1257
+ const RESIDENT_START_COMMAND = 'ashlr authority resident start';
1258
+ /**
1259
+ * The command that moves each step on while it is not in place. Setup is
1260
+ * rerun-safe, so for everything it performs itself the answer is setup; the
1261
+ * rest is what only Mason runs (sudo, the build, the grant, resident start).
1262
+ * A row may name its own (the resident step names what its admission asks
1263
+ * for, or none when a prerequisite outside this Mac's commands is missing).
1264
+ */
1265
+ const SETUP_STEP_COMMAND = Object.freeze({
1266
+ 'custody-helper': 'sudo scripts/install-custody.sh',
1267
+ 'host-binding': 'sudo scripts/install-custody.sh',
1268
+ 'signing-key': AUTHORITY_SETUP_COMMAND,
1269
+ 'trust-root': AUTHORITY_SETUP_COMMAND,
1270
+ deploy: 'npm run build',
1271
+ 'github-app': AUTHORITY_SETUP_COMMAND,
1272
+ 'claude-token': AUTHORITY_SETUP_COMMAND,
1273
+ 'canary-repo': AUTHORITY_SETUP_COMMAND,
1274
+ rulesets: AUTHORITY_SETUP_COMMAND,
1275
+ 'old-activation-state': AUTHORITY_SETUP_COMMAND,
1276
+ 'provenance-key': AUTHORITY_SETUP_COMMAND,
1277
+ 'standing-grant': 'ashlr authority grant',
1278
+ 'autonomy-switch': 'ashlr authority switch autonomous',
1279
+ 'daemon-service': RESIDENT_START_COMMAND,
1280
+ 'resident-runtime': RESIDENT_START_COMMAND,
1281
+ });
1282
+ // Every exit before the grant is known to be active (the first unmet live
1283
+ // prerequisite, or a dry run past one) reports the resident step with this:
1284
+ // a resident service is admitted only under an active standing grant
1285
+ // (docs/RESIDENT-RUNTIME.md), so without one it is genuinely blocked.
1286
+ 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
1287
  function setupStepId(step) {
850
1288
  return step.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
851
1289
  }
@@ -854,7 +1292,17 @@ export function authoritySetupReport(report, dryRun) {
854
1292
  const count = (status) => report.filter((r) => r.status === status).length;
855
1293
  const steps = report.map((r) => {
856
1294
  const id = setupStepId(r.step);
857
- return { id, step: r.step, status: r.status, detail: r.detail, needs: SETUP_STEP_NEEDS[id] ?? [] };
1295
+ // A step in place needs nothing; one that is not names what moves it on.
1296
+ const open = r.status !== 'done' && r.status !== 'already';
1297
+ return {
1298
+ id,
1299
+ step: r.step,
1300
+ status: r.status,
1301
+ detail: r.detail,
1302
+ needs: SETUP_STEP_NEEDS[id] ?? [],
1303
+ command: open ? (r.command !== undefined ? r.command : SETUP_STEP_COMMAND[id] ?? null) : null,
1304
+ link: open ? r.link ?? null : null,
1305
+ };
858
1306
  });
859
1307
  const open = steps.find((s) => s.status !== 'done' && s.status !== 'already');
860
1308
  return {
@@ -868,7 +1316,6 @@ export function authoritySetupReport(report, dryRun) {
868
1316
  }
869
1317
  async function cmdSetup(parsed, deps) {
870
1318
  const dryRun = parsed.flags.has('--dry-run');
871
- const yes = parsed.flags.has('--yes');
872
1319
  const json = parsed.flags.has('--json');
873
1320
  if (json && !dryRun) {
874
1321
  // A real run asks questions and prints what gh / signing did; only the
@@ -876,24 +1323,119 @@ async function cmdSetup(parsed, deps) {
876
1323
  deps.err('setup --json needs --dry-run');
877
1324
  return 2;
878
1325
  }
1326
+ const report = await runSetup(parsed, deps, { dryRun, yes: parsed.flags.has('--yes'), print: !json });
1327
+ return finish(report, deps, dryRun, json);
1328
+ }
1329
+ // ---------------------------------------------------------------------------
1330
+ // The read-only checklist (Verse: GET /api/verse/authority/setup)
1331
+ // ---------------------------------------------------------------------------
1332
+ /** Per `gh api` GET in the read-only probe. */
1333
+ const READ_ONLY_GH_TIMEOUT_MS = 10_000;
1334
+ /** All `gh api` reads of one read-only probe together. */
1335
+ const READ_ONLY_PROBE_BUDGET_MS = 30_000;
1336
+ const GH_API_PATH_RE = /^[A-Za-z0-9][A-Za-z0-9._~/?=&%-]{0,400}$/u;
1337
+ /**
1338
+ * The read-only probe's runner: `gh api <path>` — a GET, since no field,
1339
+ * method or input flag is ever passed — run asynchronously (the Verse server
1340
+ * must not block its event loop) and killed after READ_ONLY_GH_TIMEOUT_MS.
1341
+ * Anything else (git, `gh pr`, `gh repo create`, `--method`) is refused
1342
+ * without running, so a dry-run path that ever grew a write could not reach it.
1343
+ */
1344
+ /** PURE: `gh api <path>` with nothing else — the only call the read-only probe runs. */
1345
+ export function isReadOnlyGhApiCall(bin, args) {
1346
+ return bin === 'gh' && args.length === 2 && args[0] === 'api' && GH_API_PATH_RE.test(args[1] ?? '');
1347
+ }
1348
+ export function readOnlyGhRun(bin, args) {
1349
+ if (!isReadOnlyGhApiCall(bin, args)) {
1350
+ return Promise.resolve({ status: 1, stdout: '', stderr: 'refused: the read-only setup probe only runs `gh api <path>` reads' });
1351
+ }
1352
+ return new Promise((done) => {
1353
+ execFile('gh', ['api', args[1]], {
1354
+ encoding: 'utf8',
1355
+ timeout: READ_ONLY_GH_TIMEOUT_MS,
1356
+ maxBuffer: 8 * 1024 * 1024,
1357
+ env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' },
1358
+ }, (error, stdout, stderr) => {
1359
+ const code = error?.code;
1360
+ done({
1361
+ status: error ? (typeof code === 'number' ? code : 1) : 0,
1362
+ stdout: stdout ?? '',
1363
+ stderr: stderr || (error ? error.message : ''),
1364
+ });
1365
+ });
1366
+ });
1367
+ }
1368
+ function refusedInProbe(what) {
1369
+ throw new Error(`${what} is not available to the read-only setup probe`);
1370
+ }
1371
+ /**
1372
+ * `ashlr authority setup --dry-run --json` as a function: the same planning
1373
+ * path (cmdSetup's steps with --dry-run), with dependencies that cannot ask,
1374
+ * prompt, open a browser, reach the network directly or run anything but a
1375
+ * bounded `gh api` GET. Custody is only asked read-only questions (status,
1376
+ * host binding, public key, and whether the App can see a repo).
1377
+ */
1378
+ export async function planAuthoritySetup(injected) {
1379
+ // The whole probe gets READ_ONLY_PROBE_BUDGET_MS of gh time: past it, a read
1380
+ // is answered "not read" at once (the step then says what it could not
1381
+ // check) instead of queueing more ten-second waits behind a slow network.
1382
+ const deadline = Date.now() + READ_ONLY_PROBE_BUDGET_MS;
1383
+ const budgeted = (bin, args) => (Date.now() > deadline
1384
+ ? Promise.resolve({ status: 1, stdout: '', stderr: 'not read: the setup probe ran out of time' })
1385
+ : readOnlyGhRun(bin, args));
1386
+ const deps = {
1387
+ out: () => undefined,
1388
+ err: () => undefined,
1389
+ confirm: async () => false,
1390
+ readSecret: async () => refusedInProbe('a secret prompt'),
1391
+ run: budgeted,
1392
+ openBrowser: () => refusedInProbe('a browser'),
1393
+ fetch: (async () => refusedInProbe('the network')),
1394
+ custody: injected?.custody ?? await import('../core/authority/custody-client.js'),
1395
+ daemonService: () => realDaemonService(true),
1396
+ // The resident step only reads (admission verdict, service + plist
1397
+ // observation); the effects are refused here, never reached by setup.
1398
+ resident: {
1399
+ admission: realResidentDeps.admission,
1400
+ observe: realResidentDeps.observe,
1401
+ operatorRefusal: async () => 'the read-only setup probe is not the operator at a terminal',
1402
+ start: async () => refusedInProbe('resident start'),
1403
+ stop: async () => refusedInProbe('resident stop'),
1404
+ },
1405
+ ...(injected ?? {}),
1406
+ };
1407
+ const parsed = { positional: ['setup'], flags: new Set(['--dry-run', '--json']), values: new Map() };
1408
+ return authoritySetupReport(await runSetup(parsed, deps, { dryRun: true, yes: false, print: false }), true);
1409
+ }
1410
+ /**
1411
+ * Every step of setup, in order. RERUN-SAFE: Mason runs it several times, so
1412
+ * each step first checks whether it is already in place and says so
1413
+ * ('already') instead of doing it again — the provenance key is rotated once,
1414
+ * a ruleset GitHub already holds is not PUT again, the App counts only when it
1415
+ * is installed, an open trust-root PR is found rather than reopened. A step
1416
+ * that throws is recorded as failed and the run still ends with its summary.
1417
+ */
1418
+ async function runSetup(parsed, deps, opts) {
1419
+ const { dryRun, yes } = opts;
879
1420
  const report = [];
880
- const note = (step, status, detail) => {
881
- report.push({ step, status, detail });
882
- if (json)
1421
+ const note = (step, status, detail, extra = {}) => {
1422
+ report.push({ step, status, detail, ...extra });
1423
+ if (!opts.print)
883
1424
  return;
884
1425
  // 'skipped' gets its own glyph: a ✓ on a step a dry run did not perform read as done.
885
1426
  const glyph = status === 'failed' ? '✗' : status === 'blocked' ? '×' : status === 'waiting-on-you' ? '…' : status === 'skipped' ? '·' : '✓';
886
1427
  deps.out(`${glyph} ${step}: ${detail}`);
887
1428
  };
888
1429
  const done = () => {
889
- // Every exit, including the first unmet live prerequisite, must report
890
- // this independent source/release block exactly once.
1430
+ // Every exit, including the first unmet live prerequisite, reports the
1431
+ // resident step exactly once — it is never silently complete.
891
1432
  if (!report.some((row) => row.step === 'resident runtime')) {
892
- note('resident runtime', 'blocked', RESIDENT_RUNTIME_BLOCK);
1433
+ note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
893
1434
  }
894
- return finish(report, deps, dryRun, json);
1435
+ return report;
895
1436
  };
896
1437
  const ask = async (question) => !dryRun && (yes || deps.confirm(question));
1438
+ const failure = (error) => (error?.message ?? String(error)).replace(homedir(), '~').slice(0, 300);
897
1439
  // --dry-run PLANS: the CHANGELOG and docs/AUTHORITY.md promise it "prints
898
1440
  // every step first" (3.10 review c20). A real run must stop at an unmet
899
1441
  // prerequisite; a dry run notes it and keeps describing the remaining
@@ -907,256 +1449,550 @@ async function cmdSetup(parsed, deps) {
907
1449
  planning = true;
908
1450
  return false;
909
1451
  };
910
- // 1. Custody helper (installing it needs sudo — Mason runs that himself).
911
- let custody = null;
1452
+ // Which step is running: an exception nobody anticipated becomes its
1453
+ // failure (with the summary still printed), never a bare abort.
1454
+ let current = 'custody helper';
912
1455
  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 {
1456
+ // 1. Custody helper (installing it needs sudo — Mason runs that himself).
1457
+ let custody = null;
934
1458
  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)}…`);
1459
+ custody = await deps.custody.custodyStatus();
944
1460
  }
945
1461
  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`);
1462
+ note('custody helper', 'failed', `status unavailable (${failure(error)})`);
1463
+ }
1464
+ if (!custody || !custody.installed) {
1465
+ note('custody helper', 'waiting-on-you', 'run `sudo scripts/install-custody.sh` from the ashlr-hub checkout, then run setup again');
963
1466
  if (stopHere())
964
1467
  return done();
965
1468
  }
966
1469
  else {
967
- const info = await deps.custody.custodyInit();
968
- keyId = info.keyId;
969
- publicKeyPem = info.publicKeyPem;
970
- note('signing key', 'done', `created ${info.keyId}`);
1470
+ note('custody helper', 'already', `installed${custody.version ? ` (${custody.version})` : ''}`);
971
1471
  }
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) {
1472
+ // 1b. The helper and this CLI must agree on which Mac this is: the helper
1473
+ // derives StandingGrantV1.hostBinding as sha256(uppercase IOPlatformUUID)
1474
+ // and so does surface.ts. A disagreement would make every grant fail
1475
+ // "signed for a different Mac" only AFTER Mason's Touch ID — catch it first.
1476
+ current = 'host binding';
1477
+ if (planning) {
1478
+ note('host binding', 'skipped', 'would check that ashlr-custody and this release agree on which Mac this is');
1479
+ }
1480
+ else {
993
1481
  try {
994
- const info = await deps.custody.custodyPublicKey();
995
- keyId = info.keyId;
996
- publicKeyPem = info.publicKeyPem;
1482
+ const { currentHostBinding } = await import('../core/authority/surface.js');
1483
+ const ours = currentHostBinding();
1484
+ const helpers = await deps.custody.custodyHostBinding();
1485
+ if (ours === null)
1486
+ note('host binding', 'failed', "this Mac's IOPlatformUUID could not be read, so no grant can be bound to it");
1487
+ else if (ours !== helpers)
1488
+ note('host binding', 'failed', 'ashlr-custody and this release disagree on the host binding — reinstall the helper from this checkout');
1489
+ else
1490
+ note('host binding', 'already', `this Mac is ${ours.slice(0, 12)}…`);
997
1491
  }
998
1492
  catch (error) {
999
- note('trust root', 'failed', `could not read the custody public key (${error.message})`);
1000
- trustRootFailed = true;
1493
+ note('host binding', 'failed', `could not compare with ashlr-custody (${failure(error)})`);
1001
1494
  }
1002
1495
  }
1003
- if (trustRootFailed) {
1004
- if (stopHere())
1005
- return done();
1496
+ if (report.some((r) => r.step === 'host binding' && r.status === 'failed') && stopHere())
1497
+ return done();
1498
+ // 2. Secure Enclave key (Touch ID). The helper is not on PATH, so every
1499
+ // message names it by its installed path.
1500
+ current = 'signing key';
1501
+ let keyId = custody?.keyId ?? null;
1502
+ let publicKeyPem = null;
1503
+ if (planning || !custody) {
1504
+ note('signing key', 'skipped', 'would create the Secure Enclave signing key (Touch ID) unless it already exists');
1006
1505
  }
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');
1506
+ else if (!custody.keyInitialized) {
1507
+ if (!(await ask('Create the Secure Enclave signing key now (Touch ID)?'))) {
1508
+ note('signing key', dryRun ? 'skipped' : 'waiting-on-you', dryRun
1509
+ ? `would create it (Touch ID) — or run \`${CUSTODY_HELPER_PATH} init\``
1510
+ : `create it with \`${CUSTODY_HELPER_PATH} init\` or rerun setup`);
1511
+ if (stopHere())
1512
+ return done();
1513
+ }
1514
+ else {
1515
+ try {
1516
+ const info = await deps.custody.custodyInit();
1517
+ keyId = info.keyId;
1518
+ publicKeyPem = info.publicKeyPem;
1519
+ note('signing key', 'done', `created ${info.keyId}`);
1520
+ }
1521
+ catch (error) {
1522
+ note('signing key', 'failed', `not created (${failure(error)}) — rerun setup, or run \`${CUSTODY_HELPER_PATH} init\``);
1523
+ if (stopHere())
1524
+ return done();
1525
+ }
1526
+ }
1527
+ }
1528
+ else {
1529
+ note('signing key', 'already', `key ${keyId ?? '(id unknown)'}`);
1530
+ }
1531
+ // 3. Trust root compiled in (Mason's own PR).
1532
+ current = 'trust root';
1533
+ const { STANDING_GRANT_TRUST_ROOTS } = await import('../core/authority/trust-roots.js');
1534
+ // A key made on an earlier run whose id status did not report: read its
1535
+ // public half back (`ashlr-custody pubkey`, no Touch ID) instead of asking
1536
+ // Mason to copy it by hand.
1537
+ const isTrusted = (id) => id !== null && STANDING_GRANT_TRUST_ROOTS.some((root) => root.keyId === id);
1538
+ // No kickstart here: a daemon that is disabled or not loaded (the usual
1539
+ // state before the first grant) has nothing to kickstart, and the last step
1540
+ // (daemon service) says how to start it once the grant is in force.
1541
+ 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`)';
1542
+ if (planning) {
1543
+ 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');
1544
+ note('deploy', 'skipped', `would wait for you: ${DEPLOY_DETAIL}`);
1010
1545
  }
1011
1546
  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);
1547
+ let trustRootFailed = false;
1548
+ if (!isTrusted(keyId) && !publicKeyPem) {
1549
+ try {
1550
+ const info = await deps.custody.custodyPublicKey();
1551
+ keyId = info.keyId;
1552
+ publicKeyPem = info.publicKeyPem;
1553
+ }
1554
+ catch (error) {
1555
+ note('trust root', 'failed', `could not read the custody public key (${failure(error)})`);
1556
+ trustRootFailed = true;
1557
+ }
1558
+ }
1559
+ if (trustRootFailed) {
1560
+ if (stopHere())
1561
+ return done();
1562
+ }
1563
+ else if (isTrusted(keyId)) {
1564
+ note('trust root', 'already', `${keyId} is compiled into this release`);
1565
+ note('deploy', 'already', 'this release carries your trust root');
1016
1566
  }
1017
1567
  else {
1018
- let pr;
1568
+ const root = { keyId: keyId, alg: 'ES256', publicKeyPem: publicKeyPem };
1569
+ const invalid = validateCustodyRoot(root, deps.custody.keyIdForPublicKeyPem);
1570
+ if (invalid) {
1571
+ note('trust root', 'failed', invalid);
1572
+ }
1573
+ else {
1574
+ let pr;
1575
+ try {
1576
+ // A dry run looks too, through `gh api` reads only (the
1577
+ // Verse checklist is this path): an open PR, a merged key or a
1578
+ // pushed branch is reported instead of "add this root yourself".
1579
+ pr = (dryRun ? await probeTrustRootReadOnly(deps, root) : null) ?? await openTrustRootPr(parsed, deps, root, ask, !dryRun);
1580
+ }
1581
+ catch (error) {
1582
+ pr = { status: 'failed', detail: failure(error) };
1583
+ }
1584
+ note('trust root', pr.status, pr.detail, {
1585
+ ...(pr.link ? { link: pr.link } : {}),
1586
+ ...(pr.command !== undefined ? { command: pr.command } : {}),
1587
+ });
1588
+ note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
1589
+ }
1590
+ if (stopHere())
1591
+ return done();
1592
+ }
1593
+ }
1594
+ // The canary's existence decides whether the App and ruleset checks
1595
+ // include it yet (setup creates it at step 6). One read-only GET.
1596
+ const canaryRepo = 'ashlrai/fleet-canary';
1597
+ let canaryExists = planning ? false : (await deps.run('gh', ['api', `repos/${canaryRepo}`])).status === 0;
1598
+ // 4. GitHub App: its key in custody is half of it — it also has to be
1599
+ // INSTALLED on the repos the fleet works in, or every mint fails.
1600
+ current = 'GitHub App';
1601
+ if (!custody?.githubApp) {
1602
+ if (await ask(`Create the ${FLEET_APP_NAME} GitHub App now (one browser page)?`)) {
1019
1603
  try {
1020
- pr = await openTrustRootPr(parsed, deps, root, ask, !dryRun);
1604
+ const app = await runGithubAppFlow(deps, { org: one(parsed, '--org') ?? 'ashlrai' });
1605
+ // A new App is installed nowhere yet.
1606
+ 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
1607
  }
1022
1608
  catch (error) {
1023
- pr = { status: 'failed', detail: error.message };
1609
+ note('GitHub App', 'failed', `not created (${failure(error)}) — rerun setup, or run \`ashlr authority github-app\``);
1024
1610
  }
1025
- note('trust root', pr.status, pr.detail);
1026
- note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
1027
1611
  }
1028
- if (stopHere())
1029
- return done();
1612
+ else
1613
+ note('GitHub App', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority github-app`');
1614
+ }
1615
+ else if (planning) {
1616
+ note('GitHub App', 'skipped', `the ${FLEET_APP_NAME} key is in custody; would check the App is installed on the enrolled repos`);
1030
1617
  }
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
1618
  else {
1049
- await deps.custody.storeClaudeToken(token);
1050
- note('Claude token', 'done', 'stored in custody (never written to disk here)');
1619
+ const targets = (await setupTargetRepos()).map((repo) => repo.nameWithOwner)
1620
+ .filter((repo) => canaryExists || repo.toLowerCase() !== canaryRepo);
1621
+ const probe = await probeAppInstallation(deps, targets);
1622
+ const later = canaryExists ? '' : ` (${canaryRepo} needs it too once it exists)`;
1623
+ if (probe.missing.length > 0) {
1624
+ 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 });
1625
+ }
1626
+ else if (probe.unknown.length > 0) {
1627
+ 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 });
1628
+ }
1629
+ else {
1630
+ 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`;
1631
+ // 3.13: it must also be able to post ashlr/verify (checks: write); an
1632
+ // App created before 3.13 has checks: read and must be raised by hand.
1633
+ const app = await readFleetApp(deps);
1634
+ if (app && app.checks !== 'write') {
1635
+ const urls = fleetAppPermissionUrls(app);
1636
+ 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'}): `
1637
+ + `set "Checks: Read and write" at ${urls.permissions}, accept the new permission on each installation at ${urls.installations}, then rerun setup`, { link: urls.permissions });
1638
+ }
1639
+ else {
1640
+ note('GitHub App', 'already', app
1641
+ ? `the ${FLEET_APP_NAME} key is in custody; ${where}${later}; the App can post ${ASHLR_VERIFY_CHECK} (checks: write)`
1642
+ : `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})`);
1643
+ }
1644
+ }
1645
+ }
1646
+ // 5. Claude token for restricted judge / Leader calls.
1647
+ current = 'Claude token';
1648
+ if (custody?.claudeToken)
1649
+ note('Claude token', 'already', 'stored in custody');
1650
+ else if (await ask('Store a Claude token now (run `claude setup-token` in another terminal and paste it)?')) {
1651
+ try {
1652
+ const token = (await deps.readSecret('Paste the token (hidden): ')).trim();
1653
+ if (!token)
1654
+ note('Claude token', 'failed', 'nothing was pasted');
1655
+ else {
1656
+ await deps.custody.storeClaudeToken(token);
1657
+ note('Claude token', 'done', 'stored in custody (never written to disk here)');
1658
+ }
1659
+ }
1660
+ catch (error) {
1661
+ note('Claude token', 'failed', `not stored (${failure(error)}) — run \`claude setup-token\`, then rerun setup`);
1662
+ }
1663
+ }
1664
+ else
1665
+ note('Claude token', dryRun ? 'skipped' : 'waiting-on-you', 'run `claude setup-token`, then rerun setup');
1666
+ // 6. Canary repo (the App cannot write workflows, so this uses your own gh
1667
+ // auth). Before the rulesets: the grant's server-enforced repos include the
1668
+ // canary, and a ruleset cannot be applied to a repo that does not exist yet.
1669
+ current = 'canary repo';
1670
+ if (canaryExists)
1671
+ note('canary repo', 'already', `${canaryRepo} exists`);
1672
+ else if (await ask(`Create the public repo ${canaryRepo} with its CI workflow (your gh auth)?`)) {
1673
+ const created = await deps.run('gh', ['repo', 'create', canaryRepo, '--public', '--add-readme', '--description', 'ashlr fleet canary (revert drills)']);
1674
+ const workflow = created.status === 0
1675
+ ? await deps.run('gh', ['api', '--method', 'PUT', `repos/${canaryRepo}/contents/.github/workflows/ci.yml`, '--input', '-'], {
1676
+ input: JSON.stringify({ message: 'ci: canary workflow', content: Buffer.from(CANARY_WORKFLOW, 'utf8').toString('base64') }),
1677
+ })
1678
+ : null;
1679
+ canaryExists = created.status === 0;
1680
+ 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));
1681
+ }
1682
+ else
1683
+ note('canary repo', dryRun ? 'skipped' : 'waiting-on-you', `create ${canaryRepo} with a CI workflow`);
1684
+ // 7. Rulesets — only where GitHub does not already hold exactly this one.
1685
+ current = 'rulesets';
1686
+ if (planning) {
1687
+ note('rulesets', 'skipped', 'would check the fleet ruleset on the server-enforced repos and apply it where it is missing or different');
1688
+ }
1689
+ else {
1690
+ const targets = (await setupTargetRepos()).filter((repo) => canaryExists || repo.nameWithOwner.toLowerCase() !== canaryRepo);
1691
+ const plans = await protectPlans(parsed, deps, targets);
1692
+ const stale = plans.filter((plan) => !plan.skipped && !plan.upToDate);
1693
+ const inPlace = plans.filter((plan) => !plan.skipped && plan.upToDate).length;
1694
+ const unreadable = plans.filter((plan) => plan.skipped?.startsWith('could not read')).map((plan) => plan.repo);
1695
+ const byHand = plans.filter((plan) => plan.skipped && !plan.skipped.startsWith('could not read')).length;
1696
+ const unread = unreadable.length > 0 ? `; could not read ${shortList(unreadable)} from GitHub` : '';
1697
+ const left = byHand > 0 ? `; ${byHand} left to you (local enforcement or no CI)` : '';
1698
+ if (plans.length === 0) {
1699
+ // Nothing to look at yet — never "in place on 0 repos".
1700
+ if (canaryExists)
1701
+ note('rulesets', 'already', 'no server-enforced repo needs one');
1702
+ else
1703
+ note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${canaryRepo} gets the fleet ruleset once it exists`);
1704
+ }
1705
+ else if (stale.length > 0) {
1706
+ const where = shortList(stale.map((plan) => plan.repo));
1707
+ if (await ask(`Apply the fleet ruleset to ${stale.length} repo(s) where it is missing or different (${where})?`)) {
1708
+ const failures = await applyProtectPlans(stale, deps);
1709
+ note('rulesets', failures > 0 ? 'failed' : unreadable.length > 0 ? 'waiting-on-you' : 'done', failures === 0
1710
+ ? `applied to ${where}${inPlace > 0 ? `; ${inPlace} already in place` : ''}${unread}${left}`
1711
+ : `${failures} of ${stale.length} failed (see above)${unread}`);
1712
+ }
1713
+ else {
1714
+ note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${stale.length} repo(s) need the fleet ruleset (${where})${unread} — run \`ashlr authority protect --print\`, then \`--apply\``);
1715
+ }
1716
+ }
1717
+ else if (unreadable.length > 0) {
1718
+ 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)` : ''}`);
1719
+ }
1720
+ else {
1721
+ note('rulesets', 'already', `the fleet ruleset is in place on ${inPlace === 1 ? '1 repo' : `${inPlace} repos`}${left}`);
1722
+ }
1723
+ }
1724
+ // 8. Retire the burned activation directory.
1725
+ current = 'old activation state';
1726
+ const activation = join(homedir(), '.ashlr', 'activation');
1727
+ if (!existsSync(activation))
1728
+ note('old activation state', 'already', 'nothing to retire');
1729
+ else if (await ask('Move ~/.ashlr/activation (the burned key) out of ~/.ashlr so no code path can reach it?')) {
1730
+ try {
1731
+ const target = join(homedir(), `ashlr-activation-retired-${new Date().toISOString().replace(/[:.]/g, '-')}`);
1732
+ renameSync(activation, target);
1733
+ note('old activation state', 'done', `moved to ${target.replace(homedir(), '~')} — move it offline, then delete it`);
1734
+ }
1735
+ catch (error) {
1736
+ note('old activation state', 'failed', `not moved (${failure(error)})`);
1737
+ }
1738
+ }
1739
+ else
1740
+ note('old activation state', dryRun ? 'skipped' : 'waiting-on-you', 'archive ~/.ashlr/activation offline, then delete it');
1741
+ // 9. Provenance key — rotated ONCE. The burned key is whichever one no
1742
+ // rotation recorded; a key a rotation made (and recorded) is left alone,
1743
+ // because rotating again would invalidate every pending proposal for nothing.
1744
+ current = 'provenance key';
1745
+ const provenance = await provenanceKeyState();
1746
+ if (provenance.state === 'rotated') {
1747
+ note('provenance key', 'already', `rotated ${provenance.rotatedAt.slice(0, 10)}; not rotated again, so pending proposals keep verifying`);
1748
+ }
1749
+ else if (await ask(provenance.state === 'missing'
1750
+ ? 'Create the provenance HMAC key now?'
1751
+ : 'Rotate the provenance HMAC key (agents could read the old one)?')) {
1752
+ try {
1753
+ const rotated = await rotateProvenanceKey();
1754
+ note('provenance key', 'done', rotated.retiredAs ? 'rotated; the old key was moved aside' : 'created');
1755
+ }
1756
+ catch (error) {
1757
+ note('provenance key', 'failed', `not rotated (${failure(error)})`);
1758
+ }
1759
+ }
1760
+ else {
1761
+ const why = provenance.state === 'unhealthy' ? `the current key is unusable (${provenance.reason}); ` : '';
1762
+ note('provenance key', dryRun ? 'skipped' : 'waiting-on-you', `${why}run \`ashlr authority rotate-provenance\``);
1763
+ }
1764
+ // 10. First grant (Touch ID), 11. the switch, 12. the daemon service,
1765
+ // 13. resident admission (docs/RESIDENT-RUNTIME.md). Setup itself never
1766
+ // touches launchd: it names the one command that does.
1767
+ const DAEMON_DETAIL = 'would check that the ai.ashlr.daemon service is loaded and running (each tick re-verifies the grant)';
1768
+ if (planning) {
1769
+ note('standing grant', 'skipped', 'would sign the first standing grant (Touch ID)');
1770
+ note('autonomy switch', 'skipped', 'would offer to set the autonomy switch to Autonomous (the ladder starts in shadow)');
1771
+ note('daemon service', 'skipped', DAEMON_DETAIL);
1772
+ return done();
1773
+ }
1774
+ current = 'standing grant';
1775
+ const { evaluateStandingAuthority, displaySurfaceTarget } = await import('../core/authority/effective-config.js');
1776
+ const evaluation = evaluateStandingAuthority({ mode: 'cached', surface: displaySurfaceTarget() });
1777
+ let grantActive = evaluation.grantState === 'active';
1778
+ if (grantActive) {
1779
+ note('standing grant', 'already', `grant #${evaluation.grant?.grantSeq ?? '?'} is active`);
1780
+ }
1781
+ else if (await ask('Sign the first standing grant now (Touch ID)?')) {
1782
+ try {
1783
+ const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
1784
+ const draft = await buildStandingGrantDraft('auto');
1785
+ for (const line of draft.summary)
1786
+ deps.out(` ${line}`);
1787
+ const signed = await signAndInstall(draft.payload, deps);
1788
+ grantActive = signed.ok;
1789
+ note('standing grant', signed.ok ? 'done' : 'failed', signed.ok ? `grant #${signed.grant.grantSeq} installed, starting at ${signed.grant.rollout.stages[0].id}` : signed.reason);
1790
+ }
1791
+ catch (error) {
1792
+ note('standing grant', 'failed', `no grant was drafted (${failure(error)}) — rerun setup, or run \`ashlr authority grant\``);
1793
+ }
1794
+ }
1795
+ else
1796
+ note('standing grant', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority grant`');
1797
+ // Resumable: a grant signed on an earlier run (or with `authority grant`)
1798
+ // still gets the switch offered — it used to be offered only in the run
1799
+ // that signed. Raising stays capped by the grant (requestAutonomySwitch).
1800
+ current = 'autonomy switch';
1801
+ if (!grantActive) {
1802
+ note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', 'needs an active standing grant first');
1803
+ }
1804
+ else if (evaluation.switch === 'autonomous') {
1805
+ note('autonomy switch', 'already', 'Autonomous');
1806
+ }
1807
+ else if (await ask('Set the autonomy switch to Autonomous (the ladder starts in shadow)?')) {
1808
+ const { requestAutonomySwitch } = await import('../core/authority/effective-config.js');
1809
+ const switched = requestAutonomySwitch('autonomous', 'mason', 'ashlr authority setup');
1810
+ note('autonomy switch', switched.ok ? 'done' : 'failed', switched.ok ? 'Autonomous' : switched.reason);
1811
+ }
1812
+ else
1813
+ note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', `it is ${evaluation.switch} — run \`ashlr authority switch autonomous\``);
1814
+ // 12. Read-only service observation. Only `resident start` (Mason, in his
1815
+ // own terminal, under the active grant) installs or restarts the service.
1816
+ current = 'daemon service';
1817
+ let service;
1818
+ try {
1819
+ service = await deps.daemonService();
1820
+ }
1821
+ catch {
1822
+ service = 'unknown';
1823
+ }
1824
+ const START = `\`${RESIDENT_START_COMMAND}\``;
1825
+ const after = grantActive ? `run ${START}` : `after the standing grant, run ${START}`;
1826
+ // Before the grant, the command that moves this step on is the grant.
1827
+ const serviceCommand = grantActive ? {} : { command: 'ashlr authority grant' };
1828
+ if (service === 'running')
1829
+ note('daemon service', 'already', 'ai.ashlr.daemon is running (each tick re-verifies the grant)');
1830
+ else if (service === 'absent')
1831
+ note('daemon service', 'waiting-on-you', `no ai.ashlr.daemon service is installed — ${after} to install it`, serviceCommand);
1832
+ else if (service === 'loaded')
1833
+ note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is loaded but stopped — ${after} to restart it`, serviceCommand);
1834
+ else if (service === 'not-loaded')
1835
+ note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is not loaded — ${after} to load it`, serviceCommand);
1836
+ else
1837
+ note('daemon service', 'waiting-on-you', `ai.ashlr.daemon state is unknown — \`ashlr authority resident status\` shows why; ${after} to repair it`, serviceCommand);
1838
+ // 13. Resident admission: the grant, this release and the service together.
1839
+ current = 'resident runtime';
1840
+ if (!grantActive) {
1841
+ note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
1842
+ }
1843
+ else {
1844
+ const resident = await residentSetupRow(deps, service);
1845
+ note('resident runtime', resident.status, resident.detail, { command: resident.command });
1051
1846
  }
1052
1847
  }
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));
1848
+ catch (error) {
1849
+ // Nothing above is expected to throw; if something does, it is this
1850
+ // step's failure and the run still ends with the summary.
1851
+ note(report.some((row) => row.step === current) ? 'setup' : current, 'failed', `stopped: ${failure(error)}`);
1069
1852
  }
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)');
1853
+ return done();
1854
+ }
1855
+ /**
1856
+ * Setup's resident step (docs/RESIDENT-RUNTIME.md §d):
1857
+ * already admitted AND ai.ashlr.daemon running AND its plist is what
1858
+ * `resident start` would write from config now;
1859
+ * waiting-on-you the exact command that gets there (start / clear-stop / switch);
1860
+ * blocked a prerequisite is missing (no active grant, not a compiled
1861
+ * release, dirty build, no confinement, not macOS).
1862
+ * Read-only: never mints, never touches launchd.
1863
+ */
1864
+ async function residentSetupRow(deps, service) {
1865
+ let admission;
1866
+ try {
1867
+ admission = await deps.resident.admission();
1076
1868
  }
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`);
1869
+ catch (error) {
1870
+ return { status: 'failed', detail: `resident admission could not be evaluated (${error.message})` };
1087
1871
  }
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');
1872
+ if (!admission.ok) {
1873
+ const fix = admission.command ? ` — run \`${admission.command}\`` : '';
1874
+ // The checklist's command is exactly what the admission asks for (none when it names none).
1875
+ return { status: admission.status === 'waiting-on-you' ? 'waiting-on-you' : 'blocked', detail: `${admission.reason}${fix}`, command: admission.command ?? null };
1094
1876
  }
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);
1877
+ let observed;
1878
+ try {
1879
+ observed = await deps.resident.observe();
1121
1880
  }
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');
1881
+ catch (error) {
1882
+ return { status: 'failed', detail: `the resident service could not be observed (${error.message})` };
1129
1883
  }
1130
- else if (evaluation.switch === 'autonomous') {
1131
- note('autonomy switch', 'already', 'Autonomous');
1884
+ if (observed.problem)
1885
+ return { status: 'blocked', detail: observed.problem, command: null };
1886
+ const start = '`ashlr authority resident start`';
1887
+ if (service !== 'running') {
1888
+ 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
1889
  }
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);
1890
+ if (observed.plist === 'drifted') {
1891
+ const budget = observed.installedBudgetUsd !== observed.expectedBudgetUsd
1892
+ ? ` (installed budget $${observed.installedBudgetUsd ?? '?'}/day, config says $${observed.expectedBudgetUsd ?? '?'}/day)`
1893
+ : '';
1894
+ return { status: 'waiting-on-you', detail: `the running service's plist differs from config${budget} — run ${start} to regenerate and restart it` };
1137
1895
  }
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();
1896
+ if (observed.plist !== 'current') {
1897
+ return { status: 'waiting-on-you', detail: `the running service's plist could not be matched (${observed.plist}) — run ${start} to regenerate it` };
1145
1898
  }
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();
1899
+ return { status: 'already', detail: `the resident daemon is running under grant #${admission.grantSeq ?? '?'} (until ${admission.expiresAt ?? '?'}), from clean release ${admission.revision?.slice(0, 12) ?? '?'}` };
1900
+ }
1901
+ // ---------------------------------------------------------------------------
1902
+ // resident start | stop | status
1903
+ // ---------------------------------------------------------------------------
1904
+ function describeAdmission(admission) {
1905
+ if (admission.ok)
1906
+ return `admitted — ${admission.reason}`;
1907
+ return `${admission.status} — ${admission.reason}${admission.command ? ` (run \`${admission.command}\`)` : ''}`;
1908
+ }
1909
+ async function cmdResident(sub, parsed, deps) {
1910
+ switch (sub) {
1911
+ case 'status':
1912
+ return cmdResidentStatus(parsed, deps);
1913
+ case 'start':
1914
+ return cmdResidentStart(parsed, deps);
1915
+ case 'stop':
1916
+ return cmdResidentStop(deps);
1917
+ default:
1918
+ deps.err('Usage: ashlr authority resident <start|stop|status [--json]>');
1919
+ return 2;
1920
+ }
1921
+ }
1922
+ async function cmdResidentStatus(parsed, deps) {
1923
+ const admission = await deps.resident.admission();
1924
+ const observed = await deps.resident.observe();
1925
+ if (parsed.flags.has('--json')) {
1926
+ deps.out(JSON.stringify({ schema: 'ashlr.resident-status.v1', admission, service: observed }, null, 2));
1927
+ return 0;
1928
+ }
1929
+ deps.out(`Resident admission: ${describeAdmission(admission)}`);
1930
+ deps.out(`Service: ai.ashlr.daemon ${observed.state}${observed.plistPath ? ` (${observed.plistPath.replace(homedir(), '~')})` : ''}`);
1931
+ const budget = `installed $${observed.installedBudgetUsd ?? '—'}/day, config $${observed.expectedBudgetUsd ?? '?'}/day`;
1932
+ deps.out(`Plist: ${observed.plist}${observed.plist === 'drifted' ? ' — `ashlr authority resident start` regenerates it' : ''} (budget: ${budget})`);
1933
+ if (observed.problem)
1934
+ deps.out(` ${observed.problem}`);
1935
+ return 0;
1936
+ }
1937
+ /**
1938
+ * `resident start` — the only command that installs or restarts the resident
1939
+ * service. Order: refuse a non-operator context; show the verdict; stop if
1940
+ * not admitted; no-op when already running from the current plist; show
1941
+ * exactly what will happen and ask (no --yes: the operator confirms here);
1942
+ * then the real start, which re-verifies everything itself (mint).
1943
+ */
1944
+ async function cmdResidentStart(parsed, deps) {
1945
+ if (parsed.flags.has('--yes')) {
1946
+ deps.err('resident start takes no --yes: confirm it yourself at the prompt.');
1947
+ return 2;
1948
+ }
1949
+ const refusal = await deps.resident.operatorRefusal();
1950
+ if (refusal) {
1951
+ deps.err(`resident start refused: ${refusal}`);
1952
+ return 1;
1953
+ }
1954
+ const admission = await deps.resident.admission();
1955
+ if (!admission.ok) {
1956
+ deps.err(`resident start refused: ${describeAdmission(admission)}`);
1957
+ return 1;
1958
+ }
1959
+ const observed = await deps.resident.observe();
1960
+ if (observed.problem) {
1961
+ deps.err(`resident start refused: ${observed.problem}`);
1962
+ return 1;
1963
+ }
1964
+ if (observed.state === 'running' && observed.plist === 'current') {
1965
+ deps.out(`ai.ashlr.daemon is already running under grant #${admission.grantSeq ?? '?'} from the current plist; nothing to do.`);
1966
+ return 0;
1967
+ }
1968
+ const action = observed.state === 'running' ? 'regenerate the plist from config and restart' : observed.plist === 'absent' ? 'install and start' : 'regenerate, load and start';
1969
+ deps.out(`Resident daemon under grant #${admission.grantSeq ?? '?'} (expires ${admission.expiresAt ?? '?'}):`);
1970
+ deps.out(` release ${admission.revision ?? '?'} (${admission.packageRoot?.replace(homedir(), '~') ?? '?'})`);
1971
+ deps.out(` service ai.ashlr.daemon → ${observed.plistPath?.replace(homedir(), '~') ?? '?'} (${observed.state}, plist ${observed.plist})`);
1972
+ deps.out(` budget $${observed.expectedBudgetUsd ?? '?'}/day from config daemon.dailyBudgetUsd${observed.installedBudgetUsd !== null && observed.installedBudgetUsd !== observed.expectedBudgetUsd ? ` (installed: $${observed.installedBudgetUsd})` : ''}`);
1973
+ deps.out(' launchd enable + bootstrap (RunAtLoad); every tick re-verifies the grant, Stop and the switch');
1974
+ if (!(await deps.confirm(`${action[0].toUpperCase()}${action.slice(1)} ai.ashlr.daemon now?`))) {
1975
+ deps.out('Nothing was changed.');
1976
+ return 1;
1977
+ }
1978
+ const result = await deps.resident.start();
1979
+ if (!result.ok) {
1980
+ deps.err(`resident start failed: ${result.reason}`);
1981
+ return 1;
1982
+ }
1983
+ deps.out(`✓ ${result.detail}`);
1984
+ deps.out('Stop anytime: `ashlr authority stop` halts agents (the service stays up, idle); `ashlr authority resident stop` removes the service.');
1985
+ return 0;
1986
+ }
1987
+ /** Lowering: never asks, never needs the grant. */
1988
+ async function cmdResidentStop(deps) {
1989
+ const result = await deps.resident.stop();
1990
+ if (!result.ok) {
1991
+ deps.err(`resident stop failed: ${result.reason}`);
1992
+ return 1;
1993
+ }
1994
+ deps.out(`${result.detail}. Stop (~/.ashlr/KILL) and the grant are unchanged.`);
1995
+ return 0;
1160
1996
  }
1161
1997
  function finish(report, deps, dryRun = false, json = false) {
1162
1998
  const result = authoritySetupReport(report, dryRun);
@@ -1166,10 +2002,54 @@ function finish(report, deps, dryRun = false, json = false) {
1166
2002
  }
1167
2003
  else {
1168
2004
  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}.`);
2005
+ 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
2006
  }
1171
2007
  return summary.failed > 0 ? 1 : 0;
1172
2008
  }
2009
+ const HUB_REPO = 'ashlrai/ashlr-hub';
2010
+ /**
2011
+ * The dry run's look at the trust-root PR, through `gh api <path>` GETs only
2012
+ * (each passes isReadOnlyGhApiCall, so the Verse probe can run it): the PR an
2013
+ * earlier run opened, the key already merged on the default branch, or a
2014
+ * branch an earlier run pushed without a PR. Same order as the live run's
2015
+ * probes. null = none of these (or GitHub could not be read): the step then
2016
+ * reads as it always has.
2017
+ */
2018
+ async function probeTrustRootReadOnly(deps, root) {
2019
+ const branch = `authority/trust-root-${root.keyId}`;
2020
+ const read = async (path) => {
2021
+ const result = await deps.run('gh', ['api', path]);
2022
+ if (result.status !== 0)
2023
+ return null;
2024
+ try {
2025
+ return JSON.parse(result.stdout || 'null');
2026
+ }
2027
+ catch {
2028
+ return null;
2029
+ }
2030
+ };
2031
+ const open = await read(`repos/${HUB_REPO}/pulls?head=${HUB_REPO.split('/')[0]}%3A${encodeURIComponent(branch)}&state=open&per_page=1`);
2032
+ const url = Array.isArray(open) ? open[0]?.html_url : undefined;
2033
+ if (typeof url === 'string' && /^https:\/\/github\.com\/[^\s]+\/pull\/\d+$/u.test(url)) {
2034
+ return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, link: url, command: null };
2035
+ }
2036
+ const repo = await read(`repos/${HUB_REPO}`);
2037
+ const base = typeof repo?.default_branch === 'string' && /^[A-Za-z0-9._/-]{1,100}$/u.test(repo.default_branch) ? repo.default_branch : 'master';
2038
+ const file = await read(`repos/${HUB_REPO}/contents/src/core/authority/trust-roots.ts?ref=${encodeURIComponent(base)}`);
2039
+ if (file && file.encoding === 'base64' && typeof file.content === 'string'
2040
+ && Buffer.from(file.content, 'base64').toString('utf8').includes(`keyId: '${root.keyId}'`)) {
2041
+ return {
2042
+ status: 'waiting-on-you',
2043
+ detail: `${root.keyId} is merged on ${base}, but this release was built before it — install a release built after it`,
2044
+ command: 'npm run build',
2045
+ };
2046
+ }
2047
+ const pushed = await read(`repos/${HUB_REPO}/git/ref/heads/${branch.split('/').map(encodeURIComponent).join('/')}`);
2048
+ if (pushed && pushed.ref === `refs/heads/${branch}`) {
2049
+ return { status: 'waiting-on-you', detail: `${branch} was pushed by an earlier run but has no PR yet — rerun setup to open it` };
2050
+ }
2051
+ return null;
2052
+ }
1173
2053
  /**
1174
2054
  * Open Mason's trust-root PR from a throwaway worktree (his checkout is never
1175
2055
  * touched). Resumable: a rerun finds the PR it already opened, or the key
@@ -1180,73 +2060,76 @@ async function openTrustRootPr(parsed, deps, root, ask, probe) {
1180
2060
  if (!source)
1181
2061
  return { status: 'waiting-on-you', detail: 'pass --source <your ashlr-hub checkout> so setup can open the trust-root PR' };
1182
2062
  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']);
2063
+ const base = async () => {
2064
+ const result = await deps.run('gh', ['repo', 'view', 'ashlrai/ashlr-hub', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name']);
1185
2065
  return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : 'master';
1186
2066
  };
2067
+ const prLink = (text) => /https:\/\/github\.com\/\S+\/pull\/\d+/u.exec(text)?.[0];
1187
2068
  let baseBranch = null;
1188
2069
  let pushedAlready = false;
1189
2070
  if (probe) {
1190
2071
  // 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 // ""']);
2072
+ const open = await deps.run('gh', ['pr', 'list', '--repo', 'ashlrai/ashlr-hub', '--head', branch, '--state', 'open', '--json', 'url', '--jq', '.[0].url // ""']);
1192
2073
  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` };
2074
+ const url = open.stdout.trim();
2075
+ return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
1194
2076
  }
1195
- baseBranch = base();
1196
- const fetched = deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
2077
+ baseBranch = await base();
2078
+ const fetched = await deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
1197
2079
  const onBase = fetched.status === 0
1198
- ? deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
2080
+ ? await deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
1199
2081
  : null;
1200
2082
  if (onBase?.status === 0 && onBase.stdout.includes(`keyId: '${root.keyId}'`)) {
1201
2083
  return { status: 'waiting-on-you', detail: `${root.keyId} is already on ${baseBranch}, but this release was built before it` };
1202
2084
  }
1203
2085
  // 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]);
2086
+ const remote = await deps.run('git', ['-C', source, 'ls-remote', '--exit-code', '--heads', 'origin', branch]);
1205
2087
  pushedAlready = remote.status === 0 && remote.stdout.trim() !== '';
1206
2088
  }
1207
2089
  if (!(await ask(`Open a PR in ${source.replace(homedir(), '~')} adding key ${root.keyId} to trust-roots.ts?`))) {
1208
2090
  return { status: 'waiting-on-you', detail: `add this root yourself: ${JSON.stringify({ keyId: root.keyId, alg: 'ES256' })}` };
1209
2091
  }
1210
- baseBranch ??= base();
1211
- const createPr = (cwd) => {
1212
- const pr = deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
2092
+ baseBranch ??= await base();
2093
+ const createPr = async (cwd) => {
2094
+ const pr = await deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
1213
2095
  '--title', `authority: trust custody key ${root.keyId}`,
1214
2096
  '--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
2097
  if (pr.status !== 0)
1216
2098
  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` };
2099
+ const url = pr.stdout.trim();
2100
+ return { status: 'waiting-on-you', detail: `opened ${url} — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
1218
2101
  };
1219
2102
  if (pushedAlready)
1220
2103
  return createPr();
1221
2104
  const worktree = mkdtempSync(join(tmpdir(), 'ashlr-trust-root-'));
1222
2105
  rmSync(worktree, { recursive: true, force: true });
1223
- const step = (bin, args, cwd) => {
1224
- const result = deps.run(bin, args, cwd ? { cwd } : {});
2106
+ const step = async (bin, args, cwd) => {
2107
+ const result = await deps.run(bin, args, cwd ? { cwd } : {});
1225
2108
  if (result.status !== 0)
1226
2109
  throw new Error(`${bin} ${args[0] === '-C' ? args[2] : args[0]} failed: ${result.stderr.trim().slice(0, 200)}`);
1227
2110
  };
1228
2111
  let worktreeAdded = false;
1229
2112
  try {
1230
- step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
2113
+ await step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
1231
2114
  // -B: a local branch left by an earlier run that stopped before the PR
1232
2115
  // (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}`]);
2116
+ await step('git', ['-C', source, 'worktree', 'add', '-B', branch, worktree, `origin/${baseBranch}`]);
1234
2117
  worktreeAdded = true;
1235
2118
  const file = join(worktree, 'src', 'core', 'authority', 'trust-roots.ts');
1236
2119
  const updated = renderTrustRootsWithKey(readFileSync(file, 'utf8'), root);
1237
2120
  if (!updated)
1238
2121
  return { status: 'failed', detail: `trust-roots.ts on ${baseBranch} already lists a different root — add the key by hand` };
1239
2122
  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);
2123
+ await step('git', ['add', 'src/core/authority/trust-roots.ts'], worktree);
2124
+ await step('git', ['commit', '-m', `authority: trust custody key ${root.keyId}`], worktree);
2125
+ await step('git', ['push', '-u', 'origin', branch], worktree);
2126
+ return await createPr(worktree);
1244
2127
  }
1245
2128
  finally {
1246
- deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
2129
+ await deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
1247
2130
  // The pushed branch lives on origin; the local copy only blocks a rerun.
1248
2131
  if (worktreeAdded)
1249
- deps.run('git', ['-C', source, 'branch', '-D', branch]);
2132
+ await deps.run('git', ['-C', source, 'branch', '-D', branch]);
1250
2133
  }
1251
2134
  }
1252
2135
  /** The enrolled checkout of ashlr-hub itself, if any. */