@bongos/core 1.18.0 → 1.18.18

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 (553) hide show
  1. package/.bongos-core.json +1740 -640
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +5 -3
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +11 -2
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -0,0 +1,229 @@
1
+ # 0151 — Governance: permissions as the atom, ranks as seeded roles
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-07-24
5
+ - **Deciders:** Lars (owner / Archon), Claude
6
+ - **Track:** `internal` (GDS / methodology — a Cloud Bongos platform capability)
7
+ - **Task:** BV1.R90 ([#1001838](https://cloudbongos.com/builders#/task/1001838)) — the foundation of the BONGOS-V1 **Governance** goal ([#1000030](https://cloudbongos.com/builders#/goal/1000030)); settles the model in an ADR before any of R93–R114 build against it.
8
+ - **Builds on:** [ADR 0016](<redacted>.md) (the server-enforced HTTP trust boundary — **preserved untouched**), [ADR 0043](<redacted>.md) (the out-of-band git/ssh substrate + protected-path rank floor), [ADR 0062](<redacted>.md) (core ↔ host split), [ADR 0083](<redacted>.md) (modules), [ADR 0084](<redacted>.md) (auto-derived per-task rank floor), [ADR 0086](<redacted>.md) (goal-scoped hierarchy + cross-tier edges), [ADR 0090](<redacted>.md) (Metic authoring powers), [ADR 0106](<redacted>.md) (ownership authority).
9
+ - **Gates:** R91 (module key) · R93/R94 (tables + catalog code) · R95 (resolver) · R97 (`requirePermission`) · R98–R101 (principals / session-scopes / ownership / registry) · R96/R102 (core-host split) · R104–R109 (gate migration) · R110/R111/R113 (the tab) · R103/R112/R114 (proofs). This ADR is the decision those tasks implement.
10
+
11
+ ---
12
+
13
+ ## Context
14
+
15
+ Authority in Cloud Bongos is a **single rank threshold** today. [ADR 0016](<redacted>.md) put authority in one DB column (`builders.rank`), checked server-side per request with no caching; [ADR 0018](<redacted>.md) / [ADR 0034](<redacted>.md) wired the four live ranks into the ordered tier `xenos (1) < thetes (2) < metic (3) < archon (4)`. Every privileged route calls `requireRank(...allowed)`, which admits a builder whose rank sits **at or above** the lowest-authority rank named (a threshold, [#591](https://cloudbongos.com/builders#/task/591)). That design is sound and is **not** being replaced — it is the substrate this ADR builds *on top of*.
16
+
17
+ But a rank is an **all-or-nothing bundle**. There is no way to say "this builder may triage ideas but not author tasks," or to stand up a trusted reviewer who is not a full Metic, without inventing a new rank and threading it through a dozen-plus hand-copied tier maps (§Decision 2 counts 13). The owner wants a **Discord-style roles-and-permissions system**: configurable roles, assignable to builders, composed from granular permissions — where **permissions are the atomic primitive and ranks become seeded roles** — delivered with **zero day-one behavior change** and **without weakening the trust boundary** that is Cloud Bongos's one hard-to-copy moat ([ADR 0087](<redacted>.md): *compete on governance, not tooling*).
18
+
19
+ The 2026-07-03 governance planning session ([goal 1000030](https://cloudbongos.com/builders#/goal/1000030)) locked the shape and seeded the R90–R114 task chain against six done-when criteria (C1–C6). This ADR records the decision so those tasks build against **one** settled model instead of re-litigating it fourteen times, and it enumerates the **full permission catalog** (the appendix) grounded in the 2026-07-24 authority sweep of every server-side gate in the tree.
20
+
21
+ **What a rank threshold cannot express (the axes a role model must cover).** The sweep found four distinct *kinds* of authority already live in the code that a flat rank ladder flattens together:
22
+
23
+ 1. **Rank-derived power** — `requireRank('metic')` and friends. The bulk. Maps cleanly to roles.
24
+ 2. **Machine principals** — the BFG session-eval uploader and the Discord bot are gated on their **exact `system_role`** (`requireBfgPrincipal`, `requireDiscordBot` in `src/bongos/auth.js`), *not* rank: an Archon token is rejected because it carries `system_role = NULL`. A governance axis roles don't cover.
25
+ 3. **Session-scopes** — a dev-box token (`session.source = 'box'`) is **deny-by-default**: it may reach only the routes that opted in via `allowBoxScope`, *regardless of how senior its builder is*. Capability attached to the session, orthogonal to the builder's rank.
26
+ 4. **Ownership-scoped authority** — "act on **own** X" and "act on a goal you **own/manage**." Enforced today by in-handler pure gates (`authorizeMemberRoleChange`, [ADR 0106](<redacted>.md)) and by row-level ownership checks, not by `requireRank`.
27
+
28
+ A model that claims to be "the complete authority model" (criterion C3) has to represent all four.
29
+
30
+ ---
31
+
32
+ ## Decision
33
+
34
+ Six pillars, one per criterion.
35
+
36
+ ### 1. Permissions are the atom; ranks become seeded roles (C1)
37
+
38
+ A **permission** is the atomic unit of authority — a stable string key (e.g. `idea.triage`, `task.create`, `builder.rank.set`) that maps to a real capability (a route, a claim rule, a page). Permissions are **defined in code, not UI-invented data**: because each key corresponds to an actual server capability, the catalog is a code constant that the resolver, the gates, and the tab all read from one place (R94). A permission that no route consumes does not exist.
39
+
40
+ A **role** is a named, assignable bundle of permissions (`governance_roles` + `governance_role_permissions`). A **builder** holds zero or more roles (`governance_builder_roles`).
41
+
42
+ The four live ranks become **four seeded roles** — `xenos`, `thetes`, `metic`, `archon` — whose grants reproduce today's rank thresholds **exactly** (the compat seed, below). `builders.rank` stays as the identity/graduation column it is today; the seed keeps each builder's role assignment in lock-step with their rank. Ranks do not disappear — they become the *default* roles a fresh instance ships, and the anchor the auto-graduation ([#692](https://cloudbongos.com/builders#/task/692)) and promotion ladder still move along.
43
+
44
+ A per-request, **uncached** resolver `resolveBuilderPermissions(builderId)` returns a builder's **effective permission set** = the union of their roles' permissions, read fresh on every request (R95). This preserves ADR 0016's single most important property — **instant demotion, no stale-authority window** — because a role change, like a rank change today, takes effect on the builder's very next request. The resolver is exposed through a **kernel port** so core `auth` resolves permissions without importing the `governance` module (respecting the ADR 0083 core-never-imports-a-module boundary; the module registers the port, core calls it).
45
+
46
+ A new middleware `requirePermission(...perms)` gates a route on the caller holding **all** of the named permissions (fail-closed: 403 on any missing). `requireRank` keeps working unchanged throughout the migration — the seeded rank-roles make `requireRank('metic')` and `requirePermission(<the metic-role's keys>)` equivalent — so gates migrate route-by-route with no flag day (R97).
47
+
48
+ ### 2. Every DB-resolvable authority gate reads permissions, not rank (C2)
49
+
50
+ Once the primitive exists, the gates migrate off rank:
51
+
52
+ - **Core routes** (`src/bongos/routes/`) and the **~60 module route gates** (`modules/*/routes/*`) move `requireRank(...)` → `requirePermission(<key>)` with the catalog key that names each route's capability, behavior verified identical against the parity intent (R104, R105).
53
+ - **Claim eligibility** — today rank-driven (`xenosClaimAllowed` / the per-task `requires_rank` floor) — is re-expressed as permissions (`task.claim.newcomer` vs `task.claim.any`), with the per-task floor still honored and the abuse cases still blocked (R106).
54
+ - **HTML page gates** (`requireArchonPage` / `requireNonXenosPage` / `requireBuilderPage`) and the **wandering-prefs rank-default/ceiling clamp** resolve via permissions / role metadata (R107).
55
+ - The **duplicated rank-ladder / tier / order maps** collapse behind the resolver, with a fitness guard forbidding a new parallel map (R108). The 2026-07-24 sweep found **13 definition sites across 5 families** — `RANKS` (the full 16-value ladder, ×2 "twinned"), `LIVE_RANK_LADDER` (×2), `REQUIRES_RANK_TIER`/`RANK_TIER` (×3, one a deliberately-local copy in `permission-path-check.js`), `RANK_ORDER` (×4, one **0-indexed** while the tier maps are 1-indexed), and four one-off rank `Set`s — several commented as hand-kept copies. This is the exact drift class that *caused the Builder 25 incident* (a tier map that forgot `thetes` — [ADR 0043](<redacted>.md)); centralizing it is a security win, not just tidiness.
56
+ - The **route-rank-check fitness gate** learns to read `requirePermission`, re-expresses its EXPECTED pins in permission terms, and still hard-fails the build on an ungated or under-gated write route (R109).
57
+
58
+ ### 3. The complete authority model — roles + principals + session-scopes + ownership-scoped permissions (C3)
59
+
60
+ The model represents all four axes from §Context as **first-class types**, not as rank special-cases:
61
+
62
+ - **Roles** — the rank-seeded and custom bundles of §1. The common case.
63
+ - **Principals** (R98) — a **machine identity** is a first-class principal type, its capability keyed on `system_role` (the existing `bfg` / `discord_bot` gate), **provisioned out-of-band and never UI-grantable**. The resolver treats a principal's capability distinctly from a builder's roles; the governance tab *shows* principals but cannot *grant* to them. (A human token can never acquire a principal capability — that is the point of gating on exact identity.)
64
+ - **Session-scopes** (R99) — a **session-scope** is a first-class capability the resolver honors: a box-scoped session's effective permissions are its **deny-by-default allow-list**, intersected with (never widening) its builder's role permissions — so a box session is limited even when its builder is an Archon. This models the current ad-hoc `allowBoxScope` flag explicitly.
65
+ - **Ownership-scoped permissions** (R100) — "act on **own** X" is a **parameterized** permission distinct from the global grant: `box.manage.own` ≠ `box.manage.any`, `claim.act.own` ≠ `claim.act.any`, likewise own-memory / own-credential, and the goal owner/manager authority of [ADR 0106](<redacted>.md). A route requires the scoped form; the resolver + an ownership predicate (the in-handler pure-gate pattern) decide it against the target row. Existing own-scoped routes migrate to it.
66
+
67
+ ### 4. Policy owns / substrate enforces — the protected-surface registry + the system-permission wall (C6 / R101)
68
+
69
+ This is the pillar that keeps configurability from becoming a foot-gun, and it has two halves.
70
+
71
+ **The `system` flag.** Every permission in the catalog is flagged **`system: true`** (trust-boundary, **non-grantable**) or **`system: false`** (operational, grantable). A `system` permission — deploy, protected-path modification, rank-setting, version disposition, grade-bypass, cross-tier structural edges — **cannot be added to a custom role**; the governance API rejects the grant server-side and the tab renders it **LOCKED** (visible, so the model is legible; not togglable). `system: false` permissions are the ones an Archon may compose into custom roles freely. The rule for the flag: *a permission is `system` iff granting it to a non-Archon would breach the trust boundary or a scope/quality gate* (privilege management, scope disposition, quality-gate bypass, cross-tier structural edges, and everything the out-of-band substrate enforces).
72
+
73
+ **The protected-surface registry.** Today's rank→surface map — `PROTECTED_GLOBS` + the per-tier `EXPECTED_RANKS` — is baked into code (`src/bongos/permission-path-check.js`). Governance takes ownership of it **as data**: which paths are protected, and at what floor, becomes a governance-owned registry. The **out-of-band substrate stays the enforcer** — the pre-push hook, the grader pre-pass, `main-audit.js`, `gate-review.js`, and the goal-scope check *read the registry* instead of a hardcoded list, but enforcement never moves into the app. The Metic floor and the `system`-permission mapping *flow from* the registry. The registry (R101) is also the moment to **close the [ADR 0087](<redacted>.md) §3 path-matching gap** the sweep confirmed: `permission-path-check.js`'s `normalize()` does backslash→slash + strip-`./` only — **no case-folding and no Unicode normalization** — so a case-varied or homoglyph path evades the protected-glob match (Linux-deployed, so latent today, but it is the Cursor-class `.cursorignore`/case-bypass CVE shape). The registry matcher should normalize (case-fold + Unicode NFC) and gate file **creation** as strictly as edits.
74
+
75
+ The invariant this buys, stated as a testable promise (R112, R114): **the tab can re-map which surfaces are protected and at what floor (policy), but it cannot move enforcement into the request path (substrate).** An Archon can create a custom role and grant it every `system: false` permission in the catalog, assign it to a `xenos`, and that builder gains exactly those operational powers — **while a `system` permission the role "has" stays enforced by the substrate**: the deploy / protected-path / SSH attempt is still blocked, because [ADR 0043](<redacted>.md)'s layer-3 audit reads the real git history and the real registry, independent of anything the app grants. Config owns policy; the substrate owns enforcement; a forced grant cannot cross that line. (Note: `main` on this repo still has **no** GitHub branch protection — re-verified 2026-07-24, [ADR 0043](<redacted>.md) §"Branch-protection status" — so `main-audit.js` remains the *authoritative* substrate layer, not a redundant net. The registry does not change that; it only makes the protected list data instead of a constant.)
76
+
77
+ ### 5. Capability in core, settings per project (C4)
78
+
79
+ Governance is a **core module** (`modules/governance/`, `default: true`) — a Cloud Bongos instance ships configurable roles the way it ships its status page. The *capability* (tables, resolver, catalog, gate, tab) lives in core; the *settings* (which roles exist, their starting grants) are **per-instance config**, mirroring the branding pattern:
80
+
81
+ `config/governance.json` (instance) resolves over `config/governance.neutral.json` (neutral starter, ships with the core package) with an env override on top — the exact `env → instance → neutral` precedence of `src/branding.js` (`resolveBranding` / `deepMerge`) (R96). The neutral config seeds the four rank-role templates a fresh instance inherits; **live** role assignments live in the DB (`governance_builder_roles`, R93), never in config. A vanilla instance boots with sane default roles, no OTB-specific identity, and the trust boundary intact — proven by a vanilla-boot fitness check (R102).
82
+
83
+ The tables are **module-owned, additive migrations** (`governance_roles`, `governance_role_permissions`, `governance_builder_roles`, all `governance_*`-namespaced per ADR 0083's module-migration rule), plus a `governance` scope-map key covering `modules/governance/**` + `config/governance*.json` (protected, since it overlaps the authz core — R91).
84
+
85
+ ### 6. The Archon Governance tab + the proofs (C5, C6)
86
+
87
+ - **API** (R110): routes to list roles/permissions/assignments, create a role, toggle an **operational** permission on a role, and assign a role to a builder — all gated `governance.manage` (an Archon-seeded, itself-`system` permission), with `system` permissions **rejected as non-grantable server-side** and the routes pinned in route-rank-check.
88
+ - **Page** (R111, R113): `modules/hall-ui/public/governance.{html,js}` renders the role list, a per-role permission grid (toggle operational perms; `system` perms render LOCKED), and assign-role-to-builder — Discord-style, theme-aware, behind an Archon nav entry + page gate (defense-in-depth on top of the API gate).
89
+ - **Proofs**: a **parity test** (R103) enumerates every current builder + rank and asserts their resolved permission set grants **exactly** today's powers — no more, no less (red if the seed drifts). An **abuse-matrix test** (R112) asserts every attempt to grant a `system` permission to a custom role / low-trust builder is rejected, and the substrate still blocks the action even if a grant were forced. A **live demo** (R114) closes the goal: an Archon builds a custom role, assigns it, the builder performs the operational action — while a `system` permission stays walled.
90
+
91
+ ---
92
+
93
+ ## The compatibility seed (the "zero day-one change" guarantee)
94
+
95
+ The seed is what makes the cut-over behavior-identical. Four rank-roles are seeded with **cumulative (nested-superset)** grants, and every existing builder is backfilled the role matching their current rank (R93):
96
+
97
+ | Seeded role | Grants (cumulative — each ⊇ the one above) |
98
+ |---|---|
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
+ | **`thetes`** | everything `xenos` has **+** `task.claim.any` (the whole general queue, still honoring each task's `requires_rank` floor), `page.view.builder` |
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). |
103
+
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
+
106
+ **One deliberate, fail-closed divergence.** The threshold check currently *also* admits the reserved-but-unwired ranks **above** archon (`olympian`…`demigod`) to `requireRank('archon')` gates. The seed maps **only the four live ranks** to roles; a reserved rank maps to **no role → no permissions → fail-closed**. No current builder holds a divine rank, so parity holds for the actual roster; but the model is stricter than today's *latent* threshold-admit-divine edge. This is intended (fail-closed beats fail-open for an unwired rank) and is called out for R103/R106 so the parity test asserts against the live roster, not the reserved enum.
107
+
108
+ ---
109
+
110
+ ## The permission catalog (appendix)
111
+
112
+ Enumerated from the **2026-07-24 authority sweep** ([`docs/governance/<redacted>.md`](../governance/<redacted>.md) — 81 guarded routes + the ownership/page/substrate gates), grouped here by **capability** (one key may cover a set of sibling routes; the sweep doc holds the exhaustive 1:1 route→gate list R94 transcribes). `sys` = `system` flag (✔ = non-grantable, LOCKED in the tab). Ownership-scoped keys carry an `own`/`any` split. This catalog + the sweep doc are the single source R94 transcribes into `modules/governance/` code constants; R93 seeds roles against it.
113
+
114
+ ### System permissions (`system: true` — non-grantable; substrate- or Archon-enforced)
115
+
116
+ `system` iff granting to a non-Archon would breach the trust boundary or a scope/quality gate — privilege management, scope disposition, quality-gate bypass, cross-tier structural edges, and everything the out-of-band substrate enforces.
117
+
118
+ | Key | Guards (route / surface) | Enforced by | sys |
119
+ |---|---|---|---|
120
+ | `builder.rank.set` | `PATCH /builders/:id/rank` — the single rank writer; one-rung, upward | HTTP (`requireRank('archon')`) | ✔ |
121
+ | `version.close` | version close disposition (never delegated below Archon — canonical) | HTTP | ✔ |
122
+ | `task.confirm.grade_bypass` | `POST /tasks/:id/confirm` — ratify past a failed grade (env-relaxable; see note) | HTTP + grader | ✔ |
123
+ | `dependency.cross_tier.manage` | `POST\|DELETE /dependencies` — cross-tier structural edges ([ADR 0086](<redacted>.md)) | HTTP | ✔ |
124
+ | `goal.scope.manage` | cross-tier goal-scope edges + protected-scope admission ([ADR 0086](<redacted>.md)) | HTTP + goal-scope-check | ✔ |
125
+ | `governance.manage` | the governance tab's write routes (roles/perms/assignments) — meta-escalation | HTTP (`requireRank('archon')`) | ✔ |
126
+ | `gate_approval.decide` | `POST /gate-approvals/:pr/approve` — CI deploy-gate override | HTTP + gate-review substrate | ✔ |
127
+ | `deploy.push_main` | `git push origin main` / the deploy | **substrate** (pre-push, main-audit, deploy gate) | ✔ |
128
+ | `path.protected.modify` | modifying a `PROTECTED_GLOBS` path below the Metic floor | **substrate** (3 layers, [ADR 0043](<redacted>.md)) | ✔ |
129
+ | `infra.ssh` | `ssh` to the droplet / operator surface | **substrate** (credential placement, not app) | ✔ |
130
+ | `principal.bfg` | `/memory/bfg/*` — `requireBfgPrincipal` (exact `system_role='bfg'`) | HTTP (identity, not rank) | ✔ |
131
+ | `principal.discord_bot` | Discord bot routes — `requireDiscordBot` (exact `system_role`; dormant) | HTTP (identity, not rank) | ✔ |
132
+
133
+ The `system` set is deliberately **tight** — only gates whose delegation would *compromise the platform* (escalate rank, bypass the grader/deploy gate, touch protected paths/infra, or re-grant permissions). Every other authority — including many currently archon-gated **admin** ops (offboard, abandon, onboarding review, fleet teardown) — is **operational** and therefore *delegable*, which is the whole value of the model: an Archon can stand up a "moderator" or "onboarder" role without handing out deploy. (The exact system/operational line for a couple of scope-disposition ops is R94's to finalize; this ADR sets the rule and the default.)
134
+
135
+ > **Env-relaxable exception (flagged for R94/R106):** `POST /tasks/:id/confirm` is archon-only *unless* `GRADER_BYPASS_ENABLED=1` (read live per request, [ADR 0041](<redacted>.md)), which relaxes the middleware to any-builder — then the handler re-narrows in-code via `gateTaskOwnership` (claim holder or Archon only). The permission model represents this as `task.confirm.grade_bypass` (system) with a documented env override on the *substrate* side; the grant is never widened by config, only by the operator env flag.
136
+
137
+ ### Operational permissions (`system: false` — grantable to custom roles)
138
+
139
+ "Floor" = the rank that holds this permission *today* via the compat seed; it is the seed's job, not an intrinsic property — a custom role may be granted any of these regardless of floor.
140
+
141
+ **Admin ops — floor `archon` today (delegable to a custom role; that is the point):**
142
+
143
+ | Key | Guards (route / surface) |
144
+ |---|---|
145
+ | `builder.status.set` | `PATCH /builders/:id/status` — offboard / (de)activate |
146
+ | `builder.budget.set` | `PATCH /builders/:id/budget` |
147
+ | `task.abandon` | `POST /tasks/:id/abandon` — terminal scope removal |
148
+ | `criterion.unsatisfy` | `POST /done-when/:id/unsatisfy` |
149
+ | `auth.session.revoke` | `POST /auth/revoke`, `/auth/revoke-all` |
150
+ | `access_request.review` | `GET/PATCH /access-requests/:id` — admit a builder |
151
+ | `security.report.adjudicate` | `POST /security/reports/:id/{confirm,dispute,fix,link-fix}`, `/security/docs` CRUD |
152
+ | `provisioning.fleet.manage` | `GET /provisioning/fleet[/cost-ledger]`, `POST …/force-teardown` |
153
+ | `box.fleet.manage` | `GET /boxes[/cost-ledger]`, `POST /boxes/:b/close`, `PATCH …/block` |
154
+ | `autonomy.precheck` | `GET /autonomy/precheck` — unsupervised-runner gate |
155
+ | `discord.channels.manage` | `GET /discord/channels/plan`, `POST …/reconcile` |
156
+ | `project.curate` | platform-identity: `POST /projects`, `/projects/invite`, `PATCH /projects/:id/featured`, `GET /scouting` |
157
+ | `override_request.decide` | `GET /override-requests`, `POST …/:id/decide` |
158
+ | `claim.release.any` | `POST /claims/:id/release-on-behalf` |
159
+ | `idea.ratify` | `POST /inbox/:id/ratify` |
160
+ | `task.newcomer_restock` | `POST /tasks/newcomer-restock` |
161
+ | `task.peer_votes.tally` | `POST /tasks/peer-votes/tally` |
162
+ | `goal.reorder` | `POST /goals/reorder` |
163
+ | `builder.roster.read` | `GET /builders/roster`, `/grades/by-builder`, `/builders/:id/active-claims` |
164
+ | `session.search` | `GET /sessions/search` |
165
+ | `memory.read.any` | `GET /memory/builders/:id/file[s]` — cross-builder memory read (Archon) |
166
+ | `audit_log.read` | `GET /audit-log` |
167
+
168
+ **Working rank — floor `metic` today:**
169
+
170
+ | Key | Guards (route / surface) |
171
+ |---|---|
172
+ | `idea.triage` | `PATCH /inbox/:id` |
173
+ | `blocker.review` | `POST /blockers/:id/{resolve,link}` |
174
+ | `task.create` / `task.edit` / `task.promote` / `task.demote` | `POST /tasks`, `PATCH /tasks/:id`, `/promote`, `/demote` ([ADR 0090](<redacted>.md)) |
175
+ | `task.dependency.manage` / `task.criterion.manage` | `/tasks/:id/dependencies` + `/criteria` CRUD (same-tier) |
176
+ | `task.vote` | `POST /tasks/:id/vote` |
177
+ | `criterion.satisfy` / `criterion.review` | `POST /done-when/:id/satisfy`, `GET /done-when/pending-review` |
178
+ | `criterion.create` | `POST /versions/:id/done-when` — floor `metic` but **qualified**: a non-Archon must name a `goal_id`, on a `building` version, in an `open` goal they own or manage ([ADR 0154](<redacted>.md)). A floor alone does not describe this permission. *(Was bundled into the `criterion.review` row above while the route was still Archon-only — a transcription slip against the sweep; see ADR 0154 §5.)* |
179
+ | `planning.run` / `priority.run` | planning- / priority-session surface |
180
+ | `goal.create` | `POST /goals` (protected scope → archon in-handler; [ADR 0086](<redacted>.md)) |
181
+ | `goal.archive` | `POST /goals/:id/{archive,reopen}` |
182
+ | `goal.scope.widen` | `POST /goals/:id/scope` (same-tier; cross-tier = `goal.scope.manage`, system) |
183
+ | `module.enable` / `module.submit` | `POST /modules/:key/{enable,submit}`, `GET /modules/submissions` |
184
+ | `security.report.file` | `POST /security/reports`, `GET /security/adr` |
185
+ | `llm_cache.use` | `POST /llm-cache/{lookup,store}` |
186
+ | `backup.manage` | `GET /backup/status`, `POST /backup/trigger` |
187
+ | `source.clone.local` | Metic+ local clone / collaborator invite ([ADR 0035](<redacted>.md)) |
188
+ | `box.source.full` | `GET /box/source-access` → `full` (vs `starter`); `boxScopeForRank` |
189
+
190
+ **Ownership-scoped (`own` ≠ `any`) + baseline — floor `thetes` / `xenos` / any builder / `own`:**
191
+
192
+ | Key | Guards (route / surface) | Floor |
193
+ |---|---|---|
194
+ | `goal.manage.own` | `PATCH /goals/:id/members/:b`, `POST /goals/:id/transfer` — owner/manager, in-handler ([ADR 0106](<redacted>.md)) | owner/manager |
195
+ | `task.act.own` | act on **own** active claim (release, cost, notes) | own |
196
+ | `box.manage.own` | own dev-box lifecycle | own |
197
+ | `memory.manage.own` | own builder memory | own |
198
+ | `credential.manage.own` | own API key / credential | own |
199
+ | `me.wandering.set` | `PATCH /me/wandering` (clamped by `maxLevelForRank`) | own (rank-clamped) |
200
+ | `task.claim.any` | claim the general queue (per-task floor still applies) | thetes |
201
+ | `task.claim.newcomer` | claim a `newcomer_friendly` task (`xenosClaimAllowed`; xenos = one active claim) | xenos |
202
+ | `task.ship` · `cost.log` · `learning.create` · `blocker.file` · `idea.file` | own-work write surfaces | any builder |
203
+ | `page.view.public` | public read surfaces | any (incl. xenos) |
204
+ | `page.view.builder` | authenticated hall pages (`requireNonXenosPage` / `requireBuilderPage`) | thetes+ |
205
+ | `page.view.governance` | the Archon governance page (`requireArchonPage`; write = `governance.manage`) | archon |
206
+
207
+ > **Sweep-completeness note.** These keys cover every authority **domain** the 2026-07-24 sweep found ([the sweep doc](../governance/<redacted>.md) holds the exhaustive 1:1 route list — 81 guarded routes). R94 (the code transcription) is where each remaining `requireRank` site is mapped 1:1 to a key and any residual gap is closed; the route-rank-check fitness gate (R109) is the mechanical backstop that no write route ends up keyless. Keys are stable strings; adding a permission is a catalog + migration change, never a UI action. **A permission's `system` flag, not its floor, decides grantability** — floors are seed data and move; the flag is the wall.
208
+
209
+ ---
210
+
211
+ ## Alternatives considered
212
+
213
+ - **Keep ranks; add more of them.** Rejected. Every new rank threads through the 13 hand-copied tier/ladder maps (the exact drift that caused the Builder 25 incident) and still can't express "triage but not author," principals, session-scopes, or ownership. Ranks-as-roles subsumes ranks without losing them.
214
+ - **Permissions replace ranks entirely (drop the rank column).** Rejected for the cut-over. `builders.rank` anchors auto-graduation, the promotion ladder, and the per-task `requires_rank` floor; ripping it out forfeits the zero-day-one-change guarantee. Ranks become *seeded roles* and the column stays as identity/graduation — a strangler, not a rewrite.
215
+ - **Make every permission grantable (no `system` flag).** Rejected — it is the whole risk. A configurable role that could be granted `deploy` or `path.protected.modify` would let an Archon (or a compromised Archon token) hand the crown jewels to a `xenos`. The `system` flag + "substrate enforces" is what makes the tab safe; the abuse-matrix test (R112) exists to prove it.
216
+ - **Enforce the protected-surface floor in the app (move the substrate in-process).** Rejected — it re-opens exactly the gap [ADR 0043](<redacted>.md) closed. Git/SSH sit *outside* the HTTP boundary; the authoritative audit must read real git history + real ranks/registry independent of the request path. Governance owns the *data* (policy); the out-of-band substrate stays the *enforcer*.
217
+ - **Cache the resolved permission set per token/TTL.** Rejected, same reasoning as ADR 0016 rejected caching rank: a cache is a stale-authority window on a demotion. The resolver is per-request and uncached; the cost is one more query on a join we already do, bought for instant revocation.
218
+ - **Store roles/permissions as free-form UI data.** Rejected. Permissions map to real routes, so the catalog is **code** — a permission no route consumes is meaningless. The UI assigns and toggles; it never invents a key.
219
+
220
+ ---
221
+
222
+ ## Consequences
223
+
224
+ - **R91–R114 build against one settled model.** R93/R94 (tables + catalog code) transcribe this ADR's catalog and seed; R95–R97 build the resolver + gate; R98–R101 add the three non-role axes + the registry; R96/R102 do the core-host config split; R104–R109 migrate the gates and collapse the tier maps; R110/R111/R113 build the tab; R103/R112/R114 prove parity, the abuse matrix, and the live demo.
225
+ - **ADR 0016 is preserved, not amended.** Authority still lives server-side, resolved fresh per request against the DB, with instant demotion. The unit of authority narrows from *rank* to *permission*; the enforcement property is unchanged. `requireRank` and `requirePermission` coexist during the migration via the seed.
226
+ - **The trust boundary strengthens.** The 13 duplicated tier/ladder maps — a proven incident source — collapse to one resolver behind a fitness guard; the protected-surface list becomes auditable data with case/Unicode-normalized matching and a mechanical abuse-matrix proof; principals and session-scopes stop being ad-hoc special-cases and become modeled, testable types.
227
+ - **Configurability is bounded by design.** An Archon can compose any `system: false` permission into a custom role, but the `system` set is non-grantable and substrate-enforced, so no configuration can hand out deploy / protected-path / rank-setting power. This is the C6 safety guarantee, proven not asserted.
228
+ - **Zero day-one behavior change.** The compat seed reproduces every current builder's exact powers; the one divergence (unwired divine ranks fail closed instead of latently inheriting Archon) is a fail-closed improvement no live builder touches.
229
+ - **Portable, and the moat.** Governance ships as a `default: true` core module with a neutral config every instance inherits; it is precisely the "governance / audit / org-memory" substrate [ADR 0087](<redacted>.md) named as the hard-to-copy asset. No new recurring cost — tables + code + a config file.
@@ -0,0 +1,92 @@
1
+ # 0152 — The landing gate is served by the core app, not by the front proxy
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-07-27
5
+ - **Deciders:** Lars (owner / Archon), Claude
6
+ - **Track:** `internal` (platform serving / instance infra)
7
+ - **Task:** <redacted> ([#1002411](https://example.com/builders#/task/1002411)) — the cutover step of the landing-migration chain L01→L07 under the **Project creation flow** goal ([#1000035](https://example.com/builders#/goal/1000035)).
8
+ - **Builds on:** [ADR 0062](<redacted>.md) (core ↔ host split), [ADR 0108](<redacted>.md) (instance composition / pinned vendored core), [ADR 0111](<redacted>.md) (per-instance Caddy snippets), [ADR 0003](<redacted>.md) (origin certs).
9
+ - **Supersedes in practice:** the "give the landing its own Caddy config + its own deploy script" plan that L05 was scoped around. See §Decision.
10
+
11
+ ---
12
+
13
+ ## Context
14
+
15
+ The reference instance's landing page — the interactive orb "gate" — was historically **served off disk by the front proxy**. Caddy's apex block set `root * <checkout>/brand/cloud-bongos` and `file_server`'d `/`, `/gate/*` and `/world/*` straight out of a checkout of a *different* product repo (`example-owner/example`), while proxying every functional path to the platform app. The landing therefore had:
16
+
17
+ - **two sources of truth** — the gate bundle lived in the other repo, and a copy of the serving rules lived in that repo's `infra/Caddyfile` and `scripts/deploy/deploy-cloudbongos.sh`;
18
+ - **no deploy path of its own** — updating the landing meant pushing to the other repo and hand-running `git reset --hard origin/main` on the box;
19
+ - **a cross-product coupling** the core could not express: a Cloud Bongos instance's front page was owned by an unrelated product's repository.
20
+
21
+ L01 ([#1002407](https://example.com/builders#/task/1002407)) ported the verified gate bundle into the core as `public-landing/`, and L02 ([#1002408](https://example.com/builders#/task/1002408)) taught `src/platform-server.js` to serve it: `GET /` on the apex host 302-redirects to `/gate/`, `/gate` 301s to `/gate/`, `/gate/` serves the gate index raw, and the existing `express.static(public-landing)` already covers `/gate/*`, `/world/*` and `/assets/*`.
22
+
23
+ Once L01+L02 shipped, **the app could already serve the entire landing on its own** — but the live box was still file-serving the other repo's copy, because nothing had flipped the proxy. L05 was scoped (before L01/L02 existed) as "give cloud-bongos (a) its own Caddy config and (b) its own deploy script, then cut the box over." By the time L05 came up, both halves of that plan had dissolved.
24
+
25
+ ## Decision
26
+
27
+ **The core app owns the landing end to end. The front proxy does TLS, the staging lock, and `reverse_proxy` — nothing else.**
28
+
29
+ Concretely, the apex and vanity-door site blocks drop `root` + `file_server` and proxy the landing paths to the app, exactly like every other path:
30
+
31
+ ```caddy
32
+ example.com, www.example.com {
33
+ tls /etc/caddy/origin.crt /etc/caddy/origin.key
34
+ encode gzip zstd
35
+ import security_headers
36
+
37
+ # The landing surface. Identical to the catch-all except for the staging
38
+ # lock — so lifting the lock is a one-block delete, not a re-plumb.
39
+ @gate path / /gate /gate/* /world/*
40
+ handle @gate {
41
+ basic_auth {
42
+ # optional pre-launch lock; omit once the site is public
43
+ }
44
+ # Caddy runs `redir` BEFORE `basic_auth`, so "/" redirects first and the
45
+ # visitor meets the lock at /gate/. The app issues the same 302, so this
46
+ # line stays correct after the lock is removed.
47
+ redir / /gate/ 302
48
+ reverse_proxy 127.0.0.1:3000
49
+ }
50
+
51
+ handle {
52
+ reverse_proxy 127.0.0.1:3000
53
+ }
54
+ }
55
+ ```
56
+
57
+ Three consequences follow, and they are the actual decision:
58
+
59
+ **1. There is no separate Caddy config to "give" the landing.** The block above is the *generic* instance block — the same pure-proxy shape `provision.js`'s `caddyBlock()` already writes for every provisioned instance ([ADR 0111](<redacted>.md) §4). The landing stopped being a special case in the proxy the moment the app could serve it. Host-specific values (hostnames, cert paths, whether a lock exists) stay on the host, per [ADR 0062](<redacted>.md) §7 — the core ships the *shape*, not an instance's hostnames.
60
+
61
+ **2. There is no separate deploy script to write.** The landing ships **inside the pinned core package**, so it rides the instance's existing deploy: bump the core pin in the instance repo → the box's pull-deploy picks it up → the new landing is live. One artifact, one version number, one deploy path. A landing change is now a normal core change (edit `public-landing/`, ship, release), not an out-of-band push to a foreign repo.
62
+
63
+ **3. The proxy keeps exactly three jobs on the landing surface:** terminate TLS, enforce any pre-launch `basic_auth` lock, and hide anything the bundle ships but should not expose (the gate's own `README.md` — an `@hidden` matcher for `*.md`/`*.json`/`*.py` and the brand-pipeline `proof`/`refs`/`references` paths). These are deployment-environment concerns, correctly the proxy's; everything else is the app's.
64
+
65
+ ## Consequences
66
+
67
+ **Good**
68
+
69
+ - **One source of truth.** The landing bundle exists once, in the core, versioned with everything else. The foreign-repo checkout becomes dead weight (retired by L06, [#1002412](https://example.com/builders#/task/1002412)).
70
+ - **Portable.** Any instance that ships a `public-landing/gate/` gets the gate with a stock proxy block. L02 already guards this: the apex `/` → `/gate/` redirect only engages when the instance actually ships a gate, so a gate-less instance keeps its plain placeholder.
71
+ - **Atomic + rollback-able.** The landing and the app that serves it move together on one pin. Rolling the core back rolls the landing back with it.
72
+ - **Smaller proxy config.** No `root`, no `file_server`, no filesystem path in the site block — so a moved or re-cloned checkout can no longer silently change what the front page serves.
73
+
74
+ **Costs / risks**
75
+
76
+ - **The landing is now in the app's request path.** A dead app means a dead front page, where previously Caddy could still serve static files. Accepted: the front page's whole purpose is to reach the app (sign-in, project creation, the live featured-projects feed that fills the orbs), so a dead app already meant a useless landing.
77
+ - **A landing tweak needs a core release.** Slower than pushing to a static checkout. That is the point — the landing is now under the same review and versioning as the rest of the platform.
78
+ - **The `@hidden` guard is load-bearing and hand-maintained.** It is what keeps `gate/README.md` unreachable. If the landing bundle ever gains a file that should not be public, the matcher must keep up. Better long-term fix: don't ship non-public files in `public-landing/` at all.
79
+
80
+ ## Alternatives considered
81
+
82
+ - **Keep file-serving, but from the core's `public-landing/` instead of the foreign checkout.** Would have removed the cross-repo coupling with a one-line `root` change. Rejected: it hardcodes a `node_modules/@bongos/core/...` path into the proxy config, which then breaks on any packaging-layout change, and it keeps the landing a special case in a config the core cannot ship.
83
+ - **Write the landing its own Caddy config + deploy script, as originally scoped.** Rejected: it builds a second deploy path to maintain, for an artifact that already ships inside the core package and already has one.
84
+ - **Serve the landing from a CDN / object store.** Rejected: the gate is not fully static — it calls the instance's own API for the featured-projects feed and the sign-in state — and it adds a service and a cost for a page the app can serve for free.
85
+
86
+ ## Verification
87
+
88
+ The cutover was verified against the live reference instance:
89
+
90
+ - Every landing path returns an identical status code before and after the flip, across the apex and both vanity doors — including the staging lock (`401` on `/gate/*`), the `@hidden` guard (`404` on `/gate/README.md`), and the proxied functional paths (`/healthz`, `/docs`, `/create`, the featured-projects feed).
91
+ - The app's redirects are **relative** (`Location: /gate/`), so proxying them leaks no internal origin.
92
+ - **Severance test:** with the foreign repo's landing folder moved aside entirely, the site continued to serve the byte-identical gate (same md5, same 34,366 bytes) and every asset, proving the dependency is genuinely cut rather than merely unused.
@@ -0,0 +1,67 @@
1
+ # 0152 — Metic gains task abandonment, paired with an un-abandon at the same rank
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-07-27
5
+ - **Deciders:** Lars (Archon), Claude
6
+
7
+ **Builds on / amends:** [ADR 0090](<redacted>.md) (Metic task authoring — this ADR reverses one line of its "what stays Archon-only" list), [ADR 0016](<redacted>.md) (server-enforced rank boundary — unchanged), [ADR 0151](<redacted>.md) (permissions-as-atom — the `task.abandon` floor moves with the gate).
8
+
9
+ **Track:** `internal` (GDS / methodology — a Cloud Bongos platform capability). Task [1002476](https://cloudbongos.com/builders#/task/1002476).
10
+
11
+ ---
12
+
13
+ ## Context
14
+
15
+ [ADR 0090](<redacted>.md) moved task **authoring** down to the trusted working rank: a Metic can create a task, edit it, promote it backlog→ready, and wire its dependencies and criterion links. Its "what stays Archon-only" list kept one lifecycle transition behind: `POST /tasks/:id/abandon`, described there as "terminal scope removal — the inverse of promote; a destructive lifecycle kill."
16
+
17
+ A year of using that split showed the asymmetry costs more than it protects. A Metic runs `/idea-triage`, decides which ideas become work, seeds the tasks, and shapes them — and then hits a wall on the one operation that cleans up after all of it. Duplicates they created, chores that a parallel session already shipped, and tasks their own re-scoping made moot all had to be handed back to the owner to retire. The Archon was not adjudicating anything in those hand-backs; they were doing janitorial work on a queue they had already delegated.
18
+
19
+ Lars's directive (2026-07-27): **"Metics should also be able to abandon tasks."**
20
+
21
+ ### The real risk was not the grant — it was that abandon had no undo
22
+
23
+ Abandoning is a one-way door *through the API*: `/promote` accepted only `backlog`/`blocked`, `PATCH /tasks/:id` refuses `status` outright ([#514](https://cloudbongos.com/builders#/task/514)), and nothing else writes `status='abandoned'` back to a live value. Restoring a wrongly-retired task meant hand-SQL on prod — an Archon-with-ssh operation.
24
+
25
+ That was tolerable while abandon *was* Archon-only (the person who could break it could fix it). Handing the kill switch to Metic without an undo would not have removed the owner-cleanup burden; it would have swapped "please retire this" requests for "please un-retire this" requests, with a worse failure mode — a wrongly-abandoned task is invisible in every claimable list and every version rollup ([migration 017](../../migrations/<redacted>.sql) deliberately excludes abandoned from `version_progress`), so nobody notices the mistake until the work silently doesn't happen.
26
+
27
+ ## Decision
28
+
29
+ **1. Re-rank the gate.** `POST /tasks/:id/abandon` moves from `requireRank('archon')` to `requireRank('metic', 'archon')` — the same threshold ADR 0090 gave create/edit/promote. A rank trusted to seed and shape work is trusted to retire it.
30
+
31
+ **2. Add the undo at the same rank.** `POST /tasks/:id/promote` now also accepts `abandoned`, restoring the task to **`backlog`**:
32
+
33
+ | Route | Was | Now |
34
+ |---|---|---|
35
+ | `POST /tasks/:id/abandon` | archon | **metic+** |
36
+ | `POST /tasks/:id/promote` | backlog\|blocked → ready | **+ abandoned → backlog** (metic+, unchanged rank) |
37
+
38
+ Restore lands in `backlog`, never straight to `ready`. The task was retired deliberately; bringing it back should re-enter it as un-workable and require a second, explicit promote to become claimable. The restore branch returns before the reward gate ([ADR 0096](<redacted>.md)) — that is a promote-to-*workable* concern, and `backlog` is the pre-workable state, the same reasoning `/demote` already uses.
39
+
40
+ The backing `db.restoreTask` is the mirror of `db.abandonTask`: `SELECT … FOR UPDATE`, guard, single `UPDATE`, commit, with `TASK_NOT_FOUND` / `TASK_NOT_ABANDONED` as its refusals. It clears the `value_summary = 'Abandoned: <reason>'` stamp abandon writes — and *only* that stamp (`CASE WHEN value_summary LIKE 'Abandoned: %'`) — so a restored task never carries a retire note into a later ship card or `#ship-news` broadcast, while an unrelated summary survives.
41
+
42
+ ### What is unchanged
43
+
44
+ - **The abandon guards.** A `shipped` task is still refused (immutable history — retiring it would corrupt the ledger and the version rollups); a task with an **active claim** is still refused (release it first, so an abandon can never race a ship in flight); an already-abandoned task is still an idempotent no-op.
45
+ - **The audit trail.** Both the abandon and the restore are `POST`s through the writing middleware, so `audit_log` records who retired what, with the `reason`, and who brought it back.
46
+ - **Everything else on ADR 0090's Archon-only list** — setting rank, closing versions, grader-bypass confirm, cross-tier `POST|DELETE /dependencies` and goal-scope edges, `done-when` unsatisfy, newcomer-restock, peer-vote tally, override adjudication. This ADR moves exactly one row.
47
+
48
+ ### Downgrade protection + the permission catalog
49
+
50
+ - `EXPECTED_RANKS`'s lifecycle pin (`src/bongos/route-rank-check.js`, `MODULE_EXPECTED_RANKS.lifecycle`) moves to `metic+archon` in lockstep, so a *further* silent downgrade (to `any-builder`) is still a hard CI fail, and the route's `// rank:` annotation matches the middleware.
51
+ - Under [ADR 0151](<redacted>.md) the `task.abandon` permission's `floor` moves `archon` → `metic`. Because `RANK_ROLE_SEED` is *derived* from floors, that one edit re-derives the seed; the live grant arrives as `modules/governance/migrations/<redacted>.sql`, since the compat seed (002) is run-once and editing it would leave every already-migrated database on the old grant. The floor change is purely **additive** (archon already holds the permission — the seeded roles are cumulative supersets), so nothing is revoked. `tests/governance_seed.mjs` was generalized to compare the **union** of all grant migrations against the catalog, and now fails loudly if a future migration ever *revokes*, which the union model could not represent.
52
+
53
+ ## Consequences
54
+
55
+ - **The triage→author→retire loop closes for Metic.** The owner is out of the janitorial path for the queue they already delegated; the remaining owner-only lifecycle powers are ratification and scope-closing, which is where ADR 0090 wanted the line.
56
+ - **A mis-retire is now self-service to fix, at the rank that caused it** — and, notably, this also removes an Archon-with-ssh chore that existed *before* this grant.
57
+ - **Restore is deliberately not idempotent-friendly the way abandon is.** Calling `/promote` on a non-abandoned task still 409s `cannot_promote` with the current status; the restore path refuses rather than guessing. That keeps `/promote`'s contract honest — it is one route with two explicit source states, not a "make this task alive somehow" verb.
58
+ - **Larger trusted surface, same shape as ADR 0090's.** A Metic can now retire any non-shipped, unclaimed task in any open version, with no per-version/per-module fence. That is the same accepted "tighten later" debt: if Metic authority is ever folded into the goal-scope model ([ADR 0086](<redacted>.md)), abandon travels with it.
59
+ - **Trust boundary intact** ([ADR 0016](<redacted>.md)): enforcement stays server-side, per request, uncached. Editing a doc or `CLAUDE.md` grants nothing; `requireRank('metic','archon')` on the route is the authority.
60
+
61
+ ## Alternatives considered
62
+
63
+ - **Re-rank abandon with no undo.** Rejected: it relocates the owner-cleanup burden instead of removing it, and the failure mode (a silently-invisible task) is worse than the one it fixes. The undo is ~40 lines including its guards.
64
+ - **A dedicated `POST /tasks/:id/restore` route.** Rejected as surface for its own sake: `/promote` already *is* the "move this task toward workable" verb at exactly the right rank, and the restore is a third source state on it. A separate route would need its own gate, its own pin, its own catalog permission, and its own docs row — all to express the same transition.
65
+ - **Restore straight to `ready`.** Rejected: it would make a two-keystroke mistake (abandon → restore) leave the task claimable again with no human re-read of *why* it was retired. Landing in `backlog` costs one deliberate extra call and puts the scope decision back in front of someone.
66
+ - **Fence the grant (only tasks the Metic authored, or only non-`security_sensitive` ones).** Rejected as a rule builders have to learn for a threat that isn't real here: a Metic can already `PATCH` and re-scope any task, so "abandon only your own" fences nothing they couldn't achieve by editing. ADR 0090 made the same call for authoring.
67
+ - **Promote the affected builders to Archon.** Rejected for the reason ADR 0090 gave: Archon also carries rank-setting, version-close, grader-bypass, and infra — far more than "clean up the task queue."
@@ -0,0 +1,51 @@
1
+ # ADR 0153 — Sequential chain execution: one kickoff walks a dependency chain
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-07-28
5
+ - **Task:** 1002484
6
+ - **Relates to:** [ADR 0015](<redacted>.md) (dependencies + auto-promotion), [ADR 0049](<redacted>.md) (the split parallel-safety contract), [ADR 0086](<redacted>.md) §5 (polymorphic dependency edges)
7
+
8
+ ## Context
9
+
10
+ ADR 0015 made `dependencies[]` load-bearing: a task cannot be claimed until its predecessors ship, and shipping a task auto-promotes its now-unblocked dependents `backlog`→`ready` (the `promote_dependents_on_ship` trigger, migrations 020/163). That machinery works. What it does not do is **pick the newly-unblocked task up**.
11
+
12
+ The result, measured on the live instance on 2026-07-28: **146 of 303 open tasks sit behind a predecessor, and only ~79 are `ready`** at any moment. A single planning seed like goal 1000045 fronts a 24-task chain worth 1,440 credits and ~62 hours. Working it meant 24 separate human kickoffs — each one a fresh session that had to re-derive what was now claimable.
13
+
14
+ The only "sequential" affordance was `<redacted>` in `modules/hall-ui/public/work.js`: a clipboard template listing manually multi-selected tasks in topological order. Two problems. It is *text*, not execution. And the tasks a chain most needs to queue — the dependency-gated successors — are not selectable on the board at all, because they are still `backlog`.
15
+
16
+ Two builders asked for this independently: idea 1000461 (jaxri — workflows that run tasks "in parallel or sequentially") and idea 1000398 (IAMMASTERQUA — chains as the claimable unit, priced by total chain value).
17
+
18
+ ## Decision
19
+
20
+ Ship a **chain runner**: one kickoff walks a dependency chain end to end.
21
+
22
+ **1. No schema change.** The ordering already exists in the polymorphic `dependencies` table, the `task_is_fully_unblocked` predicate, and the promote-on-ship trigger. `GET /tasks/:id` already returns `dependencies[]` and `dependents[]`. Adding a `queue`, `chain_id`, or `next_task` column would create a second, drift-prone source of truth for an order the graph already encodes. The runner derives the chain on demand and stays stateless.
23
+
24
+ **2. No new claim guard.** One-active-claim-per-session is keyed on the *working tree* (`shouldRefuseSessionClaim`, `modules/lifecycle/routes/claims.js`), so ship-before-next-claim is already enforced — which is exactly the sequential shape. The chain therefore claims **one task at a time**, and a task is always fully `shipped` before the next is claimed. Batch-claiming a chain is not merely discouraged, it is impossible: successors fail `DEPS_NOT_SHIPPED` by construction, because their predecessor has not shipped yet. The ship *is* the thing that unblocks the next step.
25
+
26
+ **3. Split deterministic plumbing from judgement.** `scripts/gds/sequence.js` resolves scope, orders it, and evaluates a halt matrix — all from facts on the task row, no model judgement. `.claude/skills/builder-sequence/SKILL.md` owns the loop and defers to `/builder-claim`, `/dev`, and `/builder-ship` for their existing mechanics. A halt is a named reason with a non-zero exit code, not a narrative.
27
+
28
+ **4. Halt, do not skip** (owner decision, 2026-07-28). When the next task is something the runner should not decide alone, it **stops and reports**, leaving that task claimed. It does not ship a half-finished task, and it does not skip ahead to the next unblocked sibling.
29
+
30
+ The halt matrix: `needs_migration`, `security_sensitive`, `protected_path` (touches[] ∩ `PROTECTED_GLOBS`), `rank_floor`, `thin_spec`, `judgement_kind` (`decision`/`spike`), `not_claimable`, `deps_unmet`, `in_flight`, `cycle`. The skill adds two it must detect itself: `ship_failed` and `scope_surprise`.
31
+
32
+ ## Why halt rather than skip
33
+
34
+ Skipping looks like the higher-throughput option and is not. In a dependency graph, a skipped task is a *predecessor* — skipping it strands everything behind it, so the run continues while quietly guaranteeing that the rest of the chain cannot complete. The apparent progress is the dangerous part: a run that skipped three tasks and shipped nine reads as a success until someone asks why the chain never finished.
35
+
36
+ Halting also makes unattended running safe, which is the whole point of one kickoff. The owner can start a chain and walk away knowing the failure mode is "stopped early with a reason", not "made twelve judgement calls you have not seen yet".
37
+
38
+ ## Consequences
39
+
40
+ - A chain of N mechanical tasks now costs one kickoff instead of N.
41
+ - Chains stop silently stranding: a halt names what a human must do, so the queue does not just quietly stop advancing.
42
+ - The halt matrix is deliberately conservative. Tasks with thin specs will halt runs, which is a **feature** — it turns "the runner stopped" into pressure to write acceptance criteria at planning time.
43
+ - `not_claimable` defers to the server's own claimable feed rather than re-deriving its filter client-side. If that endpoint is unreadable the runner stays quiet rather than halting on everything — an early bug where the envelope key was misread (`tasks` vs `claimable`) turned an empty result into a universal halt, and the null-vs-empty distinction is now covered by a test.
44
+ - **Not built:** chains as a priced, claimable unit on the Tasks board (idea 1000398). The runner is the capability; surfacing chains with their total value is a separate, additive change.
45
+
46
+ ## Alternatives rejected
47
+
48
+ - **A `chain_id` / queue table.** A second source of truth for an order the dependency graph already encodes, and one that drifts the moment an edge is added.
49
+ - **Batch-claim the whole chain up front.** Impossible under the dep gate, and it would defeat the isolation that makes each ship safe.
50
+ - **Let the runner use judgement and keep shipping** (the third option the owner was offered). Faster, but it moves review after the fact — the owner would be auditing decisions that had already landed.
51
+ - **Extend `overnight-builder`.** That routine walks the claimable queue by priority across the whole backlog; this walks one bounded chain the user pointed at. Different scope, and the routine is instance-only (absent from the portable core).
@@ -0,0 +1,86 @@
1
+ # 0154 — A goal's done-when criteria are authored by that goal's owner or managers
2
+
3
+ - **Status:** Accepted
4
+ - **Date:** 2026-07-29
5
+ - **Deciders:** Lars (owner / Archon), Claude
6
+
7
+ **Builds on / amends:** [ADR 0086](<redacted>.md) (the goal tier — this ADR extends its owner/manager authority to the goal's criteria), [ADR 0090](<redacted>.md) (Metic task authoring), [ADR 0106](<redacted>.md) (owner vs manager), [ADR 0105](<redacted>.md) (why this route exists at all), [ADR 0016](<redacted>.md) (server-enforced rank boundary — unchanged), [ADR 0151](<redacted>.md) (permissions-as-atom — and see §5, which corrects one of its rows).
8
+
9
+ **Track:** `internal` (GDS / methodology). Task [1002538](https://cloudbongos.com/builders#/task/1002538), under the **Builder experience** goal [1000025](https://cloudbongos.com/builders#/goal/1000025).
10
+
11
+ ---
12
+
13
+ ## Context
14
+
15
+ The Metic planning loop is complete except in one place. A Metic may create a goal ([ADR 0086](<redacted>.md): `POST /goals` is `requireRank('metic','archon')`), author the tasks under it ([ADR 0090](<redacted>.md)), wire their dependencies and criterion links, and **confirm** a criterion when the work lands ([ADR 0086](<redacted>.md) §6 re-ranked `POST /done-when/:criterionId/satisfy` to Metic+ "so the planner — not only the Archon — closes the criterion/goal loop").
16
+
17
+ But they could not **create** the criteria those tasks are measured against: `POST /versions/:id/done-when` was `requireRank('archon')`. So a Metic could stand up a goal, fill it with work, and close its criteria out — but had to ask an Archon to write them in the first place. That is the one Archon-shaped hole in an otherwise Metic-complete loop, and it is what blocked `/goal-create` ([1002540](https://cloudbongos.com/builders#/task/1002540)) from being a skill a Metic can actually run end to end.
18
+
19
+ ### Why the obvious fix — a flat re-rank — is wrong
20
+
21
+ The tempting move is symmetry: a Metic may confirm a criterion, so let them create one. That argument does not survive contact with what a criterion *does*.
22
+
23
+ **Creating an unsatisfied criterion is a backward ratchet, and there is no undo.** Every "is this done" aggregate counts unsatisfied rows:
24
+
25
+ - `modules/lifecycle/version-close.js` gates on `criteria.satisfied === criteria.total`, counted **per version regardless of goal**. One unsatisfied row parks the version's close suggestion indefinitely — and that suggestion is served on the **unauthenticated** `GET /versions/close-suggestions`, so it is publicly visible.
26
+ - `db.achieveGoalIfComplete` refuses while `unsatisfied > 0`, so the goal never auto-achieves, and `promote_dependents_on_goal_achieve` never releases the tasks waiting on it.
27
+
28
+ That is operationally the same effect as `POST /done-when/:criterionId/unsatisfy` — which is Archon-only, and which `docs/governance/<redacted>.md` lists in the same Archon "Lifecycle disposition" line as this route. And there is **no DELETE route for a criterion**: descoping is direct SQL under a claim. A mistake is not correctable through the API by anyone.
29
+
30
+ **The create path also had no ownership check whatsoever.** Its entire authorization surface was `requireBuilder` → `requireRank` → version exists → (if `goal_id` given) the goal belongs to that version. That last one is referential integrity, not authority. Every *other* goal-touching write in the module runs `authorizeGoalManagement` (owner | manager | Archon) or `authorizeGoalTaskCreate` (membership + scope wall). A flat re-rank would therefore have let any Metic attach a permanent, un-deletable criterion to **any goal on any version**, including goals they had never joined and private goals ([ADR 0112](<redacted>.md)) — published to the anonymous status feed.
31
+
32
+ `docs/canonical-permissions.md` named "set version scope (`done-when`)" as Archon-exclusive. A flat re-rank contradicts that flatly; the decision below narrows it instead.
33
+
34
+ ## Decision
35
+
36
+ **Authority over a goal's criteria follows goal ownership, not global rank.**
37
+
38
+ `POST /versions/:id/done-when` becomes `requireRank('metic','archon')`, plus an **ordered, fail-closed in-handler wall** for any non-Archon:
39
+
40
+ | Step | Refusal |
41
+ |---|---|
42
+ | a `goal_id` must be named | `403 goal_id_required` |
43
+ | the caller must be the goal's **owner or a manager** | `400 bad_goal_id` — **masked**, see below |
44
+ | the goal must be `open` | `409 goal_not_open` |
45
+ | the version must be open (`planning` or `building`) | `409 version_closed` |
46
+
47
+ An Archon short-circuits all four: version-level (goal-less) criteria, goals in any state, and versions in any state stay theirs.
48
+
49
+ The authority predicate is `isGoalOwnerOrManager` — the **same** owner-or-manager test behind visibility changes, invitations, join-request responses, member roles and conflict resolution. It moved from `modules/lifecycle/routes/goals.js` to a module sibling, `modules/lifecycle/goal-authz.js`, so both routes share one definition of "who holds this goal"; `authorizeGoalManagement` is rebuilt on it and re-exported from `routes/goals.js`, so its existing DB-free tests are untouched. No route requires another route.
50
+
51
+ ### Authority is checked before state, and the refusal is masked
52
+
53
+ Ordering matters here in a way it does not for the sibling goal routes. Those are reached by callers who already know the goal exists (they are looking at it); this one takes an arbitrary integer `goal_id` from any Metic.
54
+
55
+ If the refusals were distinguishable — `400 bad_goal_id` (no such goal) vs `403 not_owner_or_manager` (exists, not yours) vs `409 goal_not_open` (exists, settled) — the route would be an **existence oracle**: a Metic could enumerate `goal_id`s and learn which exist on a version, including **private goals** ([ADR 0112](<redacted>.md)). That surface did not exist while the route was Archon-only, because an Archon can already read every goal. Re-ranking it creates the surface, so the re-rank has to close it.
56
+
57
+ So the ownership test runs **ahead of** both state checks, and a caller who fails it gets byte-identical `400 bad_goal_id` to one naming a goal that does not exist — same code, same message, same envelope, rendered by the same `badGoalId()` helper the genuine-absence branch uses. Only someone who already holds the goal learns anything further about it. This mirrors the O1 no-leak ordering the goal abuse matrix already pins (`not_a_member` is returned before any scope hint).
58
+
59
+ **What does not move.** `POST /done-when/:criterionId/unsatisfy` stays Archon — un-flipping a *confirmed* criterion rewrites shipped scope-truth, and keeping the backward ratchet at the top rank is the safety this grant is paired with. It was previously **unpinned** in `route-rank-check`; this ADR pins it at `archon` so a silent downgrade reds the build.
60
+
61
+ ### Why this is the right boundary
62
+
63
+ Creating a criterion inside a goal you own is not scope-*truth* authorship in the sense the Archon gate was protecting — it is describing the finish line of a workspace you are already accountable for. The blast radius is bounded by something the actor already holds. Creating a criterion at the *version* level, or inside someone else's goal, still shapes the project's scope for other people, and that stays Archon.
64
+
65
+ ## Consequences
66
+
67
+ - `/goal-create` becomes genuinely Metic-runnable: create the goal → write its criteria → seed its tasks, in one session, without an Archon in the loop.
68
+ - A Metic can still park **their own** goal by adding a criterion they never satisfy. That is a self-inflicted wound inside their own workspace, visible in `/goal-review`, not an attack on anyone else's.
69
+ - The proposal → ratify queue (`kind='criterion-proposal'` → `POST /inbox/:id/ratify`, Archon-only) remains the path for **version-level** criteria and for anyone who is not the goal's owner. It is no longer the only path for a goal owner describing their own goal. Note the direct path writes one row and awards no `proposal_ratified` karma and no `criterion_proposal_audit` provenance — proposals remain the richer, attributable route, and the one an Archon adjudicates.
70
+ - Five generated artifacts regenerate (`docs/api/openapi.json`, `docs/api-reference.md`, `docs/routes-permissions.md`, `clients/bongos-client/*`), because the rank flows into the published contract via the `// rank:` annotation.
71
+
72
+ ## §5 — A correction to ADR 0151's catalog
73
+
74
+ [ADR 0151](<redacted>.md)'s "floor `metic` today" table bundles three routes into one row:
75
+
76
+ > `criterion.satisfy` / `criterion.review` | `POST /done-when/:id/satisfy`, `GET /done-when/pending-review`, `POST /versions/:id/done-when`
77
+
78
+ Two of those were Metic; the third (`POST /versions/:id/done-when`) was **Archon** at the time that table was written. 0151's own cited source — the exhaustive `docs/governance/<redacted>.md` — lists it correctly under Archon. The bundled row was a transcription slip, and it was latent-dangerous: 0151's R94 compat seed grants "exactly today's powers," so seeding from that row would have handed the `metic` role an Archon route, and R103's parity test would either have gone red or, worse, ratified the escalation.
79
+
80
+ This ADR makes the row's *outcome* correct but for a different reason, so the row is split anyway: `criterion.create` is its own atom, floor `metic`, **qualified by the goal-ownership wall above** — a floor alone does not describe this permission.
81
+
82
+ ## Alternatives considered
83
+
84
+ - **Leave it Archon; use the proposal queue.** Rejected: it keeps the Archon in the loop for a Metic describing a goal they own, which is the janitorial-delegation problem [ADR 0152](<redacted>.md) already named. The queue survives for the cases that genuinely need adjudication.
85
+ - **Flat re-rank to Metic+.** Rejected — §Context.
86
+ - **Add a DELETE for criteria so the ratchet has an undo** (the [ADR 0152](<redacted>.md) "pair the grant with its inverse" pattern). Deferred, not rejected: it is the honest completion of "manage," and it would neutralise the no-undo objection directly. It is a new capability nobody has today — including Archons — and deleting a criterion with linked `task_criteria` rows needs its own cascade decision. Worth its own task.