@bongos/core 1.18.19 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.bongos-core.json +514 -194
  2. package/.claude/skills/builder-box/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +1 -1
  4. package/.claude/skills/goal-create/SKILL.md +1 -1
  5. package/.claude/skills/idea-triage/SKILL.md +1 -1
  6. package/.claude/skills/planning-session/SKILL.md +5 -5
  7. package/.claude/skills/scan-before-install/SKILL.md +165 -0
  8. package/clients/bongos-client/README.md +1 -1
  9. package/clients/bongos-client/bongos-client.global.js +63 -41
  10. package/clients/bongos-client/index.cjs +63 -41
  11. package/clients/bongos-client/index.d.ts +78 -44
  12. package/clients/bongos-client/index.mjs +63 -41
  13. package/docs/adr/0020-security-model-public-stub.md +1 -1
  14. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -0
  15. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +1 -1
  16. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +2 -1
  17. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +51 -0
  18. package/docs/adr/0156-user-set-display-name-wins.md +62 -0
  19. package/docs/adr/0157-archon-is-rank-and-identity-only.md +93 -0
  20. package/docs/adr/0158-choke-point-verdict-taxonomy.md +174 -0
  21. package/docs/adr/0159-merge-gate-reconciliation.md +88 -0
  22. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +77 -0
  23. package/docs/adr/README.md +8 -0
  24. package/docs/api/openapi.json +1209 -233
  25. package/docs/api-reference.md +61 -46
  26. package/docs/architecture.md +1 -1
  27. package/docs/canonical-permissions.md +61 -16
  28. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  29. package/docs/file-map.md +4 -1
  30. package/docs/onboarding/diagrams/assertions.json +3 -3
  31. package/docs/recipes/core-release-pipeline.md +179 -0
  32. package/docs/recipes/packaging-the-core.md +2 -0
  33. package/docs/recipes/parallel-goal-waves.md +1 -3
  34. package/docs/recipes/private-npm-distribution.md +2 -0
  35. package/docs/recipes/upgrading-the-core.md +2 -0
  36. package/migrations/core_197_display_name_source.sql +56 -0
  37. package/migrations/core_204_rekey_active_claim_per_session_worktree.sql +77 -0
  38. package/modules/autonomy/routes/autonomy.js +1 -1
  39. package/modules/builder-settings/db.js +7 -1
  40. package/modules/dev-box/app/src/vendor/bongos-client.cjs +63 -41
  41. package/modules/dev-box/routes/box.js +11 -9
  42. package/modules/discord/auth.js +3 -1
  43. package/modules/discord/routes/discord.js +8 -2
  44. package/modules/discord/ship-broadcast.js +48 -1
  45. package/modules/governance/catalog.js +70 -20
  46. package/modules/governance/db.js +102 -0
  47. package/modules/governance/migrations/governance_006_version_create.sql +38 -0
  48. package/modules/governance/migrations/governance_007_delegate_archon_ops_to_metic.sql +72 -0
  49. package/modules/governance/routes/governance.js +235 -6
  50. package/modules/grading/grader-workers/index.js +18 -9
  51. package/modules/hall-ui/public/gate.js +6 -2
  52. package/modules/hall-ui/public/goals-page.js +6 -2
  53. package/modules/hall-ui/public/governance.css +100 -0
  54. package/modules/hall-ui/public/governance.html +77 -0
  55. package/modules/hall-ui/public/governance.js +353 -0
  56. package/modules/hall-ui/public/harbor.js +14 -8
  57. package/modules/hall-ui/public/instances-panel.js +3 -1
  58. package/modules/hall-ui/public/new-project-wizard.js +6 -0
  59. package/modules/hall-ui/public/people.html +51 -0
  60. package/modules/hall-ui/public/people.js +100 -0
  61. package/modules/hall-ui/public/profile.html +2 -1
  62. package/modules/hall-ui/public/profile.js +9 -3
  63. package/modules/hall-ui/public/sessions.js +4 -2
  64. package/modules/hall-ui/public/shell.js +28 -7
  65. package/modules/hall-ui/public/task-detail.js +6 -3
  66. package/modules/hall-ui/public/watch.js +17 -9
  67. package/modules/ideas/blockers.js +44 -7
  68. package/modules/ideas/routes/blockers.js +12 -7
  69. package/modules/ideas/routes/inbox.js +2 -2
  70. package/modules/lifecycle/db.js +84 -17
  71. package/modules/lifecycle/github-push.js +37 -10
  72. package/modules/lifecycle/publish-reconciler.js +128 -31
  73. package/modules/lifecycle/routes/claims.js +10 -4
  74. package/modules/lifecycle/routes/dependencies.js +2 -2
  75. package/modules/lifecycle/routes/done-when.js +4 -4
  76. package/modules/lifecycle/routes/gate-approvals.js +9 -3
  77. package/modules/lifecycle/routes/goals.js +5 -5
  78. package/modules/lifecycle/routes/tasks.js +16 -16
  79. package/modules/lifecycle/routes/versions.js +13 -2
  80. package/modules/lifecycle/ship-card.js +38 -7
  81. package/modules/memory/routes/memory.js +2 -2
  82. package/modules/onboarding/routes/access-requests.js +3 -3
  83. package/modules/platform-identity/routes/projects.js +10 -6
  84. package/modules/platform-identity/routes/scouting.js +2 -2
  85. package/modules/provisioning/migrations/provisioning_004_onboard_mode.sql +30 -0
  86. package/modules/provisioning/provisioning.js +24 -6
  87. package/modules/provisioning/routes/provisioning.js +24 -6
  88. package/modules/security/artifact-scan/aggregate.js +133 -0
  89. package/modules/security/artifact-scan/detectors/capabilities.js +53 -0
  90. package/modules/security/artifact-scan/detectors/config-obfuscation.js +41 -0
  91. package/modules/security/artifact-scan/detectors/credentials.js +112 -0
  92. package/modules/security/artifact-scan/detectors/dependencies.js +115 -0
  93. package/modules/security/artifact-scan/detectors/hooks.js +116 -0
  94. package/modules/security/artifact-scan/detectors/injection.js +118 -0
  95. package/modules/security/artifact-scan/detectors/mcp.js +102 -0
  96. package/modules/security/artifact-scan/detectors/network.js +67 -0
  97. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +67 -0
  98. package/modules/security/artifact-scan/detectors/permissions.js +91 -0
  99. package/modules/security/artifact-scan/detectors/shared.js +219 -0
  100. package/modules/security/artifact-scan/fence-local.js +99 -0
  101. package/modules/security/artifact-scan/fetch.js +330 -0
  102. package/modules/security/artifact-scan/floor.js +319 -0
  103. package/modules/security/artifact-scan/limits.js +66 -0
  104. package/modules/security/artifact-scan/manifest.js +203 -0
  105. package/modules/security/artifact-scan/mirror.js +158 -0
  106. package/modules/security/artifact-scan/quarantine.js +149 -0
  107. package/modules/security/artifact-scan/rebuild-spec.js +120 -0
  108. package/modules/security/artifact-scan/report.js +155 -0
  109. package/modules/security/artifact-scan/sanitize.js +317 -0
  110. package/modules/security/migrations/security_001_tool_scans.sql +145 -0
  111. package/modules/security/module.json +2 -2
  112. package/modules/security/routes/reports.js +11 -7
  113. package/modules/security/routes/scans.js +115 -0
  114. package/modules/security/scan-store.js +214 -0
  115. package/modules/security/tests/aggregate.mjs +283 -0
  116. package/modules/security/tests/artifact_scan.mjs +591 -0
  117. package/modules/security/tests/floor.mjs +549 -0
  118. package/modules/security/tests/mirror.mjs +350 -0
  119. package/modules/security/tests/sanitize_rebuild.mjs +499 -0
  120. package/modules/security/tests/scan_store.mjs +250 -0
  121. package/modules/security/tests/tool_scans_migration.mjs +116 -0
  122. package/modules/sessions/routes/sessions.js +6 -5
  123. package/package-lock.json +2 -2
  124. package/package.json +1 -1
  125. package/public-landing/create.html +13 -3
  126. package/scripts/gds/abandon.js +1 -1
  127. package/scripts/gds/audit-authorship.sh +6 -0
  128. package/scripts/gds/box.js +1 -1
  129. package/scripts/gds/claim.js +15 -0
  130. package/scripts/gds/cli-lib.js +16 -4
  131. package/scripts/gds/gating.js +28 -13
  132. package/scripts/gds/gen-api-docs.js +1 -1
  133. package/scripts/gds/goal.js +234 -0
  134. package/scripts/gds/init.js +70 -15
  135. package/scripts/gds/main-audit.js +80 -11
  136. package/scripts/gds/mirror-redact.js +6 -0
  137. package/scripts/gds/newcomer-floor.js +2 -2
  138. package/scripts/gds/provision.js +230 -94
  139. package/scripts/gds/run-unit-tests.js +9 -1
  140. package/scripts/gds/sandbox-stage.js +66 -9
  141. package/scripts/gds/search-ingest-md.js +1 -0
  142. package/scripts/gds/sequence.js +67 -8
  143. package/scripts/gds/ship.js +183 -50
  144. package/scripts/hall-preview/server.js +1 -1
  145. package/scripts/migrate.sh +5 -5
  146. package/src/bongos/auth.js +129 -27
  147. package/src/bongos/db-kernel.js +66 -5
  148. package/src/bongos/db.js +6 -7
  149. package/src/bongos/github-repos.js +19 -7
  150. package/src/bongos/goal-scope-check.js +1 -1
  151. package/src/bongos/module-scope-map.js +3 -1
  152. package/src/bongos/permission-path-check.js +1 -1
  153. package/src/bongos/route-rank-check.js +73 -30
  154. package/src/bongos/routes/audit-log.js +4 -2
  155. package/src/bongos/routes/auth.js +8 -4
  156. package/src/bongos/routes/backup.js +7 -4
  157. package/src/bongos/routes/builders.js +23 -11
  158. package/src/bongos/routes/github.js +15 -4
  159. package/src/bongos/routes/llm-cache.js +5 -2
  160. package/src/bongos/routes/modules.js +9 -5
  161. package/src/bongos/routes/security.js +21 -12
  162. package/src/bongos/serve-internal.js +22 -7
  163. package/src/module-api.js +86 -13
  164. package/src/platform-server.js +14 -0
  165. package/tests/api_client.mjs +18 -2
  166. package/tests/api_path_404.mjs +3 -0
  167. package/tests/art_stage.mjs +2 -1
  168. package/tests/atlas.mjs +32 -4
  169. package/tests/auth_admission_gate.mjs +17 -5
  170. package/tests/auth_error_brand.mjs +41 -21
  171. package/tests/auth_resolve_failure.mjs +155 -0
  172. package/tests/blocker_link.mjs +88 -0
  173. package/tests/bongos_downloads.mjs +2 -1
  174. package/tests/box_dns_repoint.mjs +2 -0
  175. package/tests/box_ship_permissions.mjs +2 -0
  176. package/tests/box_terminal_ssrf.mjs +23 -17
  177. package/tests/claim_error_surface.mjs +23 -0
  178. package/tests/claude_materialize.mjs +4 -0
  179. package/tests/cli_box_scope.mjs +40 -0
  180. package/tests/cloud_bongos_pack.mjs +34 -4
  181. package/tests/conductor.mjs +2 -1
  182. package/tests/criterion_authoring_gate.mjs +28 -5
  183. package/tests/currency_label.mjs +10 -1
  184. package/tests/deploy_instance.mjs +2 -0
  185. package/tests/devbox_downloads.mjs +2 -1
  186. package/tests/display_name.mjs +95 -6
  187. package/tests/eviction_policy.mjs +2 -1
  188. package/tests/file_map.mjs +10 -0
  189. package/tests/first_admin_bootstrap.mjs +10 -2
  190. package/tests/first_run_engine_check.mjs +2 -1
  191. package/tests/fitness.mjs +26 -6
  192. package/tests/gate_approvals.mjs +15 -2
  193. package/tests/github_push_land_proof.mjs +50 -4
  194. package/tests/github_repos_helpers.mjs +30 -5
  195. package/tests/github_token_store.mjs +23 -0
  196. package/tests/goal_abuse_matrix.mjs +13 -3
  197. package/tests/goal_cli.mjs +89 -0
  198. package/tests/governance_abuse_matrix.mjs +386 -0
  199. package/tests/governance_fold.mjs +156 -0
  200. package/tests/governance_fold_bench.mjs +50 -0
  201. package/tests/governance_live_proof.mjs +201 -0
  202. package/tests/governance_page_gates.mjs +21 -9
  203. package/tests/governance_parity.mjs +11 -2
  204. package/tests/governance_require_permission.mjs +125 -1
  205. package/tests/governance_routes.mjs +334 -0
  206. package/tests/grader.mjs +7 -3
  207. package/tests/hall_nav.mjs +14 -4
  208. package/tests/helpers.mjs +73 -0
  209. package/tests/hook_core_resolve.mjs +2 -1
  210. package/tests/host_topology_skips.mjs +97 -0
  211. package/tests/infra_exec_paths.mjs +14 -4
  212. package/tests/init.mjs +128 -0
  213. package/tests/known_failing_ratchet.mjs +89 -0
  214. package/tests/lifecycle-card.mjs +2 -1
  215. package/tests/main_audit.mjs +72 -1
  216. package/tests/memory_isolation.mjs +56 -9
  217. package/tests/migrate_root_resolution.mjs +1 -1
  218. package/tests/mirror_redact.mjs +19 -0
  219. package/tests/module_api.mjs +3 -1
  220. package/tests/module_loader.mjs +4 -0
  221. package/tests/modules.mjs +35 -7
  222. package/tests/modules_route.mjs +29 -5
  223. package/tests/onboarding_observability.mjs +49 -11
  224. package/tests/onboarding_route_signals.mjs +1 -0
  225. package/tests/one_active_claim_per_session.mjs +80 -0
  226. package/tests/pre_compact.mjs +2 -1
  227. package/tests/preview_server.mjs +2 -1
  228. package/tests/profile_route.mjs +1 -0
  229. package/tests/provision.mjs +170 -0
  230. package/tests/provisioning_onboard_mode.mjs +180 -0
  231. package/tests/publish_manifest.mjs +16 -3
  232. package/tests/publish_reconciler.mjs +181 -10
  233. package/tests/rank_tier_single_source.mjs +205 -0
  234. package/tests/recall_hints.mjs +2 -1
  235. package/tests/repo_map.mjs +4 -0
  236. package/tests/route_permission_check.mjs +14 -5
  237. package/tests/rubric_fixtures.mjs +3 -0
  238. package/tests/run_routine_two_root.mjs +2 -0
  239. package/tests/sandbox_stage.mjs +69 -7
  240. package/tests/search_ingest.mjs +7 -0
  241. package/tests/security_docs.mjs +28 -6
  242. package/tests/sequence.mjs +64 -0
  243. package/tests/session-cards.mjs +2 -1
  244. package/tests/session_start_freshness.mjs +2 -1
  245. package/tests/ship_broadcast_server.mjs +63 -1
  246. package/tests/ship_card.mjs +66 -0
  247. package/tests/ship_ci_deploy.mjs +66 -1
  248. package/tests/ship_smoke_gate.mjs +76 -0
  249. package/tests/versions_route.mjs +15 -0
  250. package/tests/workflow_exec_paths.mjs +16 -4
  251. package/tests/worktree_guard.mjs +2 -1
@@ -99,7 +99,7 @@ export function createClient(opts = {}) {
99
99
  getAnalyticsBuilderId: (args) => request("GET", "/analytics/builder/{id}", { hasBody: false }, args),
100
100
  },
101
101
  "auditLog": {
102
- // GET /audit-log — rank: archon — GET /audit-log
102
+ // GET /audit-log — rank: metic+archon — GET /audit-log
103
103
  getAuditLog: (args) => request("GET", "/audit-log", { hasBody: false }, args),
104
104
  },
105
105
  "auth": {
@@ -141,7 +141,7 @@ export function createClient(opts = {}) {
141
141
  getAuthWebStart: (args) => request("GET", "/auth/web/start", { hasBody: false }, args),
142
142
  },
143
143
  "autonomy": {
144
- // GET /autonomy/precheck — rank: archon — GET /autonomy/precheck
144
+ // GET /autonomy/precheck — rank: metic+archon — GET /autonomy/precheck
145
145
  getAutonomyPrecheck: (args) => request("GET", "/autonomy/precheck", { hasBody: false }, args),
146
146
  },
147
147
  "backup": {
@@ -201,13 +201,13 @@ export function createClient(opts = {}) {
201
201
  postBoxVersion: (args) => request("POST", "/box/version", { hasBody: true }, args),
202
202
  },
203
203
  "boxes": {
204
- // GET /boxes — rank: archon — GET /boxes
204
+ // GET /boxes — rank: metic+archon — GET /boxes
205
205
  getBoxes: (args) => request("GET", "/boxes", { hasBody: false }, args),
206
- // PATCH /boxes/{builderId}/block — rank: archon — PATCH /boxes/:builderId/block
206
+ // PATCH /boxes/{builderId}/block — rank: metic+archon — PATCH /boxes/:builderId/block
207
207
  patchBoxesBuilderIdBlock: (args) => request("PATCH", "/boxes/{builderId}/block", { hasBody: true }, args),
208
- // POST /boxes/{builderId}/close — rank: archon — POST /boxes/:builderId/close
208
+ // POST /boxes/{builderId}/close — rank: metic+archon — POST /boxes/:builderId/close
209
209
  postBoxesBuilderIdClose: (args) => request("POST", "/boxes/{builderId}/close", { hasBody: true }, args),
210
- // GET /boxes/cost-ledger — rank: archon — GET /boxes/cost-ledger
210
+ // GET /boxes/cost-ledger — rank: metic+archon — GET /boxes/cost-ledger
211
211
  getBoxesCostLedger: (args) => request("GET", "/boxes/cost-ledger", { hasBody: false }, args),
212
212
  },
213
213
  "bugAttachments": {
@@ -215,9 +215,9 @@ export function createClient(opts = {}) {
215
215
  getBugAttachmentsName: (args) => request("GET", "/bug-attachments/{name}", { hasBody: false }, args),
216
216
  },
217
217
  "builders": {
218
- // GET /builders/{id}/active-claims — rank: archon — GET /builders/:id/active-claims
218
+ // GET /builders/{id}/active-claims — rank: metic+archon — GET /builders/:id/active-claims
219
219
  getBuildersIdActiveClaims: (args) => request("GET", "/builders/{id}/active-claims", { hasBody: false }, args),
220
- // PATCH /builders/{id}/budget — rank: archon — PATCH /builders/:id/budget
220
+ // PATCH /builders/{id}/budget — rank: metic+archon — PATCH /builders/:id/budget
221
221
  patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
222
222
  // GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
223
223
  getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
@@ -231,13 +231,13 @@ export function createClient(opts = {}) {
231
231
  getBuildersDirectory: (args) => request("GET", "/builders/directory", { hasBody: false }, args),
232
232
  // GET /builders/me/pending-rank-change — rank: any-builder — GET /builders/me/pending-rank-change
233
233
  getBuildersMePendingRankChange: (args) => request("GET", "/builders/me/pending-rank-change", { hasBody: false }, args),
234
- // GET /builders/roster — rank: archon — GET /builders/roster
234
+ // GET /builders/roster — rank: metic+archon — GET /builders/roster
235
235
  getBuildersRoster: (args) => request("GET", "/builders/roster", { hasBody: false }, args),
236
236
  },
237
237
  "claims": {
238
238
  // POST /claims — rank: any-builder — POST /claims
239
239
  postClaims: (args) => request("POST", "/claims", { hasBody: true }, args),
240
- // POST /claims/{id}/release-on-behalf — rank: archon — POST /claims/:id/release-on-behalf
240
+ // POST /claims/{id}/release-on-behalf — rank: metic+archon — POST /claims/:id/release-on-behalf
241
241
  postClaimsIdReleaseOnBehalf: (args) => request("POST", "/claims/{id}/release-on-behalf", { hasBody: true }, args),
242
242
  // POST /claims/{id}/resolve — rank: any-builder — POST /claims/:id/resolve
243
243
  postClaimsIdResolve: (args) => request("POST", "/claims/{id}/resolve", { hasBody: true }, args),
@@ -259,21 +259,21 @@ export function createClient(opts = {}) {
259
259
  postDependencies: (args) => request("POST", "/dependencies", { hasBody: true }, args),
260
260
  },
261
261
  "discord": {
262
- // GET /discord/channels/plan — rank: archon — GET /discord/channels/plan
262
+ // GET /discord/channels/plan — rank: metic+archon — GET /discord/channels/plan
263
263
  getDiscordChannelsPlan: (args) => request("GET", "/discord/channels/plan", { hasBody: false }, args),
264
- // POST /discord/channels/reconcile — rank: archon — POST /discord/channels/reconcile
264
+ // POST /discord/channels/reconcile — rank: metic+archon — POST /discord/channels/reconcile
265
265
  postDiscordChannelsReconcile: (args) => request("POST", "/discord/channels/reconcile", { hasBody: true }, args),
266
266
  },
267
267
  "doneWhen": {
268
268
  // POST /done-when/{criterionId}/satisfy — rank: metic+archon — POST /done-when/:criterionId/satisfy
269
269
  postDoneWhenCriterionIdSatisfy: (args) => request("POST", "/done-when/{criterionId}/satisfy", { hasBody: true }, args),
270
- // POST /done-when/{criterionId}/unsatisfy — rank: archon — POST /done-when/:criterionId/unsatisfy
270
+ // POST /done-when/{criterionId}/unsatisfy — rank: metic+archon — POST /done-when/:criterionId/unsatisfy
271
271
  postDoneWhenCriterionIdUnsatisfy: (args) => request("POST", "/done-when/{criterionId}/unsatisfy", { hasBody: true }, args),
272
272
  // GET /done-when/pending-review — rank: metic+archon — GET /done-when/pending-review
273
273
  getDoneWhenPendingReview: (args) => request("GET", "/done-when/pending-review", { hasBody: false }, args),
274
274
  },
275
275
  "gateApprovals": {
276
- // GET /gate-approvals — rank: archon — GET /gate-approvals
276
+ // GET /gate-approvals — rank: metic+archon — GET /gate-approvals
277
277
  getGateApprovals: (args) => request("GET", "/gate-approvals", { hasBody: false }, args),
278
278
  // POST /gate-approvals/{pr}/approve — rank: archon — POST /gate-approvals/:pr/approve
279
279
  postGateApprovalsPrApprove: (args) => request("POST", "/gate-approvals/{pr}/approve", { hasBody: true }, args),
@@ -331,11 +331,29 @@ export function createClient(opts = {}) {
331
331
  patchGoalsIdVisibility: (args) => request("PATCH", "/goals/{id}/visibility", { hasBody: true }, args),
332
332
  // GET /goals/inbox — rank: any-builder — GET /goals/inbox
333
333
  getGoalsInbox: (args) => request("GET", "/goals/inbox", { hasBody: false }, args),
334
- // POST /goals/reorder — rank: archon — POST /goals/reorder
334
+ // POST /goals/reorder — rank: metic+archon — POST /goals/reorder
335
335
  postGoalsReorder: (args) => request("POST", "/goals/reorder", { hasBody: true }, args),
336
336
  },
337
+ "governance": {
338
+ // GET /governance/assignments — rank: archon — GET /governance/assignments
339
+ getGovernanceAssignments: (args) => request("GET", "/governance/assignments", { hasBody: false }, args),
340
+ // POST /governance/assignments — rank: archon — POST /governance/assignments
341
+ postGovernanceAssignments: (args) => request("POST", "/governance/assignments", { hasBody: true }, args),
342
+ // DELETE /governance/assignments/{builderId}/{roleKey} — rank: archon — DELETE /governance/assignments/:builderId/:roleKey
343
+ deleteGovernanceAssignmentsBuilderIdRoleKey: (args) => request("DELETE", "/governance/assignments/{builderId}/{roleKey}", { hasBody: false }, args),
344
+ // GET /governance/permissions — rank: archon — GET /governance/permissions
345
+ getGovernancePermissions: (args) => request("GET", "/governance/permissions", { hasBody: false }, args),
346
+ // GET /governance/roles — rank: archon — GET /governance/roles
347
+ getGovernanceRoles: (args) => request("GET", "/governance/roles", { hasBody: false }, args),
348
+ // POST /governance/roles — rank: archon — POST /governance/roles
349
+ postGovernanceRoles: (args) => request("POST", "/governance/roles", { hasBody: true }, args),
350
+ // DELETE /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — DELETE /governance/roles/:roleKey/permissions/:permissionKey
351
+ deleteGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("DELETE", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: false }, args),
352
+ // PUT /governance/roles/{roleKey}/permissions/{permissionKey} — rank: archon — PUT /governance/roles/:roleKey/permissions/:permissionKey
353
+ putGovernanceRolesRoleKeyPermissionsPermissionKey: (args) => request("PUT", "/governance/roles/{roleKey}/permissions/{permissionKey}", { hasBody: true }, args),
354
+ },
337
355
  "grades": {
338
- // GET /grades/by-builder — rank: archon — GET /grades/by-builder
356
+ // GET /grades/by-builder — rank: metic+archon — GET /grades/by-builder
339
357
  getGradesByBuilder: (args) => request("GET", "/grades/by-builder", { hasBody: false }, args),
340
358
  },
341
359
  "healthz": {
@@ -351,7 +369,7 @@ export function createClient(opts = {}) {
351
369
  getInboxId: (args) => request("GET", "/inbox/{id}", { hasBody: false }, args),
352
370
  // PATCH /inbox/{id} — rank: metic+archon — PATCH /inbox/:id
353
371
  patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
354
- // POST /inbox/{id}/ratify — rank: archon — POST /inbox/:id/ratify
372
+ // POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
355
373
  postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
356
374
  },
357
375
  "instance": {
@@ -429,9 +447,9 @@ export function createClient(opts = {}) {
429
447
  getMemoryBfgBuildersBuilderIdFiles: (args) => request("GET", "/memory/bfg/builders/{builderId}/files", { hasBody: false }, args),
430
448
  // POST /memory/builders/{builderId}/bfg-write — rank: bfg-principal — POST /memory/builders/:builderId/bfg-write
431
449
  postMemoryBuildersBuilderIdBfgWrite: (args) => request("POST", "/memory/builders/{builderId}/bfg-write", { hasBody: true }, args),
432
- // GET /memory/builders/{builderId}/file — rank: archon — GET /memory/builders/:builderId/file
450
+ // GET /memory/builders/{builderId}/file — rank: metic+archon — GET /memory/builders/:builderId/file
433
451
  getMemoryBuildersBuilderIdFile: (args) => request("GET", "/memory/builders/{builderId}/file", { hasBody: false }, args),
434
- // GET /memory/builders/{builderId}/files — rank: archon — GET /memory/builders/:builderId/files
452
+ // GET /memory/builders/{builderId}/files — rank: metic+archon — GET /memory/builders/:builderId/files
435
453
  getMemoryBuildersBuilderIdFiles: (args) => request("GET", "/memory/builders/{builderId}/files", { hasBody: false }, args),
436
454
  // DELETE /memory/file — rank: any-builder — DELETE /memory/file
437
455
  deleteMemoryFile: (args) => request("DELETE", "/memory/file", { hasBody: true }, args),
@@ -467,27 +485,27 @@ export function createClient(opts = {}) {
467
485
  postMyProjectsJoin: (args) => request("POST", "/my-projects/join", { hasBody: true }, args),
468
486
  },
469
487
  "overrideRequests": {
470
- // GET /override-requests — rank: archon — GET /override-requests
488
+ // GET /override-requests — rank: metic+archon — GET /override-requests
471
489
  getOverrideRequests: (args) => request("GET", "/override-requests", { hasBody: false }, args),
472
- // POST /override-requests/{id}/decide — rank: archon — POST /override-requests/:id/decide
490
+ // POST /override-requests/{id}/decide — rank: metic+archon — POST /override-requests/:id/decide
473
491
  postOverrideRequestsIdDecide: (args) => request("POST", "/override-requests/{id}/decide", { hasBody: true }, args),
474
492
  },
475
493
  "projects": {
476
- // GET /projects — rank: archon — GET /projects
494
+ // GET /projects — rank: metic+archon — GET /projects
477
495
  getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
478
- // POST /projects — rank: archon — POST /projects
496
+ // POST /projects — rank: metic+archon — POST /projects
479
497
  postProjects: (args) => request("POST", "/projects", { hasBody: true }, args),
480
- // PATCH /projects/{id}/featured — rank: archon — PATCH /projects/:id/featured
498
+ // PATCH /projects/{id}/featured — rank: metic+archon — PATCH /projects/:id/featured
481
499
  patchProjectsIdFeatured: (args) => request("PATCH", "/projects/{id}/featured", { hasBody: true }, args),
482
500
  // GET /projects/featured — rank: public — GET /projects/featured
483
501
  getProjectsFeatured: (args) => request("GET", "/projects/featured", { hasBody: false }, args),
484
- // POST /projects/invite — rank: archon — POST /projects/invite
502
+ // POST /projects/invite — rank: metic+archon — POST /projects/invite
485
503
  postProjectsInvite: (args) => request("POST", "/projects/invite", { hasBody: true }, args),
486
504
  },
487
505
  "provisioning": {
488
- // GET /provisioning/fleet — rank: archon — GET /provisioning/fleet
506
+ // GET /provisioning/fleet — rank: metic+archon — GET /provisioning/fleet
489
507
  getProvisioningFleet: (args) => request("GET", "/provisioning/fleet", { hasBody: false }, args),
490
- // GET /provisioning/fleet/cost-ledger — rank: archon — GET /provisioning/fleet/cost-ledger
508
+ // GET /provisioning/fleet/cost-ledger — rank: metic+archon — GET /provisioning/fleet/cost-ledger
491
509
  getProvisioningFleetCostLedger: (args) => request("GET", "/provisioning/fleet/cost-ledger", { hasBody: false }, args),
492
510
  // GET /provisioning/github-app/callback — rank: public — GET /provisioning/github-app/callback
493
511
  getProvisioningGithubAppCallback: (args) => request("GET", "/provisioning/github-app/callback", { hasBody: false }, args),
@@ -501,7 +519,7 @@ export function createClient(opts = {}) {
501
519
  postProvisioningInstances: (args) => request("POST", "/provisioning/instances", { hasBody: true }, args),
502
520
  // GET /provisioning/instances/{id} — rank: any-builder — GET /provisioning/instances/:id
503
521
  getProvisioningInstancesId: (args) => request("GET", "/provisioning/instances/{id}", { hasBody: false }, args),
504
- // POST /provisioning/instances/{id}/force-teardown — rank: archon — POST /provisioning/instances/:id/force-teardown
522
+ // POST /provisioning/instances/{id}/force-teardown — rank: metic+archon — POST /provisioning/instances/:id/force-teardown
505
523
  postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
506
524
  // POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
507
525
  postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
@@ -551,7 +569,7 @@ export function createClient(opts = {}) {
551
569
  getPublicVersions: (args) => request("GET", "/public/versions", { hasBody: false }, args),
552
570
  },
553
571
  "scouting": {
554
- // GET /scouting — rank: archon — GET /scouting
572
+ // GET /scouting — rank: metic+archon — GET /scouting
555
573
  getScouting: (args) => request("GET", "/scouting", { hasBody: false }, args),
556
574
  },
557
575
  "search": {
@@ -563,24 +581,28 @@ export function createClient(opts = {}) {
563
581
  "security": {
564
582
  // GET /security/adr — rank: metic+archon — GET /security/adr
565
583
  getSecurityAdr: (args) => request("GET", "/security/adr", { hasBody: false }, args),
566
- // GET /security/docs — rank: archon — GET /security/docs
584
+ // GET /security/docs — rank: metic+archon — GET /security/docs
567
585
  getSecurityDocs: (args) => request("GET", "/security/docs", { hasBody: false }, args),
568
- // POST /security/docs — rank: archon — POST /security/docs
586
+ // POST /security/docs — rank: metic+archon — POST /security/docs
569
587
  postSecurityDocs: (args) => request("POST", "/security/docs", { hasBody: true }, args),
570
- // GET /security/docs/{slug} — rank: archon — GET /security/docs/:slug
588
+ // GET /security/docs/{slug} — rank: metic+archon — GET /security/docs/:slug
571
589
  getSecurityDocsSlug: (args) => request("GET", "/security/docs/{slug}", { hasBody: false }, args),
572
- // GET /security/reports — rank: archon — GET /security/reports
590
+ // GET /security/reports — rank: metic+archon — GET /security/reports
573
591
  getSecurityReports: (args) => request("GET", "/security/reports", { hasBody: false }, args),
574
592
  // POST /security/reports — rank: metic+archon — POST /security/reports
575
593
  postSecurityReports: (args) => request("POST", "/security/reports", { hasBody: true }, args),
576
- // POST /security/reports/{id}/confirm — rank: archon — POST /security/reports/:id/confirm
594
+ // POST /security/reports/{id}/confirm — rank: metic+archon — POST /security/reports/:id/confirm
577
595
  postSecurityReportsIdConfirm: (args) => request("POST", "/security/reports/{id}/confirm", { hasBody: true }, args),
578
- // POST /security/reports/{id}/dispute — rank: archon — POST /security/reports/:id/dispute
596
+ // POST /security/reports/{id}/dispute — rank: metic+archon — POST /security/reports/:id/dispute
579
597
  postSecurityReportsIdDispute: (args) => request("POST", "/security/reports/{id}/dispute", { hasBody: true }, args),
580
- // POST /security/reports/{id}/fix — rank: archon — POST /security/reports/:id/fix
598
+ // POST /security/reports/{id}/fix — rank: metic+archon — POST /security/reports/:id/fix
581
599
  postSecurityReportsIdFix: (args) => request("POST", "/security/reports/{id}/fix", { hasBody: true }, args),
582
- // POST /security/reports/{id}/link-fix — rank: archon — POST /security/reports/:id/link-fix
600
+ // POST /security/reports/{id}/link-fix — rank: metic+archon — POST /security/reports/:id/link-fix
583
601
  postSecurityReportsIdLinkFix: (args) => request("POST", "/security/reports/{id}/link-fix", { hasBody: true }, args),
602
+ // GET /security/scans — rank: any-builder — GET /security/scans
603
+ getSecurityScans: (args) => request("GET", "/security/scans", { hasBody: false }, args),
604
+ // POST /security/scans — rank: metic+archon — POST /security/scans
605
+ postSecurityScans: (args) => request("POST", "/security/scans", { hasBody: true }, args),
584
606
  },
585
607
  "session": {
586
608
  // POST /session/optimize — rank: any-builder — POST /session/optimize
@@ -593,7 +615,7 @@ export function createClient(opts = {}) {
593
615
  getSessionsId: (args) => request("GET", "/sessions/{id}", { hasBody: false }, args),
594
616
  // GET /sessions/mine — rank: any-builder — GET /sessions/mine
595
617
  getSessionsMine: (args) => request("GET", "/sessions/mine", { hasBody: false }, args),
596
- // GET /sessions/search — rank: archon — GET /sessions/search
618
+ // GET /sessions/search — rank: metic+archon — GET /sessions/search
597
619
  getSessionsSearch: (args) => request("GET", "/sessions/search", { hasBody: false }, args),
598
620
  },
599
621
  "sso": {
@@ -661,17 +683,17 @@ export function createClient(opts = {}) {
661
683
  getTasksClaimable: (args) => request("GET", "/tasks/claimable", { hasBody: false }, args),
662
684
  // GET /tasks/newcomer-floor — rank: any-builder — GET /tasks/newcomer-floor
663
685
  getTasksNewcomerFloor: (args) => request("GET", "/tasks/newcomer-floor", { hasBody: false }, args),
664
- // POST /tasks/newcomer-restock — rank: archon — POST /tasks/newcomer-restock
686
+ // POST /tasks/newcomer-restock — rank: metic+archon — POST /tasks/newcomer-restock
665
687
  postTasksNewcomerRestock: (args) => request("POST", "/tasks/newcomer-restock", { hasBody: true }, args),
666
688
  // GET /tasks/peer-votes/feed — rank: any-builder — GET /tasks/peer-votes/feed
667
689
  getTasksPeerVotesFeed: (args) => request("GET", "/tasks/peer-votes/feed", { hasBody: false }, args),
668
- // POST /tasks/peer-votes/tally — rank: archon — POST /tasks/peer-votes/tally
690
+ // POST /tasks/peer-votes/tally — rank: metic+archon — POST /tasks/peer-votes/tally
669
691
  postTasksPeerVotesTally: (args) => request("POST", "/tasks/peer-votes/tally", { hasBody: true }, args),
670
692
  },
671
693
  "versions": {
672
694
  // GET /versions — rank: public — GET /versions
673
695
  getVersions: (args) => request("GET", "/versions", { hasBody: false }, args),
674
- // POST /versions — rank: archon — POST /versions
696
+ // POST /versions — rank: metic+archon — POST /versions
675
697
  postVersions: (args) => request("POST", "/versions", { hasBody: true }, args),
676
698
  // GET /versions/{id}/done-when — rank: public — GET /versions/:id/done-when
677
699
  getVersionsIdDoneWhen: (args) => request("GET", "/versions/{id}/done-when", { hasBody: false }, args),
@@ -59,7 +59,7 @@ The defense doesn't depend on the ADR's secrecy. Every protection mentioned in t
59
59
  - New builders see the public stub (this file) when reading the ADR index. The stub tells them how to read the full ADR once promoted to Metic+.
60
60
  - Any change to the *real* defenses (new gated route, new redaction rule, new rate-limit knob) updates the full ADR on the droplet. Significant structural changes (new threat-model attacker, new defense category) also update this public stub.
61
61
  - The `GET /api/gds/security/adr` endpoint exists *now* (V3.R68 / [#287](https://example.com/builders#/task/287)) but returns the not-provisioned placeholder until an Archon writes the file. That's intentional — the endpoint is the contract, the file is the content.
62
- - A Xenos hitting the endpoint sees `403 rank_forbidden` whether or not the file is provisioned — the rank gate runs before the file-existence check, so existence of the document isn't leaked downward.
62
+ - A Xenos hitting the endpoint sees a `403` whether or not the file is provisioned — the gate runs before the file-existence check, so existence of the document isn't leaked downward. The code is `permission_forbidden` with `{ required, held }`: BV1.R104 migrated this route (and the `/security/docs*` store below it) from `requireRank` to `requirePermission`, which admits exactly the same builders via the atom's catalog floor (ADR 0151). It read `rank_forbidden` before that.
63
63
 
64
64
  ## Cross-references
65
65
 
@@ -38,6 +38,8 @@ The default claim flow (`.claude/skills/builder-claim/SKILL.md`) ensures the tas
38
38
  - **Predicate divergence from the task spec, on purpose.** The spec cited `WHERE status='active'`, but `claims` has **no `status` column** — "active" is the table's `released_at IS NULL` convention (the row's lifecycle status lives on `tasks`). The index uses the real column.
39
39
  - **Migration safety.** A unique index *fails to create* if existing rows already violate it, which would block deploy. The migration first heals any current violation (keep the earliest active bound claim per session, close the rest — the same shape as migration 174), so the `CREATE` always succeeds; it is idempotent. The **app-layer guard is authoritative**; the index is belt-and-braces and must not break deploy.
40
40
 
41
+ > **Amended 2026-08-06 (task 1002570 / `core_204`).** The index as shipped was *stricter* than the worktree-scoped guard above: keyed on `creator_session_id` alone, it refused at the DB layer the second-worktree parallel claim the guard deliberately allows — breaking the many-sessions-per-builder norm this ADR set out to preserve. `migrations/<redacted>.sql` re-keys it to `(creator_session_id, COALESCE(worktree_name, ''))` under the name `uniq_active_claim_per_session_worktree`, same partial predicate, so the backstop now enforces exactly the guard's boundary. The `COALESCE` fold (rather than PG15's `NULLS NOT DISTINCT`) is load-bearing: a plain composite index treats NULLs as distinct, so two worktree-less claims off one shared clone would both insert and re-open the commingling hole. A `23505` from either index generation surfaces as `session_already_has_bound_claim` (task 1002589), never the false `ALREADY_CLAIMED`.
42
+
41
43
  ## Consequences
42
44
 
43
45
  - A session can hold at most one active claim — commingled ships are structurally prevented, not just discouraged.
@@ -35,7 +35,7 @@ Stand up a **cloudbongos-native box plane** alongside (not replacing) the legacy
35
35
 
36
36
  ## Follow-ups (open)
37
37
 
38
- 1. **Box first-boot convergence — ROOT-CAUSED + fixed (workaround live; proper fix open, task 1002243).** Symptom: provisioned boxes reached `active` but `cloudflared`/terminal never came up (`term-…` = CF 530) and SSH keys weren't applied. Root cause: **`box.js` provisioned droplets with EMPTY user-data** (`/var/lib/cloud/instance/user-data.txt` = 0 bytes; cloud-init logged "No user-data found"). `box.js` renders the box cloud-init from `<REPO_ROOT>/infra/` (the 26 KB `builder-box-cloud-init.yaml` + ~12 gz+b64-inlined scripts), and `fileGzB64` **silently returns `''` on a missing file**, so a missing `infra/` yields empty user-data with no error. The R89 split moved `scripts/gds/` into the `@bongos/core` package but **kept `infra/` in the instance repo** — yet `box.js` still resolves `infra/` relative to *its own* location (the vendored core), where `infra/` **isn't shipped at all** (0 files). So every box on any vendored/installed instance booted with only DigitalOcean's default user-data. **Workaround applied (temporary):** populated the vendored core's `infra/` from the instance's `infra/` (`/home/lars/cloudbongos/node_modules/@bongos/core/infra/`); re-provision then rendered a 62 KB user-data and the box converged — `cloudflared` active, `term-example-owner.cloudbongos.com` → 200 (authed), builder SSH key baked + working. **Proper fix (task 1002243):** ship `infra/` in the core publish-manifest, OR make `box.js` resolve `infra/` from the instance root (like `core-node.sh`'s candidate-base search), AND make `fileGzB64`/the template render **fail loudly** instead of emitting empty user-data. The workaround is under `node_modules` and will be lost on the next `npm ci`/core reinstall until the proper fix lands.
38
+ 1. **Box first-boot convergence — ROOT-CAUSED + CLOSED (proper fix landed; [ADR 0150](<redacted>.md), task 1002386).** Symptom: provisioned boxes reached `active` but `cloudflared`/terminal never came up (`term-…` = CF 530) and SSH keys weren't applied. Root cause: **`box.js` provisioned droplets with EMPTY user-data** (`/var/lib/cloud/instance/user-data.txt` = 0 bytes; cloud-init logged "No user-data found"). `box.js` renders the box cloud-init from `<REPO_ROOT>/infra/` (the 26 KB `builder-box-cloud-init.yaml` + ~12 gz+b64-inlined scripts), and `fileGzB64` **silently returns `''` on a missing file**, so a missing `infra/` yields empty user-data with no error. The R89 split moved `scripts/gds/` into the `@bongos/core` package but **kept `infra/` in the instance repo** — yet `box.js` still resolves `infra/` relative to *its own* location (the vendored core), where `infra/` **isn't shipped at all** (0 files). So every box on any vendored/installed instance booted with only DigitalOcean's default user-data. **Workaround applied (temporary):** populated the vendored core's `infra/` from the instance's `infra/` (`/home/lars/cloudbongos/node_modules/@bongos/core/infra/`); re-provision then rendered a 62 KB user-data and the box converged — `cloudflared` active, `term-example-owner.cloudbongos.com` → 200 (authed), builder SSH key baked + working. **Proper fix — LANDED (spike task 1002385 → [ADR 0150](<redacted>.md); build task 1002386, "R98").** `box.js` gained `boxInfraCandidates()`/`resolveBoxInfraDir()` (an ordered instance-root-first search, including the `node_modules` walk-up) plus `BOX_INFRA_TEMPLATE`; `readCloudInit` now routes every `infra/` read through the resolved dir and **throws** on a missing/incomplete `infra/` instead of silently emitting empty user-data, and `cmdProvision` refuses to provision on empty user-data. Pinned by [`tests/box_infra_resolve.mjs`](../../tests/box_infra_resolve.mjs). The temporary `node_modules` workaround described above is therefore no longer load-bearing. *(Superseded task 1002243 was closed as part of task 1002557.)*
39
39
  2. **Cloudflare token scope.** Provision warned the ADR 0053 cookie-strip Transform Rule was **not** applied — `CLOUDFLARE_API_TOKEN` lacks "Transform Rules" edit scope on the cloudbongos.com zone. Add the scope (or apply the rule once in the dashboard) **before** other builders get boxes on `*.cloudbongos.com`, so platform cookies (`cookieDomain=<redacted> aren't sent to box subdomains.
40
40
  3. **Cosmetic `otb-` prefix.** Tunnel names (`otb-term-<login>`) and the droplet name (`otb-box-<login>`) are still hardcoded in `box-onboard.js`/`box.js`. Harmless (fleet separation is by tag, and DNS is per-zone), but a branding leak worth parameterizing.
41
41
  4. **Phase 4 — retire the legacy example plane** (owner decision) — **part (a) DONE in task 1002248 (2026-07-15).** Pre-checked read-only that the legacy fleet was empty (0 `otb-builder-box` droplets on DigitalOcean; cohost intent queue `ran=0`), then `systemctl disable --now` on the four `cohost-box-*` timers (intent-runner / idle-suspend / drift-reconcile / dormant-reclaim) on the control-plane host — now `disabled`/`inactive`; the native `cloudbongos-box-*` timers were left `enabled`/`active`. **Part (b) — disabling the dev-box module on example.com itself — NOT reachable from the control plane, flagged to owner:** example's web app + instance checkout are not co-located on the cloudbongos droplet (no local checkout, no ssh alias) and the local example GDS token is expired, so a durable module-disable needs owner access to example's own config/host. Harmless meanwhile: with the runners stopped there is no cost/security impact — worst case is a box request on the legacy instance that now hangs unfulfilled.
@@ -99,7 +99,7 @@ The seed is what makes the cut-over behavior-identical. Four rank-roles are seed
99
99
  | **`xenos`** | own-work + public read: `task.claim.newcomer`, `task.ship`, `task.act.own`, `cost.log`, `learning.create`, `blocker.file`, `idea.file`, `box.manage.own`, `memory.manage.own`, `credential.manage.own`, `page.view.public` |
100
100
  | **`thetes`** | everything `xenos` has **+** `task.claim.any` (the whole general queue, still honoring each task's `requires_rank` floor), `page.view.builder` |
101
101
  | **`metic`** | everything `thetes` has **+** `me.wandering.set` (higher clamp) **+** the review/triage/authoring set: `idea.triage`, `blocker.review`, `task.{create,edit,promote,demote,vote}`, `task.dependency.manage`, `task.criterion.manage`, `criterion.satisfy`, `criterion.review`, `planning.run`, `priority.run`, `goal.create`, `goal.archive`, `goal.scope.widen`, `goal.manage.own`, `module.enable`, `module.submit`, `security.report.file`, `llm_cache.use`, `backup.manage`, `source.clone.local`, `box.source.full` |
102
- | **`archon`** | everything `metic` has **+** the **admin** operational set (`builder.status.set`, `builder.budget.set`, `task.abandon`, `criterion.unsatisfy`, `auth.session.revoke`, `access_request.review`, `security.report.adjudicate`, `provisioning.fleet.manage`, `box.fleet.manage`, `autonomy.precheck`, `discord.channels.manage`, `project.curate`, `override_request.decide`, `claim.release.any`, `idea.ratify`, `task.newcomer_restock`, `task.peer_votes.tally`, `goal.reorder`, `builder.roster.read`, `session.search`, `memory.read.any`, `audit_log.read`, `page.view.governance`) **+** the **`system`** set (`builder.rank.set`, `version.close`, `task.confirm.grade_bypass`, `dependency.cross_tier.manage`, `goal.scope.manage`, `governance.manage`, `gate_approval.decide`, `deploy.push_main`, `path.protected.modify`, `infra.ssh`). The `principal.*` permissions are **not** in any rank-role — they are held only by the machine-principal identities (`system_role='bfg'`/`'discord_bot'`), never by a human, not even an Archon (that is what gating on exact identity means). |
102
+ | **`archon`** | everything `metic` has **+** the **admin** operational set (`builder.status.set`, `builder.budget.set`, `task.abandon`, `criterion.unsatisfy`, `version.create`, `auth.session.revoke`, `access_request.review`, `security.report.adjudicate`, `provisioning.fleet.manage`, `box.fleet.manage`, `autonomy.precheck`, `discord.channels.manage`, `project.curate`, `override_request.decide`, `claim.release.any`, `idea.ratify`, `task.newcomer_restock`, `task.peer_votes.tally`, `goal.reorder`, `builder.roster.read`, `session.search`, `memory.read.any`, `audit_log.read`, `page.view.governance`) **+** the **`system`** set (`builder.rank.set`, `version.close`, `task.confirm.grade_bypass`, `dependency.cross_tier.manage`, `goal.scope.manage`, `governance.manage`, `gate_approval.decide`, `deploy.push_main`, `path.protected.modify`, `infra.ssh`). The `principal.*` permissions are **not** in any rank-role — they are held only by the machine-principal identities (`system_role='bfg'`/`'discord_bot'`), never by a human, not even an Archon (that is what gating on exact identity means). |
103
103
 
104
104
  **Why cumulative preserves the threshold.** Today `requireRank('metic')` admits metic **and** archon (threshold = at-or-above). Because the seeded `archon` role ⊇ `metic` role, the resolver's union preserves exactly that: both roles grant `idea.triage`, so `requirePermission('idea.triage')` admits both — identical to the old threshold. A builder in the compat seed holds exactly **one** rank-role, so their effective set = that role's set = exactly their rank's threshold powers today. The parity test (R103) is the mechanical proof.
105
105
 
@@ -146,6 +146,7 @@ The `system` set is deliberately **tight** — only gates whose delegation would
146
146
  | `builder.budget.set` | `PATCH /builders/:id/budget` |
147
147
  | `task.abandon` | `POST /tasks/:id/abandon` — terminal scope removal |
148
148
  | `criterion.unsatisfy` | `POST /done-when/:id/unsatisfy` |
149
+ | `version.create` | `POST /versions` — create a version + its done-when criteria. Added BV1.R105 (task 1001847): the route had no atom, so it was the one module write the migration could not finish. **Operational, not `system`,** even though `version.close` is `system` — the flag marks trust-boundary power, and creating a version is *authoring* (it confers no authority). Its siblings are `goal.create` / `task.create`. Close is the disposition that finalises a version's scope-truth; create opens it. |
149
150
  | `auth.session.revoke` | `POST /auth/revoke`, `/auth/revoke-all` |
150
151
  | `access_request.review` | `GET/PATCH /access-requests/:id` — admit a builder |
151
152
  | `security.report.adjudicate` | `POST /security/reports/:id/{confirm,dispute,fix,link-fix}`, `/security/docs` CRUD |
@@ -0,0 +1,51 @@
1
+ # 0155 — Adopting a private repo: widen the onboarding OAuth scope to `repo`
2
+
3
+ - **Status:** Accepted — built in task [#1002561](https://example.com/builders#/task/1002561).
4
+ - **Date:** 2026-08-04
5
+ - **Deciders:** the owner chose the credential approach after being shown the trade-off; Claude diagnosed + built.
6
+ - **Amends:** [ADR 0143](<redacted>.md) §1 (its Decision 1 — scope choice). 0143 otherwise stands unchanged: opt-in per sign-in, lazy consent, encrypted short-TTL token store, personal repos only, 503-degrade. Per 0143's own append-only rule, this is a new ADR, not an edit.
7
+ - **Related:** [ADR 0121](<redacted>.md) (greenfield vs brownfield onboarding).
8
+
9
+ ## Context
10
+
11
+ Standing up a project on an **existing private repo** failed. Live evidence — `provisioning_instances` id=14, `example-owner/hermeslines-marketing`, `status='error'`:
12
+
13
+ ```
14
+ git -C /srv/cloudbongos/hermeslines-marketing … push https://x-access-token@github.com/… HEAD:main
15
+ remote: Write access to repository not granted.
16
+ fatal: … The requested URL returned error: 403
17
+ ```
18
+
19
+ `scopeForWebFlow()` requested `read:user public_repo`. **`public_repo` grants write to *public* repos only**, so the provisioner's scaffold push to a private repo 403s.
20
+
21
+ Three things confirm this was scope and not credentials:
22
+
23
+ 1. The live `github_oauth_tokens` row was `scope='public_repo,read:user'` and **still valid** during the failure window. A dead token returns `401 Bad credentials`; `403 Write access to repository not granted` is GitHub's canonical *scope-insufficient* response.
24
+ 2. Every **greenfield** project ever created worked, because `createUserRepo` hardcodes `private:false` — the platform only ever pushed to repos it had just made public. Adopt is the first flow where a private repo can be the target, so this had never been exercised.
25
+ 3. The picker already renders a **"Private" badge** (`public-landing/create.html`) — UI written for a case `public_repo` made unreachable, since `/user/repos` omits private repos under that scope.
26
+
27
+ ADR 0143 §1 anticipated exactly this and named the exit: *"**Accepted limitation:** private repos can't be listed/adopted or created; revisit to `repo` only if private support is needed"*, with *"switching to `repo` (private support) is a one-line scope change"* under Consequences. This ADR takes that path.
28
+
29
+ Options weighed with the owner:
30
+
31
+ - **(A) Never let the server touch the user's repo.** For adopt, the user runs `bongos init --adopt` and pushes themselves — which the wizard already instructs — and provisioning only stands up server/DB/address. Keeps the narrow scope. Cost: a private repo still needs a read-only deploy key for the instance to *run*, and the "we set it up for you" promise weakens.
32
+ - **(B) Widen the scope to `repo` and keep pushing on the user's behalf.** One consent screen, no extra steps, adopt works for public and private alike. Cost: the persisted token can write to **every repo the user owns**.
33
+ - **(C) Public repos only.** Detect a private repo at the form and refuse early. Cheapest, but declines the feature.
34
+
35
+ ## Decision
36
+
37
+ **Take (B) — `scopeForWebFlow({repos:true})` now returns `read:user repo`** — per the owner's explicit call, having been shown that this reverses 0143's least-privilege posture and enlarges what a leaked token can reach. Within that:
38
+
39
+ 1. **`repo`, and only on the wizard path.** 0143 §2 is unchanged and load-bearing: the broadened scope is requested *only* via `?repos=1` from `/create`. Hall web sign-in and the CLI Device Flow stay `read:user`. `scopeForWebFlow` remains the single place the string is decided, and the unit test now asserts both that the wizard gets `repo` and that the default path carries **no** repo-acting scope.
40
+ 2. **A stale-scope token must re-prompt, not degrade.** Widening the *request* does not upgrade tokens already stored. A surviving `public_repo` row still lists and creates *public* repos, so without a check the wizard would 200 while **silently omitting the user's private repos** — and the real failure would resurface minutes later as the same opaque push 403. New `grantedScopeCoversRepos()` parses GitHub's comma-separated granted scope and gates `resolveRepoToken`, which now returns the existing `repo_scope_required` (403) so the wizard's well-exercised reconnect path handles it. Parsing (not substring-matching) matters: `public_repo` *contains* `"repo"`, and a naive match would accept precisely the token this guards against.
41
+ 3. **A repo *we* create stays public.** `createUserRepo` keeps `private:false`. Under `repo` it *could* create a private repo; it doesn't, because a Cloud Bongos instance is open-source / capture-resistant by design. This is now a **product** choice rather than a credential limit, it is deliberately not a parameter, and a test pins it so the widen can't silently change what we create. A user wanting a private instance adopts a private repo they made themselves.
42
+ 4. **Consent is stated in our own words first.** GitHub's screen for `repo` reads as full control of all repositories. The wizard's connect copy now says plainly that this is the only scope that can write to a private repo, and what we do with it (read the repo you pick; push the setup commit), so the user does not first learn the breadth from GitHub's wording.
43
+
44
+ ## Consequences
45
+
46
+ - **Security posture — the real cost.** The stored token is now a materially bigger honeypot: write access to every repo the user owns, public and private. 0143's containment still applies unchanged and is what makes this tolerable — encrypted at rest via secret-box, **1h TTL**, lazily swept, requested only on the wizard path, and never stored at all when secret-box is unconfigured. The blast radius is nonetheless larger than before, and this is the ADR to revisit first if a token ever leaks.
47
+ - **A GitHub App remains the principled fix.** 0143 §Context option (B) — a central App with fine-grained `Contents:write` on *only* the selected repo — gives adopt-private without the account-wide grant. It was rejected then for the install step, and this decision makes the case for it stronger, not weaker. A future ADR adopting it would supersede this one.
48
+ - **Returning users reconnect once.** Anyone holding a live pre-0155 token gets one `repo_scope_required` prompt. The 1h TTL means the affected window is small.
49
+ - **The picker gains private repos** — `/user/repos` returns them under `repo`, lighting up the dormant "Private" badge.
50
+ - **This alone does not make adopt work.** The second, independent defect is server-side: provisioning has no notion of adopt and unconditionally `git init`s a fresh tree and pushes to `main`, which an existing repo rejects as unrelated history. Task [#1002562](https://example.com/builders#/task/1002562) fixes that; [#1002563](https://example.com/builders#/task/1002563) verifies both live. **Because the wider scope makes a mistaken `--force` in that path a genuine data-loss route on a user's repo, the adopt push must never force, and 1002562 carries a test asserting it.**
51
+ - **Reversal.** Narrowing back to `public_repo` is a one-line change plus dropping the scope guard, and re-accepts 0143's limitation.
@@ -0,0 +1,62 @@
1
+ # 0156 — A builder's own display name wins, always
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-08-04
5
+ - **Deciders:** Lars (owner / Archon), Claude
6
+
7
+ **Track:** `internal` (GDS / builder experience). Task [1002568](https://cloudbongos.com/builders#/task/1002568), under the **Builder experience** goal [1000025](https://cloudbongos.com/builders#/goal/1000025).
8
+
9
+ ---
10
+
11
+ ## Context
12
+
13
+ A builder sets their display name in the hall's Settings page. It saves — the toast confirms it, the sub-header updates, the row really is written — and then it silently reverts to their GitHub account name the next time they sign in.
14
+
15
+ The write path was never the problem. `PATCH /me/display-name` validates via `modules/builder-settings/display-name.js`, writes through `modules/builder-settings/db.js`, and every read surface (hall header, leaderboard, work feed) already prefers `display_name` over `github_login`.
16
+
17
+ The problem is that `upsertBuilder` (`src/bongos/db-kernel.js`) is named for account creation but runs on **every authentication**. Its conflict branch re-synced the whole identity row from GitHub:
18
+
19
+ ```sql
20
+ ON CONFLICT (github_id) DO UPDATE
21
+ SET github_login = EXCLUDED.github_login,
22
+ display_name = EXCLUDED.display_name, -- ← the clobber
23
+ avatar_url = EXCLUDED.avatar_url
24
+ ```
25
+
26
+ `EXCLUDED.display_name` is whatever the caller just read off GitHub. All four login paths feed it: `pollDeviceFlow` (CLI setup/re-auth), `completeWebFlow` (browser hall sign-in), `completeIdpFlow` and `completeDeviceExchange` (federated hub). So the builder's own name survived exactly until their next sign-in — and because CLI re-auth counts, that is often the same working day.
27
+
28
+ That re-sync is not a bug on its own. It is why a builder who changes their name on GitHub sees the change reflected here without doing anything. Keeping that is a real requirement; the row is seeded from GitHub in the first place.
29
+
30
+ ## Decision
31
+
32
+ **Once a builder sets their own display name, GitHub never overwrites it. Builders who never renamed keep syncing from GitHub.**
33
+
34
+ The row records *who last set the name*. A new core column, `builders.display_name_source text NOT NULL DEFAULT 'github'` (migration `<redacted>`, CHECK-constrained to `'github' | 'user'`), carries that one bit:
35
+
36
+ - `setBuilderDisplayName` (the only path a builder can rename themselves through) stamps `display_name_source = 'user'` alongside the name.
37
+ - `upsertBuilder`'s conflict branch guards the assignment:
38
+
39
+ ```sql
40
+ display_name = CASE WHEN builders.display_name_source = 'user'
41
+ THEN builders.display_name
42
+ ELSE EXCLUDED.display_name END
43
+ ```
44
+
45
+ `github_login` and `avatar_url` keep re-syncing unconditionally — neither is user-editable, and `github_login` is identity, not a label.
46
+
47
+ `'github'` is the correct default for every pre-existing row: before this change there was no durable user-set name, so whatever is stored *is* the GitHub-derived value. A builder who had been fighting the revert re-saves once and it sticks.
48
+
49
+ ### The rejected alternative: just delete the line
50
+
51
+ The one-line fix is to drop `display_name = EXCLUDED.display_name` from the conflict branch entirely. No migration, no new column, no new state.
52
+
53
+ It was rejected because it fixes one builder's problem by taking a feature away from everyone else. `display_name` would then be frozen at whatever GitHub reported the day the account was created. A builder who renames themselves on GitHub — or who had no GitHub display name at signup and so was seeded with their bare login — would be stuck with a stale label forever, with the hall Settings editor as the only way out. The revert bug affects builders who deliberately renamed; the frozen-name bug would affect everyone who never touched Settings, which is most of them. Trading a visible bug for a silent one is a bad trade.
54
+
55
+ The chosen fix costs one boolean-shaped column and keeps both behaviours correct.
56
+
57
+ ## Consequences
58
+
59
+ - **New core column.** `builders.display_name_source`. Core, not `modules/builder-settings/migrations/`: the column sits on the core `builders` table and the guard that reads it lives in core `db-kernel.js` on the auth path. A module migration may only touch its own namespaced objects ([ADR 0083](<redacted>.md) §Decision #5), and core schema must not depend on whether a module is enabled. Same placement as the other `builders`-table preference columns (037, 075, 119, 164).
60
+ - **Not surfaced in the API.** `GET /me/display-name` and `docs/api/openapi.json` are unchanged. The flag is internal persistence state; no UI copy branches on whether a name is GitHub-derived, so exposing it would be surface with no consumer.
61
+ - **The federated hub needs no matching guard.** `modules/platform-identity/platform-identity.js`'s `upsertAccount` does the same unconditional re-sync on `platform_identity_accounts`, and its `display_name` reaches an instance through `completeIdpFlow`. It does not need changing: the `CASE` above runs regardless of what `EXCLUDED` carries, so an instance's `builders.display_name` is protected no matter which login path supplied the value. And `platform_identity_accounts` is identity-only with no self-service editor — there is no user-set value there to protect.
62
+ - **Regression-tested where it broke.** `tests/display_name.mjs` previously covered only the pure validator, which is exactly why this shipped: the half that was correct was the half under test. It now also reads the real SQL both halves emit and asserts the conflict branch's behaviour — a renamed builder keeps their name against a different incoming GitHub name, and a never-renamed builder still picks up a GitHub rename.
@@ -0,0 +1,93 @@
1
+ # 0157 — Archon is rank and identity only; the operational block delegates to Metic
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-08-05
5
+ - **Deciders:** Lars (Archon), Claude
6
+
7
+ **Builds on / amends:** [ADR 0016](<redacted>.md) (server-enforced trust boundary — **unchanged**, this ADR moves floors inside it, never the enforcement point), [ADR 0151](<redacted>.md) (permissions-as-atom — this ADR is only possible because the floor is declared once, in `catalog.js`), [ADR 0090](<redacted>.md) / [ADR 0152](<redacted>.md) (the two earlier one-power delegations this generalises), [ADR 0154](<redacted>.md) (**amends** — its "unsatisfy stays Archon" carve-out is superseded), [ADR 0086](<redacted>.md) (protected-scope goals — **unchanged**, `goal.scope.manage` stays Archon).
8
+
9
+ **Track:** `internal` (GDS / methodology). Task [1002584](https://cloudbongos.com/builders#/task/1002584).
10
+
11
+ ---
12
+
13
+ ## Context
14
+
15
+ The Archon rank had accumulated 50 route gates. Almost none of them were about *authority*; they were about **administration** — reading the roster, listing dev boxes, viewing the audit log, adjudicating a vulnerability report, tearing down an idle droplet, reordering a queue, ratifying an idea. Each arrived Archon-gated for the same defensible reason: [ADR 0016](<redacted>.md) sets the default posture for any new privileged route to `requireRank('archon')` until a lower rank is explicitly granted — fail closed. Nothing ever came back to lower them, so "fail closed" quietly became "the owner is the only operator."
16
+
17
+ Two earlier ADRs had already delegated one power each on the same reasoning — [ADR 0090](<redacted>.md) (a rank trusted to *triage* ideas is trusted to *seed* the work they become) and [ADR 0152](<redacted>.md) (a rank trusted to author work is trusted to retire it). This ADR generalises that argument instead of relitigating it one route at a time.
18
+
19
+ Lars's directive (2026-08-05): **"Move all of these to metic, and make archon just responsible for rank and identity."**
20
+
21
+ ### Why this is now a small change
22
+
23
+ The literal reading of that directive — edit 41 `requireRank('archon')` call sites — was the wrong shape, and briefly the plan. BV1.R104/R105 (jaxri, tasks [1001846](https://cloudbongos.com/builders#/task/1001846)/[1001847](https://cloudbongos.com/builders#/task/1001847)) had migrated the route gates to `requirePermission(...)` and were sitting at `confirmed` — graded, verified, unmerged — for five days. They landed mid-session.
24
+
25
+ Once every gate reads `requirePermission('<atom>')`, the rank floor is declared in exactly one place: `floor:` in [`modules/governance/catalog.js`](../../modules/governance/catalog.js). The delegation collapses from 41 call-site edits to **20 one-word floor changes in one table**. That is the ADR 0151 payoff arriving in practice, and it is worth recording as the concrete return on that refactor: the same owner decision was a 41-file diff on Monday and a 1-file diff on Tuesday.
26
+
27
+ ## Decision
28
+
29
+ ### 1. Archon retains rank, identity, and four escalation keys — 13 atoms
30
+
31
+ | Atom | Why it stays |
32
+ |---|---|
33
+ | `builder.rank.set` | **rank** — the single writer of `builders.rank`. The whole point. |
34
+ | `builder.status.set` | **identity** — offboard / (de)activate: who exists. |
35
+ | `access_request.review` | **identity** — who is admitted at all. |
36
+ | `auth.session.revoke` | **identity** — whose sessions are valid. |
37
+ | `task.confirm.grade_bypass` | escalation key (below) |
38
+ | `gate_approval.decide` | escalation key (below) |
39
+ | `goal.scope.manage` | escalation key (below) |
40
+ | `governance.manage` | escalation key (below) |
41
+ | `deploy.push_main`, `path.protected.modify`, `infra.ssh` | substrate-enforced (pre-push hook, `main-audit.js`), outside the HTTP boundary — [ADR 0043](<redacted>.md). Not reachable to delegate here. |
42
+ | `version.close` | see §3 |
43
+
44
+ ### 2. The four escalation keys, and why a literal reading of the directive was refused
45
+
46
+ `builder.rank.set` staying Archon is **not sufficient** to keep "only the Archon sets rank" true. Four other atoms form a chain that ends in self-promotion, and every link was Archon-gated, which is why the chain was closed:
47
+
48
+ 1. Create a goal scoped to a **protected** module — the permission core itself (`goal.scope.manage`).
49
+ 2. Claim a task in it. **Already allowed**: `PROTECTED_GLOBS` floors at *metic*, by [ADR 0043](<redacted>.md)'s design.
50
+ 3. Write code deleting the rank check, then confirm past the failed grade (`task.confirm.grade_bypass`).
51
+ 4. Approve your own red CI deploy gate (`gate_approval.decide`) → it lands on `main`.
52
+
53
+ Plus `governance.manage`, whose routes R110/R111 have not built yet: it grants roles and permissions directly, so holding it means granting yourself anything — the meta-escalation.
54
+
55
+ Moving all 20 as literally asked would have opened that chain, making the delegation **self-reversible** — a Metic could reach Archon, at which point "Archon owns rank" is decoration. The concern was put to the owner with the chain spelled out; the owner chose to keep all four. Recording this because the reasoning is not visible from the floor values alone: *four of these look operational and are not.*
56
+
57
+ **This is the load-bearing invariant of this ADR:** the delegated set must contain no path to raising your own rank. A future delegation must re-run that check rather than assume it.
58
+
59
+ ### 3. Two atoms held back beyond the four, each for a stated reason
60
+
61
+ - **`version.close`** — [`docs/canonical-permissions.md`](../canonical-permissions.md) has long paired closing a version with setting rank as the two powers that never delegate. Closing a version is the disposition that finalises shipped scope-truth. Kept, and flagged to the owner as a judgment call rather than a derivation. Its sibling `version.create` **did** move: authoring opens a container, disposition finalises one — the asymmetry is deliberate and is now recorded at both atoms.
62
+ - **`dependency.cross_tier.manage`** — `system: true`, and `tests/governance_catalog.mjs` enforces that no `system` permission is granted below archon. Moving it would mean clearing the `system` flag, which widens what any **custom role** can be granted — a second-order authority change nobody asked for. The flag is the wall; it was not quietly lowered to satisfy a floor change.
63
+
64
+ ### 4. `gate_approval.read` is minted, splitting the Gate page's read from its write
65
+
66
+ `page.view.governance` moving to metic opens `/watch`, `/harbor`, `/gate` to the working rank. But `GET /gate-approvals` — the Gate page's only data endpoint — was still `requireRank('archon')`, one of three read routes R104/R105 deliberately skipped because their atom was declared over the module's *writes* only.
67
+
68
+ Left alone, a Metic would open `/gate` and get a page rendering nothing but a 403. So: `gate_approval.read` (metic) for the list, `gate_approval.decide` (archon, `system`) for approving a held PR. The same "the atom must name the read" step resolves the other two skipped gates — `GET /projects` (`project.curate`) and `GET /security/reports` (`security.report.adjudicate`) — whose write atoms had already moved to metic, which would otherwise have left the **read stricter than the write**: a Metic able to confirm a vulnerability report but not list one.
69
+
70
+ ### 5. Pins move from ranks to atoms
71
+
72
+ `EXPECTED_RANKS` / `MODULE_EXPECTED_RANKS` in [`route-rank-check.js`](../../src/bongos/route-rank-check.js) pinned these routes to the literal rank `'archon'`. Rank pins break on every floor change; `pinMismatch` compares **atom identity** for `perm:` pins. The affected pins are now `perm:<atom>`, so the guarded invariant is *"this route gates on this permission"* — which is the property actually worth protecting — and a future owner decision to move a floor needs no pin edit.
73
+
74
+ ## Consequences
75
+
76
+ **What a Metic gains.** The Watch / Harbor / Gate pages and the session log; the dev-box and provisioning fleets (including force-teardown and per-builder blocks); security-report adjudication; the audit log; the builder roster and per-builder grades; cross-builder memory reads; project curation and scouting; budget setting; idea ratification; override-request decisions; release-on-behalf; criterion un-satisfy; goal reorder; newcomer restock; peer-vote tally; version creation.
77
+
78
+ **The owner's remaining day-to-day surface** is rank changes, admissions, offboarding, session revocation, grade-bypass confirms, CI gate approvals, protected-scope goals, and version close. That is the intended shape: the owner sets *who may act and how far*; the working rank *acts*.
79
+
80
+ **Two consequences worth a second look** (both flagged to the owner, neither blocking):
81
+
82
+ 1. **The exploit cookbook widened.** `security.report.adjudicate` guards `/security/docs` — the engagement-report store holding attacker-usable exploit detail — so that is now Metic-readable. Every other delegated atom widens who may *act*; this one widens who may *read*. If unwanted, the fix mirrors §4: split a `security.docs.read` at archon and let adjudication delegate. Recorded in `tests/security_docs.mjs` at the assertion that used to guard it.
83
+ 2. **The backward ratchet delegated.** [ADR 0154](<redacted>.md) made "unsatisfy stays Archon" the explicit safety argument for delegating criterion *create* — the forward ratchet moved, the backward one did not. `criterion.unsatisfy` is neither rank/identity nor an escalation key, so under this directive it moved, superseding that carve-out. What still holds is the part that never depended on rank: satisfy and unsatisfy remain **separate atoms**, so confirming a criterion never confers the power to un-confirm one.
84
+
85
+ **A floor change needs a migration.** `governance_002`'s compat seed is run-once, so an edited `floor:` never reaches an already-migrated database. [`<redacted>.sql`](../../modules/governance/migrations/<redacted>.sql) grants the 20 to `metic` (+ `gate_approval.read` to both, since it is new). Lowering a floor is additive, so `tests/governance_seed.mjs`'s union model handles it and revokes nothing.
86
+
87
+ **`requireArchonPage` is now a misnomer** — it resolves `page.view.governance`, floor metic. Renaming it touches serve-internal, auth, and several test files; left deliberately to keep this diff small in authz code, and filed separately. The hall nav group was relabelled **Archon → Governance**, because a header reading "Archon" over items a Metic can open misdescribes the ladder to exactly the builders still learning it.
88
+
89
+ > **Resolved (2026-08-05, [#1002588](https://cloudbongos.com/builders#/task/1002588)).** That follow-up landed: the gate is now `requireGovernancePage`. It is a pure rename — same `page.view.governance` permission, same `deniedTo`, same audience. References to `requireArchonPage` in this ADR and in ADRs [0036](<redacted>.md) / [0058](<redacted>.md) / [0151](<redacted>.md) are left as written, since they describe what was true at the time; `src/bongos/auth.js` carries a comment naming the old spelling so a grep from any of them lands on the trail.
90
+
91
+ **What did not change.** [ADR 0016](<redacted>.md) end to end: authority is still resolved server-side, per request, uncached, from `builders.rank` via the seeded rank-roles. A builder editing their checkout still cannot grant themselves anything. This ADR moves floors *inside* that boundary; it does not touch the boundary.
92
+
93
+ **The ADR 0016 default posture still stands** for *new* routes: start at `requireRank('archon')` / an archon-floored atom and delegate deliberately. This ADR is that deliberate step taken in bulk for the accumulated backlog — not a licence to skip it.