@bongos/core 1.19.714 → 1.19.724

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 (422) hide show
  1. package/.bongos-core.json +488 -413
  2. package/.claude/skills/backlog-review/SKILL.md +3 -3
  3. package/.claude/skills/blocker-review/SKILL.md +1 -1
  4. package/.claude/skills/blocker-solve/SKILL.md +2 -2
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-ship/SKILL.md +1 -1
  8. package/.claude/skills/builder-start/SKILL.md +1 -1
  9. package/.claude/skills/design/SKILL.md +2 -2
  10. package/.claude/skills/feedback/SKILL.md +1 -1
  11. package/.claude/skills/goal-create/SKILL.md +1 -1
  12. package/.claude/skills/goal-review/SKILL.md +2 -2
  13. package/.claude/skills/idea-triage/SKILL.md +5 -5
  14. package/.claude/skills/merge-mode/SKILL.md +4 -4
  15. package/.claude/skills/new-project/SKILL.md +17 -17
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/recall/SKILL.md +2 -2
  18. package/.claude/skills/strand-fix/SKILL.md +1 -1
  19. package/.devcontainer/README.md +2 -2
  20. package/CONTRIBUTING.md +2 -2
  21. package/GOVERNANCE.md +4 -4
  22. package/clients/bongos-client/README.md +1 -1
  23. package/clients/bongos-client/bongos-client.global.js +4 -0
  24. package/clients/bongos-client/index.cjs +4 -0
  25. package/clients/bongos-client/index.d.ts +5 -0
  26. package/clients/bongos-client/index.mjs +4 -0
  27. package/config/branding.neutral.json +4 -2
  28. package/docs/adr/0001-stack-choice-single-vps.md +2 -2
  29. package/docs/adr/0002-digitalocean-over-hetzner.md +1 -1
  30. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  31. package/docs/adr/0009-autotile-architecture.md +1 -1
  32. package/docs/adr/0011-terrain-id-rendering-model.md +3 -3
  33. package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +1 -1
  34. package/docs/adr/0014-families-canonical-over-vocabulary.md +1 -1
  35. package/docs/adr/0015-task-dependencies-and-auto-promotion.md +1 -1
  36. package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +1 -1
  37. package/docs/adr/0018-three-rank-model-goes-live.md +3 -3
  38. package/docs/adr/0020-security-model-public-stub.md +9 -9
  39. package/docs/adr/0021-per-builder-skill-model-preferences.md +2 -2
  40. package/docs/adr/0022-secrets-policy.md +5 -5
  41. package/docs/adr/0024-cloneable-repo-local-first-memory.md +3 -3
  42. package/docs/adr/0024-multi-agent-system-architecture.md +10 -10
  43. package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +1 -1
  44. package/docs/adr/0025-structured-criterion-task-link.md +1 -1
  45. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +13 -13
  46. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +14 -14
  47. package/docs/adr/0029-status-page-outage-resilience.md +1 -1
  48. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  49. package/docs/adr/0031-cloud-dev-environments-for-builders.md +7 -7
  50. package/docs/adr/0032-discord-mirror-of-gds.md +3 -3
  51. package/docs/adr/0033-discord-bot-service-principal.md +4 -4
  52. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  53. package/docs/adr/0035-builder-onboarding-three-paths.md +4 -4
  54. package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +1 -1
  55. package/docs/adr/0037-discord-channel-management.md +1 -1
  56. package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +5 -5
  57. package/docs/adr/0039-setup-first-onboarding-ux.md +6 -6
  58. package/docs/adr/0040-remote-control-default-browser-onramp.md +3 -3
  59. package/docs/adr/0041-temporary-grader-bypass-killswitch.md +2 -2
  60. package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +5 -5
  61. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +2 -2
  62. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  63. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  64. package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
  65. package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +1 -1
  66. package/docs/adr/0049-split-parallel-safety-contract.md +1 -1
  67. package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +2 -2
  68. package/docs/adr/0051-full-session-transcript-corpus.md +2 -2
  69. package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
  70. package/docs/adr/0053-scoped-dev-box-session.md +1 -1
  71. package/docs/adr/0054-session-token-reward-cost-plus.md +7 -7
  72. package/docs/adr/0055-server-mediated-branch-publish.md +2 -2
  73. package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +3 -3
  74. package/docs/adr/0057-container-cost-ledger.md +1 -1
  75. package/docs/adr/0057-discord-archon-approval-channels.md +2 -2
  76. package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +6 -6
  77. package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +11 -11
  78. package/docs/adr/0060-gds-retrieval-layer.md +3 -3
  79. package/docs/adr/0061-context-layer-decomposition.md +2 -2
  80. package/docs/adr/0062-medusa-instance-model-decoupling.md +5 -5
  81. package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +2 -2
  82. package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +3 -3
  83. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +5 -5
  84. package/docs/adr/0066-generated-file-map-sections.md +2 -2
  85. package/docs/adr/0067-session-start-fast-forward-pull.md +1 -1
  86. package/docs/adr/0068-shared-art-key-for-newcomers.md +1 -1
  87. package/docs/adr/0069-hall-live-channel-sse-decouple.md +3 -3
  88. package/docs/adr/0070-deterministic-session-card-delivery.md +1 -1
  89. package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +2 -2
  90. package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +2 -2
  91. package/docs/adr/0072-dev-box-code-staleness-visibility.md +2 -2
  92. package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +3 -3
  93. package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +2 -2
  94. package/docs/adr/0076-in-session-deterministic-compaction.md +2 -2
  95. package/docs/adr/0077-content-addressed-llm-cache.md +2 -2
  96. package/docs/adr/0078-deterministic-first-cron.md +3 -3
  97. package/docs/adr/0079-value-gated-autonomous-engagement.md +2 -2
  98. package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +3 -3
  99. package/docs/adr/0081-tool-agnostic-design-layer.md +3 -3
  100. package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +2 -2
  101. package/docs/adr/0083-modular-architecture-strangler-build-plan.md +3 -3
  102. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +2 -2
  103. package/docs/adr/0086-goal-scoped-work-hierarchy.md +15 -15
  104. package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +5 -5
  105. package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +1 -1
  106. package/docs/adr/0088-consistent-character-generative-animation.md +5 -5
  107. package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -15
  108. package/docs/adr/0090-metic-task-authoring.md +8 -8
  109. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +3 -3
  110. package/docs/adr/0092-hosting-module-and-on-prem-target.md +11 -11
  111. package/docs/adr/0093-tranche-2-core-carve-sequence.md +5 -5
  112. package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +1 -1
  113. package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +6 -6
  114. package/docs/adr/0095-cross-agent-context-management.md +5 -5
  115. package/docs/adr/0096-require-reward-before-workable.md +5 -5
  116. package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -2
  117. package/docs/adr/0097-retroactive-reward-backfill.md +4 -4
  118. package/docs/adr/0098-public-mirror-publish-manifest.md +3 -3
  119. package/docs/adr/0099-delayed-redacted-mirror-export.md +1 -1
  120. package/docs/adr/0100-two-repo-instance-model.md +6 -6
  121. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +9 -9
  122. package/docs/adr/0102-finish-bounding-the-kernel.md +1 -1
  123. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +6 -6
  124. package/docs/adr/0103-gdsv4-bongos-consolidation.md +3 -3
  125. package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +3 -3
  126. package/docs/adr/0105-instance-seed-migrations-out-of-core.md +2 -2
  127. package/docs/adr/0106-goal-owner-manager-authority.md +2 -2
  128. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +6 -6
  129. package/docs/adr/0108-instance-composition-configurable-root.md +6 -6
  130. package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +3 -3
  131. package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +1 -1
  132. package/docs/adr/0111-instance-hosting-provisioning-module.md +18 -18
  133. package/docs/adr/0112-private-goals-invitations-join-requests.md +7 -7
  134. package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +1 -1
  135. package/docs/adr/0114-instance-docs-discovery-contract.md +4 -4
  136. package/docs/adr/0115-scheduled-routines-portability.md +1 -1
  137. package/docs/adr/0117-api-versioning-and-deprecation-policy.md +1 -1
  138. package/docs/adr/0118-api-contract-source-of-truth-code-first.md +1 -1
  139. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +2 -2
  140. package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +1 -1
  141. package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +3 -3
  142. package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
  143. package/docs/adr/0124-typed-api-response-bodies.md +2 -2
  144. package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +2 -2
  145. package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +3 -3
  146. package/docs/adr/0127-cross-box-fleet-observability.md +1 -1
  147. package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +3 -3
  148. package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +4 -4
  149. package/docs/adr/0130-provision-remote-exec-over-ssh.md +4 -4
  150. package/docs/adr/0131-rank-scoped-skill-visibility.md +2 -2
  151. package/docs/adr/0132-cohost-fleet-control-plane-migration.md +2 -2
  152. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +1 -1
  153. package/docs/adr/0134-private-first-npm-distribution.md +3 -3
  154. package/docs/adr/0135-module-upstream-submission-interim-queue.md +1 -1
  155. package/docs/adr/0136-update-channel-subscription-policy.md +2 -2
  156. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +6 -6
  157. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +4 -4
  158. package/docs/adr/0142-project-context-is-prose-only.md +2 -2
  159. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +2 -2
  160. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +2 -2
  161. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +2 -2
  162. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +4 -4
  163. package/docs/adr/0147-standalone-live-docs-regen.md +1 -1
  164. package/docs/adr/0149-bongos-dev-local-launcher.md +1 -1
  165. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +3 -3
  166. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +20 -20
  167. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +2 -2
  168. package/docs/adr/0152-metic-task-abandonment.md +7 -7
  169. package/docs/adr/0153-sequential-chain-runner.md +1 -1
  170. package/docs/adr/0154-goal-owner-criterion-authoring.md +7 -7
  171. package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +2 -2
  172. package/docs/adr/0156-user-set-display-name-wins.md +1 -1
  173. package/docs/adr/0157-archon-is-rank-and-identity-only.md +10 -10
  174. package/docs/adr/0158-choke-point-verdict-taxonomy.md +1 -1
  175. package/docs/adr/0159-merge-gate-reconciliation.md +1 -1
  176. package/docs/adr/0160-main-audit-repo-verifiable-caller.md +1 -1
  177. package/docs/adr/0161-publish-on-merge.md +2 -2
  178. package/docs/adr/0163-legacy-display-name-resolution.md +1 -1
  179. package/docs/adr/0165-instances-hookless-by-default.md +1 -1
  180. package/docs/adr/0166-module-maintenance-and-deprecation-policy.md +3 -3
  181. package/docs/adr/0167-gate-trust-link-agpl-default.md +2 -2
  182. package/docs/adr/0167-module-catalog-source-vs-provenance.md +3 -3
  183. package/docs/adr/0169-bongos-serve-server-bring-up.md +1 -1
  184. package/docs/adr/0171-profile-rollup-join-and-consent.md +1 -1
  185. package/docs/adr/0172-editable-rank-roles-substrate-axis.md +2 -2
  186. package/docs/adr/0172-per-craft-compensation-ideator-credit-lane.md +4 -4
  187. package/docs/adr/0173-legacy-multi-ship-shortfall-not-backfilled.md +1 -1
  188. package/docs/adr/0174-government-terminology-ranks-not-roles.md +1 -1
  189. package/docs/adr/0177-goal-membership-kind.md +3 -3
  190. package/docs/adr/0178-the-copy-desk-flag-and-queue.md +1 -1
  191. package/docs/adr/0179-board-membership-predicate-grammar.md +1 -1
  192. package/docs/adr/0181-teardown-state-gate-and-address-retention.md +1 -1
  193. package/docs/adr/0182-project-visibility-two-axes.md +1 -1
  194. package/docs/adr/0183-criteria-close-themselves.md +1 -1
  195. package/docs/adr/0184-ship-requires-an-assurance.md +1 -1
  196. package/docs/adr/0185-spark-handoff-credit-split.md +1 -1
  197. package/docs/adr/0186-per-project-idea-templates.md +1 -1
  198. package/docs/adr/0187-collab-four-decisions.md +1 -1
  199. package/docs/adr/0187-oauth-handshake-cookie-lifetime.md +1 -1
  200. package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
  201. package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +2 -2
  202. package/docs/adr/0191-majority-and-the-author-who-may-not-decide-alone.md +1 -1
  203. package/docs/adr/0192-a-category-orients-and-authorises-nothing.md +2 -2
  204. package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
  205. package/docs/adr/0195-adr-numbers-are-checked-like-migration-numbers.md +2 -2
  206. package/docs/adr/0196-chrome-world-token-contract-and-the-landing-sheets.md +2 -2
  207. package/docs/adr/0197-the-ui-design-module.md +1 -1
  208. package/docs/adr/0198-third-party-skill-vendoring-policy.md +1 -1
  209. package/docs/adr/0203-doorway-changelog-out-of-module-api.md +2 -2
  210. package/docs/adr/0204-the-apex-is-dark-only.md +2 -2
  211. package/docs/adr/0205-federated-checkin-needs-the-hubs-own-signin-witness.md +1 -1
  212. package/docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md +1 -1
  213. package/docs/adr/0207-connections-implementation-departures-and-honest-limits.md +1 -1
  214. package/docs/adr/0208-application-lifecycle-the-handle-waits-and-the-vouch-has-no-minter.md +2 -2
  215. package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +1 -1
  216. package/docs/adr/0210-scouting-graduates-on-an-instance-asserted-owner-claim.md +1 -1
  217. package/docs/adr/0212-connections-on-the-profile-surface-and-addressing.md +1 -1
  218. package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +2 -2
  219. package/docs/adr/0215-the-recruiter-sliver-is-a-ceiling-not-a-step.md +6 -6
  220. package/docs/adr/0216-a-fake-pool-interpreter-must-fail-loudly-not-silently-ignore.md +2 -2
  221. package/docs/adr/0217-rank-gates-inviting-not-viewing.md +1 -1
  222. package/docs/adr/0218-the-apex-root-web-surface.md +2 -2
  223. package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +1 -1
  224. package/docs/adr/0220-an-in-house-rebuilt-skill-is-a-first-party-skill.md +1 -1
  225. package/docs/adr/0221-the-impeccable-method-is-one-skill-with-sub-command-playbooks.md +2 -2
  226. package/docs/adr/0222-the-image-family-under-the-hero-only-rule.md +2 -2
  227. package/docs/adr/0223-the-detector-is-two-tiers-in-the-kit.md +3 -3
  228. package/docs/adr/0224-module-skills-reach-the-core-checkout-as-untracked-self-ignored-copies.md +1 -1
  229. package/docs/adr/0225-privacy-suppresses-cross-project-surfaces-only.md +1 -1
  230. package/docs/adr/0226-one-stale-timer-reporting-degrades-enforcement-refuses.md +1 -1
  231. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +1 -1
  232. package/docs/adr/0230-a-look-skill-is-a-composition-grammar-the-palette-is-the-pack.md +1 -1
  233. package/docs/adr/0231-the-style-session-an-owner-authors-a-look-inside-the-contract.md +2 -2
  234. package/docs/adr/0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md +2 -2
  235. package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +2 -2
  236. package/docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md +2 -2
  237. package/docs/adr/0235-a-fix-category-is-kindbug-under-a-goal-no-column-required.md +11 -11
  238. package/docs/adr/0236-many-ideas-one-task-the-lineage-key-is-the-pair.md +5 -5
  239. package/docs/adr/0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md +5 -5
  240. package/docs/adr/0238-the-join-door-is-a-provisioning-settings-policy-key.md +1 -1
  241. package/docs/adr/0239-a-recommendation-is-a-table-that-carries-its-own-reason.md +4 -4
  242. package/docs/adr/0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md +4 -4
  243. package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +1 -1
  244. package/docs/adr/0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md +2 -2
  245. package/docs/adr/0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md +4 -4
  246. package/docs/adr/0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md +3 -3
  247. package/docs/adr/0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md +7 -7
  248. package/docs/adr/0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md +1 -1
  249. package/docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md +2 -2
  250. package/docs/adr/0248-a-hub-project-s-owner-is-the-provisioning-row-carried-on-a-port.md +3 -3
  251. package/docs/adr/0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md +3 -3
  252. package/docs/adr/0250-strict-versioning-the-version-boundary-is-the-scope-gate.md +6 -6
  253. package/docs/adr/0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md +5 -5
  254. package/docs/adr/0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md +19 -19
  255. package/docs/adr/0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md +2 -2
  256. package/docs/adr/0254-a-relayed-link-is-contained-downward-or-it-is-not-published.md +6 -6
  257. package/docs/adr/0255-a-public-lists-ordering-is-part-of-its-payload.md +2 -2
  258. package/docs/adr/0256-the-visibility-ceiling-is-the-sign-in-gate-too-and-the-reader-moves-to-the-composer.md +3 -3
  259. package/docs/adr/0257-auth-resolves-before-the-hall-mounts-anything.md +1 -1
  260. package/docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md +1 -1
  261. package/docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md +7 -7
  262. package/docs/adr/0260-the-application-is-the-consent-and-the-echo-is-the-gate.md +4 -4
  263. package/docs/adr/0261-a-preselect-always-carries-a-reason-the-bundle-summary-is-the-floor.md +1 -1
  264. package/docs/adr/0262-a-bug-never-lands-in-the-inbox.md +1 -1
  265. package/docs/adr/0263-how-a-version-closes.md +3 -3
  266. package/docs/adr/0264-the-ten-working-areas.md +3 -3
  267. package/docs/adr/0265-work-with-no-goal-and-the-order-the-cut-needs.md +5 -5
  268. package/docs/adr/0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md +2 -2
  269. package/docs/adr/0267-unanimity-and-the-revise-and-re-sit-loop.md +1 -1
  270. package/docs/adr/0268-the-constitution-comes-from-two-roots.md +3 -3
  271. package/docs/adr/0269-the-cli-session-store-is-host-keyed-at-a-fixed-anchor.md +1 -1
  272. package/docs/adr/0270-rank-floor-derives-without-a-prediction.md +2 -2
  273. package/docs/adr/0271-the-ideator-surface-is-two-halves.md +3 -3
  274. package/docs/adr/0272-a-craft-is-an-identity-not-a-toolchain.md +4 -4
  275. package/docs/adr/0273-hall-nav-can-gate-on-a-permission.md +1 -1
  276. package/docs/adr/0274-one-kernel-three-role-packs.md +1 -1
  277. package/docs/adr/0275-one-source-for-a-written-role-responsibility.md +1 -1
  278. package/docs/adr/0276-the-skill-listing-budget-cannot-hold-every-trigger.md +1 -1
  279. package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
  280. package/docs/adr/0278-a-gated-project-still-takes-applications.md +2 -2
  281. package/docs/adr/0279-an-upgrade-is-proven-by-the-served-version-not-the-health-check.md +1 -1
  282. package/docs/adr/0280-the-unattended-lane-may-drive-a-co-tenant-because-the-roster-is-the-decision.md +1 -1
  283. package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +2 -2
  284. package/docs/adr/README.md +294 -294
  285. package/docs/api/openapi.json +72 -5
  286. package/docs/api-reference.md +8 -2
  287. package/docs/architecture.md +22 -22
  288. package/docs/branding-contract.md +5 -5
  289. package/docs/canonical-permissions.md +22 -22
  290. package/docs/copy-inventory.md +15 -13
  291. package/docs/copy-registry.json +35 -17
  292. package/docs/design/hall-direction-v2.md +3 -3
  293. package/docs/design/landing-page-direction-v2.md +1 -1
  294. package/docs/design/projects-hub-direction-v2.md +1 -1
  295. package/docs/file-map.md +3 -1
  296. package/docs/module-api-changelog.md +36 -1
  297. package/docs/modules-contract.md +25 -25
  298. package/docs/onboarding/browser-terminal-guide.md +1 -1
  299. package/docs/onboarding/diagrams/README.md +3 -3
  300. package/docs/onboarding/drachmae-and-karma.md +13 -13
  301. package/docs/onboarding/primer.md +7 -7
  302. package/docs/packs/artist.md +1 -1
  303. package/docs/packs/ideator.md +1 -1
  304. package/docs/project-context.template.md +3 -3
  305. package/docs/recipes/builder-box-lifecycle.md +5 -5
  306. package/docs/recipes/claude-design-loop.md +1 -1
  307. package/docs/recipes/core-release-pipeline.md +55 -14
  308. package/docs/recipes/figma-design-loop.md +1 -1
  309. package/docs/recipes/gds-db-backup.md +2 -2
  310. package/docs/recipes/instance-deploy.md +2 -2
  311. package/docs/recipes/instance-service-restart.md +3 -3
  312. package/docs/recipes/local-dev.md +2 -2
  313. package/docs/recipes/managed-settings-remote-control.md +3 -3
  314. package/docs/recipes/memory-backup.md +4 -4
  315. package/docs/recipes/memory-sync-conflicts.md +2 -2
  316. package/docs/recipes/multi-builder-merge.md +2 -2
  317. package/docs/recipes/ops-gotchas.md +4 -4
  318. package/docs/recipes/overnight-builder.md +1 -1
  319. package/docs/recipes/packaging-the-core.md +6 -6
  320. package/docs/recipes/private-npm-distribution.md +5 -5
  321. package/docs/recipes/search-index-ingestion.md +3 -3
  322. package/docs/recipes/self-host.md +5 -5
  323. package/docs/recipes/server-bring-up.md +1 -1
  324. package/docs/recipes/ship-and-regrade.md +2 -2
  325. package/docs/recipes/standalone-live-docs.md +1 -1
  326. package/docs/recipes/ui-look-before-you-ship.md +1 -1
  327. package/docs/recipes/upgrading-the-core.md +2 -2
  328. package/docs/recipes/work-on-a-task-from-your-dev-box.md +6 -6
  329. package/docs/research/cursor-competitive-analysis.md +3 -3
  330. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +7 -7
  331. package/migrations/core_240_artist_gate_indexes.sql +48 -0
  332. package/migrations/core_240_goal_members_drop_legacy_role.sql +79 -0
  333. package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
  334. package/modules/hall-ui/public/watch.css +6 -0
  335. package/modules/hall-ui/public/watch.js +52 -6
  336. package/modules/lifecycle/cascade.js +37 -9
  337. package/modules/lifecycle/db-claims.js +5 -2
  338. package/modules/lifecycle/db-goals.js +4 -3
  339. package/modules/lifecycle/db-overrides.js +117 -4
  340. package/modules/lifecycle/db-tasks.js +63 -0
  341. package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +12 -4
  342. package/modules/lifecycle/module.json +2 -1
  343. package/modules/lifecycle/pg-constraint-fail.js +49 -0
  344. package/modules/lifecycle/routes/artist-gate.js +101 -0
  345. package/modules/lifecycle/routes/claims.js +13 -1
  346. package/modules/lifecycle/routes/lifecycle.js +6 -0
  347. package/modules/lifecycle/routes/task-write-routes.js +7 -0
  348. package/modules/lifecycle/routes/tasks.js +24 -0
  349. package/modules/lifecycle/task-classifier.js +10 -1
  350. package/modules/lifecycle/tests/cascade.mjs +43 -0
  351. package/modules/provisioning/provisioning.js +79 -3
  352. package/modules/provisioning/routes/provisioning.js +6 -1
  353. package/modules/provisioning/tests/provisioning.mjs +8 -8
  354. package/modules/security/secret-scrub.js +40 -4
  355. package/modules/status-ui/public/README.md +1 -1
  356. package/modules/ui-design/docs/design-contract.md +2 -2
  357. package/modules/ui-design/kit/README.md +1 -1
  358. package/modules/ui-design/kit/serve.js +1 -0
  359. package/modules/ui-design/skills/README.md +6 -6
  360. package/modules/ui-design/skills/brandkit/SKILL.md +1 -1
  361. package/modules/ui-design/skills/design-taste-frontend/SKILL.md +1 -1
  362. package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +1 -1
  363. package/modules/ui-design/skills/gpt-taste/SKILL.md +1 -1
  364. package/modules/ui-design/skills/high-end-visual-design/SKILL.md +1 -1
  365. package/modules/ui-design/skills/image-to-code/SKILL.md +1 -1
  366. package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +1 -1
  367. package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +1 -1
  368. package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
  369. package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +1 -1
  370. package/modules/ui-design/skills/minimalist-ui/SKILL.md +1 -1
  371. package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +1 -1
  372. package/modules/ui-design/skills/stitch-design-taste/SKILL.md +1 -1
  373. package/modules/ui-design/skills/style/SKILL.md +2 -2
  374. package/modules/ui-design/styles/README.md +3 -3
  375. package/modules/ui-design/styles/blueprint/DESIGN.md +1 -1
  376. package/modules/ui-design/styles/chrome-world/DESIGN.md +1 -1
  377. package/package-lock.json +2 -2
  378. package/package.json +1 -1
  379. package/scripts/gds/audit-rank-roles.js +172 -0
  380. package/scripts/gds/backfill-task-rewards.js +9 -4
  381. package/scripts/gds/build-cli-package.js +1 -0
  382. package/scripts/gds/claim.js +94 -9
  383. package/scripts/gds/fitness.js +8 -8
  384. package/scripts/gds/go-live.js +139 -5
  385. package/scripts/gds/land-watch.js +13 -5
  386. package/scripts/gds/mirror-redact.js +13 -2
  387. package/scripts/gds/package-core.js +11 -3
  388. package/scripts/gds/release.js +55 -0
  389. package/scripts/gds/ship-land.js +10 -2
  390. package/scripts/gds/ship-regen.js +41 -8
  391. package/scripts/gds/start.js +7 -4
  392. package/scripts/gds/worktree-claim-guard.js +27 -1
  393. package/scripts/public-mirror-export.js +17 -4
  394. package/src/bongos/api-errors.js +9 -0
  395. package/src/bongos/artist-gate.js +204 -0
  396. package/src/branding.js +15 -0
  397. package/src/module-api.js +34 -1
  398. package/tests/api_path_404.mjs +28 -0
  399. package/tests/artist_gate.mjs +234 -0
  400. package/tests/artist_gate_release.mjs +283 -0
  401. package/tests/artist_gate_route.mjs +174 -0
  402. package/tests/box_scope_predicate.mjs +117 -0
  403. package/tests/claim_error_surface.mjs +163 -1
  404. package/tests/cli_exit_no_abort.mjs +256 -3
  405. package/tests/credit_log_single_writer.mjs +94 -0
  406. package/tests/currency_label.mjs +11 -4
  407. package/tests/go_live.mjs +144 -0
  408. package/tests/helpers.mjs +8 -0
  409. package/tests/main_worktree_busy_reason.mjs +167 -0
  410. package/tests/membership_kind_lockstep.mjs +88 -0
  411. package/tests/mirror_redact.mjs +14 -0
  412. package/tests/module_api.mjs +6 -0
  413. package/tests/provision_settings_apply.mjs +13 -5
  414. package/tests/provisioning_settings.mjs +11 -7
  415. package/tests/provisioning_settings_apply.mjs +4 -4
  416. package/tests/provisioning_settings_env.mjs +59 -7
  417. package/tests/public_mirror_export.mjs +12 -0
  418. package/tests/rank_roles_audit.mjs +114 -0
  419. package/tests/scrubber_corpus.mjs +70 -2
  420. package/tests/start_rebase_warning.mjs +50 -0
  421. package/tests/task_field_bounds_match_db.mjs +140 -0
  422. package/tests/watch_roster_self_row.mjs +289 -0
@@ -63,313 +63,313 @@ This keeps the decision history honest and traceable.
63
63
 
64
64
  | # | Title | Topic |
65
65
  |---|---|---|
66
- | 0001 | [Stack choice — single VPS](<redacted>.md) | infra (game-stack choice) |
67
- | 0002 | [DigitalOcean over Hetzner](<redacted>.md) | infra (game hosting) |
68
- | 0003 | [Cloudflare Origin Cert over Let's Encrypt](<redacted>.md) | infra / TLS (game domain) |
66
+ | 0001 | [Stack choice — single VPS](0001-stack-choice-single-vps.md) | infra (game-stack choice) |
67
+ | 0002 | [DigitalOcean over Hetzner](0002-digitalocean-over-hetzner.md) | infra (game hosting) |
68
+ | 0003 | [Cloudflare Origin Cert over Let's Encrypt](0003-cloudflare-origin-cert-over-letsencrypt.md) | infra / TLS (game domain) |
69
69
  | 0004 | [Example name + trademark acceptance](0004-example-name-and-trademark-acceptance.md) | brand |
70
- | 0005 | [Invisible other players in V1](<redacted>.md) | gameplay |
71
- | 0006 | [Singleton WorldRoom + QueueRoom](<redacted>.md) | game architecture |
72
- | 0007 | [Persistent identity via localStorage UUID](<redacted>.md) | game identity |
73
- | 0008-a | [Family-based tile generation](<redacted>.md) | art pipeline |
70
+ | 0005 | [Invisible other players in V1](0005-invisible-other-players-in-v1.md) | gameplay |
71
+ | 0006 | [Singleton WorldRoom + QueueRoom](0006-singleton-worldroom-plus-queueroom.md) | game architecture |
72
+ | 0007 | [Persistent identity via localStorage UUID](0007-persistent-identity-via-localstorage-uuid.md) | game identity |
73
+ | 0008-a | [Family-based tile generation](0008-family-based-tile-generation.md) | art pipeline |
74
74
  | 0008-b | [Google Chat OAuth user auth](0008-google-chat-oauth-user-auth.md) | chat broadcast |
75
75
  | 0009 | [Autotile architecture](0009-autotile-architecture.md) | art pipeline |
76
76
  | 0010 | [GDS architecture](0010-pms-architecture.md) | GDS |
77
77
  | 0011 | [Terrain-id rendering model](0011-terrain-id-rendering-model.md) | game rendering |
78
- | 0012 | [Ground-object cell split](<redacted>.md) | game world data |
79
- | 0013 | [Paths as object overlays with autotile dispatch](<redacted>.md) | game world data |
80
- | 0014 | [Families canonical over vocabulary](<redacted>.md) | art pipeline |
81
- | 0015 | [Task dependencies + auto-promotion](<redacted>.md) | GDS |
82
- | 0016 | [Trust boundary — server-enforced permissions](<redacted>.md) | GDS / security |
83
- | 0017 | [Code simplification & repo cleanliness as a maintained property](<redacted>.md) | GDS / methodology |
84
- | 0018 | [Three-rank model goes live (Xenos → Metic → Archon)](<redacted>.md) | GDS / security |
85
- | 0019 | [Red-team bounty payout table by severity](<redacted>.md) | GDS / security |
86
- | 0020 | [Security model — public stub (full ADR is server-side)](<redacted>.md) | GDS / security |
87
- | 0021 | [Per-builder skill model preferences + audit](<redacted>.md) | GDS / cost discipline |
78
+ | 0012 | [Ground-object cell split](0012-ground-object-cell-split.md) | game world data |
79
+ | 0013 | [Paths as object overlays with autotile dispatch](0013-paths-as-object-overlays-with-autotile-dispatch.md) | game world data |
80
+ | 0014 | [Families canonical over vocabulary](0014-families-canonical-over-vocabulary.md) | art pipeline |
81
+ | 0015 | [Task dependencies + auto-promotion](0015-task-dependencies-and-auto-promotion.md) | GDS |
82
+ | 0016 | [Trust boundary — server-enforced permissions](0016-trust-boundary-server-enforced-permissions.md) | GDS / security |
83
+ | 0017 | [Code simplification & repo cleanliness as a maintained property](0017-code-simplification-and-repo-cleanliness.md) | GDS / methodology |
84
+ | 0018 | [Three-rank model goes live (Xenos → Metic → Archon)](0018-three-rank-model-goes-live.md) | GDS / security |
85
+ | 0019 | [Red-team bounty payout table by severity](0019-red-team-bounty-payout-table.md) | GDS / security |
86
+ | 0020 | [Security model — public stub (full ADR is server-side)](0020-security-model-public-stub.md) | GDS / security |
87
+ | 0021 | [Per-builder skill model preferences + audit](0021-per-builder-skill-model-preferences.md) | GDS / cost discipline |
88
88
  | 0022 | [Secrets policy — what counts, where it lives, how rotation works](0022-secrets-policy.md) | infra / security |
89
- | 0023 | [Kind multipliers + staged idea bonus](<redacted>.md) | GDS / example economy |
90
- | 0024-a | [Cloneable repo + local-first memory with server-canonical sync](<redacted>.md) | GDS / per-builder memory |
91
- | 0024-b | [Multi-Agent System (MAS) architecture](<redacted>.md) | GDS / MAS |
92
- | 0025-a | [Offsite backup vendor: DigitalOcean Spaces](<redacted>.md) | infra / backup |
93
- | 0025-b | [Structured criterion↔task link (`task_criteria`)](<redacted>.md) | GDS / criterion tracking |
94
- | 0026 | [The BFG: documentation/memory hygiene + cross-builder learning + transparent correction](<redacted>.md) | GDS / MAS Phase 6 |
95
- | 0027 | [The BFG session-inefficiency evaluator (auto-post-mortem of builder sessions)](<redacted>.md) | GDS / MAS Phase 6 (6D) |
96
- | 0028 | [Honest LLM-spend accounting (grader envelope cost + session transcript pricing)](<redacted>.md) | GDS / cost ledger |
97
- | 0029 | [Status-page outage resilience (measure from outside, host off-box, prevent at source)](<redacted>.md) | infra / status dashboard |
98
- | 0030 | [Builders-hall live updates via Postgres NOTIFY → WebSocket](<redacted>.md) | GDS / builders hall |
99
- | 0031 | [Cloud dev environments for builders (Claude Desktop → SSH → containerized DO box)](<redacted>.md) | infra / dev environments |
89
+ | 0023 | [Kind multipliers + staged idea bonus](0023-kind-multipliers-and-idea-bonus.md) | GDS / example economy |
90
+ | 0024-a | [Cloneable repo + local-first memory with server-canonical sync](0024-cloneable-repo-local-first-memory.md) | GDS / per-builder memory |
91
+ | 0024-b | [Multi-Agent System (MAS) architecture](0024-multi-agent-system-architecture.md) | GDS / MAS |
92
+ | 0025-a | [Offsite backup vendor: DigitalOcean Spaces](0025-offsite-backup-vendor-digitalocean-spaces.md) | infra / backup |
93
+ | 0025-b | [Structured criterion↔task link (`task_criteria`)](0025-structured-criterion-task-link.md) | GDS / criterion tracking |
94
+ | 0026 | [The BFG: documentation/memory hygiene + cross-builder learning + transparent correction](0026-bfg-memory-hygiene-and-cross-builder-learning.md) | GDS / MAS Phase 6 |
95
+ | 0027 | [The BFG session-inefficiency evaluator (auto-post-mortem of builder sessions)](0027-bfg-session-inefficiency-evaluator.md) | GDS / MAS Phase 6 (6D) |
96
+ | 0028 | [Honest LLM-spend accounting (grader envelope cost + session transcript pricing)](0028-honest-llm-spend-accounting.md) | GDS / cost ledger |
97
+ | 0029 | [Status-page outage resilience (measure from outside, host off-box, prevent at source)](0029-status-page-outage-resilience.md) | infra / status dashboard |
98
+ | 0030 | [Builders-hall live updates via Postgres NOTIFY → WebSocket](0030-builders-hall-live-updates.md) | GDS / builders hall |
99
+ | 0031 | [Cloud dev environments for builders (Claude Desktop → SSH → containerized DO box)](0031-cloud-dev-environments-for-builders.md) | infra / dev environments |
100
100
  | 0032 | [Discord as a one-way mirror of the GDS (outbound broadcast moves off Google Chat)](0032-discord-mirror-of-gds.md) | GDS / team communication |
101
- | 0033 | [Discord bot as a service principal (account-linking, both-mode upvoting, role-sync, inbound retirement)](<redacted>.md) | GDS / team communication |
102
- | 0034 | [`thetes`: a 4th live rank for graduated newcomers, with auto-graduation after 3 ships](<redacted>.md) | GDS / builder onboarding |
103
- | 0035 | [Builder onboarding: three setup paths (Local / DO Box / Remote Control)](<redacted>.md) _(§2 first-box approval superseded by ADR 0059)_ | GDS / builder onboarding |
104
- | 0036 | [Builders subdomain + builder-UI reorganization (hall / watch / settings / status)](<redacted>.md) | GDS / builder UI |
105
- | 0037 | [Bot-managed Discord channels from a config file, Archon-gated commands](<redacted>.md) | Discord bot / channel layout |
106
- | 0038 | [Chromebook/browser on-ramp via a box-hosted ttyd web terminal over a Cloudflare Tunnel (replaces `claude rc`)](<redacted>.md) | GDS / builder onboarding |
107
- | 0039 | [Setup-first onboarding UX: two-config selector + action-only Path checklist](<redacted>.md) | GDS / builder onboarding |
108
- | 0040 | [Remote Control is the default browser-only on-ramp (reverses 0038's "RC is dead" verdict)](<redacted>.md) | GDS / builder onboarding |
109
- | 0041 | [Temporary global grader-bypass kill-switch (`GRADER_BYPASS_ENABLED`)](<redacted>.md) | GDS / shipping pipeline |
110
- | 0042 | [Builder self-deploy: CI auto-deploy on main + PR auto-merge (no builder box holds the prod key)](<redacted>.md) | GDS / shipping pipeline |
111
- | 0043 | [Git/SSH trust-boundary gap + a rank floor on permission-sensitive paths](<redacted>.md) | GDS / trust boundary |
101
+ | 0033 | [Discord bot as a service principal (account-linking, both-mode upvoting, role-sync, inbound retirement)](0033-discord-bot-service-principal.md) | GDS / team communication |
102
+ | 0034 | [`thetes`: a 4th live rank for graduated newcomers, with auto-graduation after 3 ships](0034-thetes-graduated-newcomer-rank.md) | GDS / builder onboarding |
103
+ | 0035 | [Builder onboarding: three setup paths (Local / DO Box / Remote Control)](0035-builder-onboarding-three-paths.md) _(§2 first-box approval superseded by ADR 0059)_ | GDS / builder onboarding |
104
+ | 0036 | [Builders subdomain + builder-UI reorganization (hall / watch / settings / status)](0036-builders-subdomain-and-ui-reorg.md) | GDS / builder UI |
105
+ | 0037 | [Bot-managed Discord channels from a config file, Archon-gated commands](0037-discord-channel-management.md) | Discord bot / channel layout |
106
+ | 0038 | [Chromebook/browser on-ramp via a box-hosted ttyd web terminal over a Cloudflare Tunnel (replaces `claude rc`)](0038-chromebook-ttyd-cloudflare-tunnel.md) | GDS / builder onboarding |
107
+ | 0039 | [Setup-first onboarding UX: two-config selector + action-only Path checklist](0039-setup-first-onboarding-ux.md) | GDS / builder onboarding |
108
+ | 0040 | [Remote Control is the default browser-only on-ramp (reverses 0038's "RC is dead" verdict)](0040-remote-control-default-browser-onramp.md) | GDS / builder onboarding |
109
+ | 0041 | [Temporary global grader-bypass kill-switch (`GRADER_BYPASS_ENABLED`)](0041-temporary-grader-bypass-killswitch.md) | GDS / shipping pipeline |
110
+ | 0042 | [Builder self-deploy: CI auto-deploy on main + PR auto-merge (no builder box holds the prod key)](0042-builder-self-deploy-ci-auto-merge.md) | GDS / shipping pipeline |
111
+ | 0043 | [Git/SSH trust-boundary gap + a rank floor on permission-sensitive paths](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) | GDS / trust boundary |
112
112
  | 0044-a | [Full Mediterranean palette replacement (not a split)](0044-mediterranean-palette-replacement.md) | Art pipeline / palette |
113
- | 0044-b | [Per-box live game preview (the builder sandbox)](<redacted>.md) | GDS / builder experience |
114
- | 0045 | [Dev Box desktop app (menu-bar / tray switch for the builder box)](<redacted>.md) | GDS / builder experience |
115
- | 0046 | [Sandbox-first review in the dev-box ship flow (stage → review → ship)](<redacted>.md) | GDS / shipping pipeline |
116
- | 0047 | [Discord #bugs intake → GDS tasks, straight-through with a layered anti-abuse guard](<redacted>.md) | GDS / Discord bot |
117
- | 0048 | ["Works in Progress" IA split: work board owns it, status mirrors a thin slice](<redacted>.md) | GDS / builder UI |
118
- | 0049 | [The split parallel-safety contract (detect-at-merge for humans, gate for the autonomous runner)](<redacted>.md) | GDS / methodology |
119
- | 0050 | [Device-Flow admission posture: invite-gated by default (with an `OTB_OPEN_XENOS_ENROLLMENT` escape-hatch)](<redacted>.md) | GDS / builder onboarding |
120
- | 0051 | [Full session transcripts in the BFG corpus (with transcript-grade scrubbing)](<redacted>.md) | GDS / BFG |
121
- | 0052 | [Sandbox for everyone: a game-only preview entrypoint + the game in the starter clone](<redacted>.md) | GDS / dev boxes |
113
+ | 0044-b | [Per-box live game preview (the builder sandbox)](0044-per-box-live-game-preview.md) | GDS / builder experience |
114
+ | 0045 | [Dev Box desktop app (menu-bar / tray switch for the builder box)](0045-devbox-desktop-app.md) | GDS / builder experience |
115
+ | 0046 | [Sandbox-first review in the dev-box ship flow (stage → review → ship)](0046-sandbox-first-review-gate.md) | GDS / shipping pipeline |
116
+ | 0047 | [Discord #bugs intake → GDS tasks, straight-through with a layered anti-abuse guard](0047-discord-bugs-intake-anti-abuse.md) | GDS / Discord bot |
117
+ | 0048 | ["Works in Progress" IA split: work board owns it, status mirrors a thin slice](0048-works-in-progress-ia-split.md) | GDS / builder UI |
118
+ | 0049 | [The split parallel-safety contract (detect-at-merge for humans, gate for the autonomous runner)](0049-split-parallel-safety-contract.md) | GDS / methodology |
119
+ | 0050 | [Device-Flow admission posture: invite-gated by default (with an `OTB_OPEN_XENOS_ENROLLMENT` escape-hatch)](0050-device-flow-admission-invite-gated-by-default.md) | GDS / builder onboarding |
120
+ | 0051 | [Full session transcripts in the BFG corpus (with transcript-grade scrubbing)](0051-full-session-transcript-corpus.md) | GDS / BFG |
121
+ | 0052 | [Sandbox for everyone: a game-only preview entrypoint + the game in the starter clone](0052-sandbox-for-everyone-game-only-preview.md) | GDS / dev boxes |
122
122
  | 0053 | [Scoped, short-lived dev-box session + apex-cookie strip at the tunnel (SR#2/#11)](0053-scoped-dev-box-session.md) | GDS / dev boxes + security |
123
- | 0054 | [Flat cost-plus session token reward (example as equity): reward = round(true_cost_usd × 1.20 × 100), server-priced from per-model tokens, idempotent per session_id](<redacted>.md) | GDS / economy |
124
- | 0055 | [Server-mediated branch publish (the dev box holds no GitHub credential): box uploads a thin bundle, the GDS pushes + opens the PR + auto-merges with a server-side PAT](<redacted>.md) | GDS / dev boxes + ship pipeline |
125
- | 0056 | [Version-control the prod deploy script as a reviewable mirror (`scripts/deploy/deploy-prod.sh`), and retry the post-restart `/healthz` so a slow service bind no longer false-fails a successful deploy](<redacted>.md) | GDS / deploy pipeline |
126
- | 0057-a | [Approve new members and first dev boxes from Discord: two Archon-only channels where a 👍/👎 fires the existing admit (`access_requests`) / first-box (`box_intents`) approve action, re-checked against the reactor's live rank](<redacted>.md) _(box half superseded by ADR 0059)_ | GDS / Discord + onboarding |
127
- | 0057-b | [Per-builder container cost ledger (org-funded → self-fund tracking)](<redacted>.md) | GDS / cost + dev boxes |
128
- | 0058 | [Automatic gate-surface gatekeeper (rank-based auto-approval + deterministic hard floor)](<redacted>.md) | GDS / shipping pipeline |
129
- | 0059 | [Single approval: remove the dev-box (first-box) Archon approval gate — collapse to one human gate (member-join admission); an admitted builder's first box provisions hands-off](<redacted>.md) | GDS / builder onboarding |
123
+ | 0054 | [Flat cost-plus session token reward (example as equity): reward = round(true_cost_usd × 1.20 × 100), server-priced from per-model tokens, idempotent per session_id](0054-session-token-reward-cost-plus.md) | GDS / economy |
124
+ | 0055 | [Server-mediated branch publish (the dev box holds no GitHub credential): box uploads a thin bundle, the GDS pushes + opens the PR + auto-merges with a server-side PAT](0055-server-mediated-branch-publish.md) | GDS / dev boxes + ship pipeline |
125
+ | 0056 | [Version-control the prod deploy script as a reviewable mirror (`scripts/deploy/deploy-prod.sh`), and retry the post-restart `/healthz` so a slow service bind no longer false-fails a successful deploy](0056-prod-deploy-script-mirror-and-healthcheck-retry.md) | GDS / deploy pipeline |
126
+ | 0057-a | [Approve new members and first dev boxes from Discord: two Archon-only channels where a 👍/👎 fires the existing admit (`access_requests`) / first-box (`box_intents`) approve action, re-checked against the reactor's live rank](0057-discord-archon-approval-channels.md) _(box half superseded by ADR 0059)_ | GDS / Discord + onboarding |
127
+ | 0057-b | [Per-builder container cost ledger (org-funded → self-fund tracking)](0057-container-cost-ledger.md) | GDS / cost + dev boxes |
128
+ | 0058 | [Automatic gate-surface gatekeeper (rank-based auto-approval + deterministic hard floor)](0058-automatic-gate-surface-gatekeeper.md) | GDS / shipping pipeline |
129
+ | 0059 | [Single approval: remove the dev-box (first-box) Archon approval gate — collapse to one human gate (member-join admission); an admitted builder's first box provisions hands-off](0059-single-approval-remove-devbox-approval-gate.md) | GDS / builder onboarding |
130
130
  | 0060 | [GDS retrieval layer: Postgres-resident, lexical-first (FTS + pg_trgm fused with RRF), rank-scoped, fail-open — the "one-call recall" architecture (no new infra; embeddings deferred behind a measured gate)](0060-gds-retrieval-layer.md) | GDS / recall + search |
131
- | 0061 | [Context-layer decomposition: a CLAUDE.md content charter + nested per-module `CLAUDE.md` (load on demand) to fight the AI-authored "context cliff" — keep the always-loaded root lean, push module depth to where it's used](<redacted>.md) | GDS / methodology + context |
132
- | 0062 | [Medusa: decouple the GDS into an instance-model build platform — the cut-line (portable core / host instance / gated feature-modules), the branding-contract schema, the feature-module selection mechanism; folds in the CLAUDE.md core/host split + the A→B→C context plan](<redacted>.md) | GDS / Medusa decoupling |
133
- | 0063 | [Generated code symbol skeleton (the repo map): a deterministic, zero-dependency, CI-regenerated altitude map — top exported symbols by reference frequency + per-dir signatures — that feeds the factual half of the nested `CLAUDE.md` docs so they can't drift (Path B; complements the prose recall layer with code)](<redacted>.md) | GDS / context + repo map |
134
- | 0064 | [Rename the platform **Medusa → Cloud Bongos** (owner directive): whole-platform rename, history preserved — canonical config identity + forward docs renamed, historical records (ADR 0062 body, logs, migrations, the R50–R74 task descriptions) kept as-is; amends ADR 0062](<redacted>.md) | Cloud Bongos / branding |
135
- | 0065 | [Cloud Bongos is **AGPL-3.0, non-profit, and AI-first**: AGPL closes the hosted-closed-fork (SaaS) loophole — the only license consistent with the anti-monopoly goal; non-profit + capture-resistant governance (never monetized from contributors); AI-first as a documented norm, not a technical gate; defines the public-mirror boundary (portable core + platform published; host identity/secrets/content + a ~6-month redaction window never). Rejects MIT/Apache (capture), GPL (SaaS loophole), BSL/open-core (not OSS), CLA-relicensing (capture). The C9 keystone — gates R69–R73](<redacted>.md) | Cloud Bongos / open-source governance |
136
- | 0066 | [Generated file-map sections: generate the `.claude/skills/` + `.claude/scheduled-tasks/` trees of `docs/file-map.md` from disk, with per-entry notes in a sidecar (`docs/file-map.notes.json`); `--check` fails CI on drift or an undescribed skill/routine (Path C), so a new skill can't land un-mapped and the doc-hygiene follow-up task is retired. Leaves the curated `scripts/gds/` highlight list hand-written.](<redacted>.md) | GDS / context + file map |
137
- | 0067 | [Fast-forward the checkout to origin/main at session start when safe (clean `main` full clone, strictly behind) — the warn-only→pull-when-safe reversal of the SessionStart freshness step; still warns in every unsafe case (feature-branch worktree, dirty, diverged, partial clone); event-driven freshness, not a faster cron](<redacted>.md) | GDS / dev boxes + methodology |
138
- | 0068 | [Shared Gemini art key delivered from the GDS to newcomers](<redacted>.md) | GDS / art pipeline |
139
- | 0069 | [Builders-hall live updates move from a Colyseus WebSocket room to an SSE endpoint (`GET /api/gds/live`) so the hall refreshes live on a game-less Cloud Bongos boot (R55, C8). Supersedes ADR 0030's §2 transport only — the `NOTIFY` + `live-channel.js` signal layer is unchanged; same opaque zero-leak `nudge`, `requireBuilder` gate, per-IP cap kept inline (no `src/rooms/` import). Deletes the room + join-guard; the hall stops loading `/vendor/colyseus.js`. EventSource auto-reconnect replaces the manual rejoin backoff.](<redacted>.md) | GDS / builders hall |
140
- | 0070 | [Deterministic delivery of in-session cards (hook-delivered, read-only vs mutating)](<redacted>.md) | GDS / context |
141
- | 0071 | [Dev-box teardown confirms the DigitalOcean droplet is actually deleted BEFORE marking the GDS row 'destroyed' (idempotent + retried; a failed delete parks at 'error' with droplet_id preserved, never a silent zombie); adds a bidirectional `reconcile-drift` sweep (force-delete zombie droplets no live row claims, mark ghost rows whose droplet vanished) on an hourly timer, with an in-flight young-droplet guard. Fixes the running-but-'destroyed' zombie that froze a box on old code.](<redacted>.md) | GDS / dev boxes |
142
- | 0072-a | [Bongos app ships Mac-signed + notarized first; Windows code signing deferred (BV1.R01, C1). Reuses the Dev Box app's signing identity verbatim — the same Apple Developer ID + the inert-until-armed, `main`-ref-gated (SR-13/SR-14) Mac block — rather than minting a new cert; declines a Windows cert for now, riding the existing Windows-only blocker [#20](https://example.com/builders#/blocker/20) (Azure Trusted Signing vs EV). One cert decision serves both apps once they merge into the umbrella. $0 new recurring cost; Windows users keep the "Run anyway" first-run friction until blocker [#20](https://example.com/builders#/blocker/20) resolves.](<redacted>.md) | Bongos app / packaging |
143
- | 0072-b | [Dev-box code staleness: make it visible, never auto-reset](<redacted>.md) | GDS / dev boxes |
144
- | 0073-a | [The secrets scan excludes TruffleHog's generic URI detector. It flagged a `scheme://userinfo@host` SSRF-guard rejection-test fixture (and, fatally, a URL quoted in a fix commit's message — TruffleHog scans commit messages, which `--exclude-paths` cannot reach), reddening every PR off main once a dev-box tunnel host stopped resolving (ADR 0071). Excluding the URI detector on both scan surfaces loses no real coverage — connection-string secrets (postgres://, mysql://, …) keep their dedicated detectors. Lesson: exclude noisy detectors at the detector level, and keep commit messages free of credential-shaped URLs.](<redacted>.md) | GDS / CI gates |
145
- | 0073-b | [A consistent "the system needs an input from you" signal, and pragmatic BYOK for the per-builder Gemini key](<redacted>.md) | GDS / builder signals + keys |
146
- | 0074 | [The memory map stays light (budget guard + auto-tidy + drawer)](<redacted>.md) | GDS / memory |
147
- | 0075 | [Per-system cost attribution + the savings counterfactual ledger (System 1 of the LLM-reduction redesign). Corrects LLM pricing to flat Opus 4.8 rates (no 1M premium); splits the single `session` `cost_log` row into call-site-tagged rows (`session:<redacted>, `cron:<routine>:*` via `OTB_COST_CRON_ROUTINE`) each carrying a per-tier USD split (migration 123); adds a `cost_baseline` table (migration 124) where every later system records a `priceUsage()`-computed counterfactual (never client-supplied) so savings are `SUM(baseline−actual)` by system; surfaces `by_source`/`by_tier` (cache_read share) / `savings` as additive blocks on the unchanged `/public/cost-summary`, with the audit folding them in plus a `savings_without_baseline` tripwire. Turns the 82%-cache_read finding from an inference into a queryable column.](<redacted>.md) | GDS / cost ledger |
148
- | 0076 | [In-session deterministic compaction (System 2 — the only lever on the 82% marathon-cache_read pool, est 20-40%). A pure classifier (`.claude/hooks/eviction-policy.js`) finds provably-stale `tool_result` blocks (superseded reads, duplicate reads, old uncited bulk dumps) and replaces each with a one-line recovery stub; never touches user/assistant/reasoning turns, the context-pack, worktree guidance, the last N turns, or the newest read of a file; a backward-reference liveness scan keeps anything a later turn cites. Batched ONLY at the PreCompact boundary (per-turn would invalidate the prompt cache); recovery is retrieval-first via the stub → `/recall` (search.js) / re-Read / tool-rerun loop, with a one-time directive that prevents re-fetch loops. Wired behind `OTB_INSESSION_COMPACT`, OFF by default (wire, don't arm). Spike 1347 measured ~24% realized headroom on real transcripts.](<redacted>.md) | GDS / context + cost |
149
- | 0077 | [Content-addressed LLM cache (System 5 — the residual safety net, est 2-5%). Memoizes idempotent model runs at the single shared spawn primitive (`grader.runSubagent`): an identical (normalized-prompt, model, task_type, git-tree-SHA) invocation over UNCHANGED inputs replays a stored result at ZERO new tokens. Validity is bound to the git SHA of the embedded inputs (self-invalidating — a real change always re-runs); the per-task-type normalizer registry strips only provably-volatile lines and is the correctness surface. Stores only on exit_code===0 (never a timeout); the wrap (`runSubagentCached`) is opt-in and behavior-preserving; a hit returns cost_usd:0 and records the saving into `cost_baseline` (ADR 0075). First plug-ins: architect-audit (tree-scoped) + onboarding-analyze (immutable session id). v1 is exact-match; semantic match deferred.](<redacted>.md) | GDS / cost |
150
- | 0078 | [Deterministic-first cron harness (System 4 — stop wrapping a committed script in an Opus session, est 3-6%). A routine declares `mode: deterministic\|hybrid\|judgment` + `steps:` in SKILL.md frontmatter; `run-routine.js` runs deterministic routines as plain node with NO model launched (hybrid escalates to `claude -p` only on a `needs_judgment` payload). peer-vote-tally + diagram-drift converted (their scripts already self-file); main-audit (trust-boundary core, ADR 0043) + dep-audit-nightly parked (their security scripts don't self-file — converting would drop alerting). A fitness check (`checkDeterministicRoutines`) fails CI if a deterministic body carries judgment/mutation instructions (git push/commit, deploy.sh, Edit/Write, synthesis — negated boundary mentions allowed) or its prose commands diverge from its steps, so the boundary can't rot back into a wrapped session. Cron-line repoint is an operator action.](<redacted>.md) | GDS / cron + cost |
151
- | 0079 | [Value-gated autonomous engagement (System 3 — the autonomous-spend lever, est 8-14%; **SHIPPED INERT**). Merges three spawn decisions behind one off-by-default flag (`OTB_AUTONOMY_ENABLED`): a $0 deterministic precheck (`autonomy-gate.js`) that NO-GOs a nightly routine on no-signal nights or a spend-brake (audited in `autonomy_runs`, migration 126); a cascade router (`cascade-dispatch.js`) that drafts a spawn on the cheapest sufficient model and escalates only on an objective signal, never above the builder's ceiling; and a safe_autofix lane that collapses review→triage→rebuild into one fix-and-ship run — permitted ONLY when high-confidence + single-file + not a protected path + the grader is LIVE (never while GRADER_BYPASS is on). BUILT DISABLED per owner directive: decide() returns GO and autofixAllowed() returns false by default, so wiring changes nothing until armed alongside the grader rework. Does NOT touch marathon spend (that's System 2 / ADR 0076).](<redacted>.md) | GDS / autonomy + cost |
152
- | 0080 | [LLM-cache API transport + trust model (System 5 — the transport that finally lets the content-addressed cache of ADR 0077 hit). Its two consumers (`architect-audit.js`, `onboarding-analyze.js`) run as CLI/cron on builder machines where the `pg` pool is unreachable (peer-socket only), so every cache op silently degraded to miss/no-store and the cache never hit organically (eval task 1372: 0 rows, 0 hits despite correct logic). Fix: expose the cache over the GDS API — `POST /api/gds/llm-cache/lookup` + `/store` in `src/bongos/routes/llm-cache.js`, each mapping 1:1 onto the existing `llm-cache.js` DB helper (the route never reimplements SQL; `/store` mounts its own 512 KB body parser since a full subagent stdout exceeds the global 64 KB cap). `llm-cache.js` self-routes: pool first (memoized `poolReachable()` — a `false` is never memoized so a boot blip can't permanently disable the server cache), else an injected `apiCall` fallback (`setApiFallback`), so the always-on server core never statically requires the CLI layer (preserving the core↔host boundary the fitness gate guards). Best-effort end-to-end: any pool/network/auth error degrades to miss/no-store and never throws. Trust model (load-bearing): a store writes a `result_stdout` a later run replays at $0 and trusts, so an open store endpoint is a content-injection vector into the planning pipeline and a metrics-integrity vector into the public savings number — hence rank-gated, reusing the ADR 0016 server-enforced trust boundary.](<redacted>.md) | GDS / LLM cache + cost |
153
- | 0081 | [Tool-agnostic design layer — the repo (design tokens + component code + exported assets) is the single source of truth; Claude Design, Figma, and any future design tool are SWAPPABLE ADAPTERS against that neutral contract, not hardwired integrations (the ADR 0062 core↔host pattern applied to design — vendor-lock-in insurance the owner asked for). Splits a `ui` discipline out of `artist`/graphics (UI = design-as-code, graded by diff + screenshot; graphics = pixel art, graded by vision). Code-canonical round-trip: structured UI round-trips as code, freeform canvas art is a committed asset; a talented human can still edit directly in Figma and the change round-trips into the repo via a normal GDS task (the escape hatch). One ship pipeline + ONE multimodal grader with a single regen loop, parameterized by type (rubric + evidence chosen by task type) — retires the standalone Gemini vision path and the two-grader-set maintenance trap. Claude Design adapter = code-native /design-sync + Claude Code handoff; Figma adapter = human-edit→code + code→Figma sync. Grader/ship changes ride the gate-review hard floor (ADR 0043). Implementation cluster tasks 1376–1380.](<redacted>.md) | GDS / design pipeline |
154
- | 0082-a | [Server-side auto-resolution of generated-file & append-index merge conflicts: the publish-reconciler runs a temp-clone `git merge origin/main` with the `otb-regen` driver + a `docs/adr/README.md merge=union` attribute active, then pushes the resolution so a PR that conflicts ONLY on generated indexes or the ADR append-index self-heals server-side (GitHub's own merge can't run the custom driver) — the PR #300/[#167](https://example.com/builders#/task/167) stall. Pushes a commit (all required checks re-run), aborts to a human on any genuine hand-conflict, best-effort + kill-switchable; zero trust-boundary change (rejects the alternative of lowering deploy access to `thetes`).](<redacted>.md) | GDS / CI + merge |
155
- | 0082-b | [Productized key-storage provisioning (server-generated master key, zero SSH)](<redacted>.md) | security / key storage |
156
- | 0083 | [Practical build plan for the modular architecture (strangler migration) — the decision record that turns the scoped design research (`docs/design/modular-architecture/`, 5 docs) into a concrete, CI-guarded, one-module-at-a-time build. Builds the whole product into `core + modules`: a ~3.5k-LOC kernel + ~16 modules, each a vertical slice reached through ONE published, semver'd doorway (`src/module-api.js` — the only core file a module may import); a loader that discovers/validates/mounts `modules/*/module.json` behind kernel-composed auth and replaces the closed `KNOWN_MODULES` registry in `src/modules.js`; modules talk only through kernel-mediated seams (ports + events), never by importing each other. Boundaries are held by fitness functions (a violation is a red build), not discipline — five new `fitness.js` checks (core-never-imports-a-module, <redacted>, module-route-rank-gate via parameterizing `route-rank-check.js`, core-carries-no-module-code, module-migrations-additive+namespaced). Module-owned migrations generalize the working `migrations/game/` enabled-gated precedent to `modules/<key>/migrations/`. Reorganize, NOT rewrite (the foundation is sound, the filing is wrong); stay single-package/single-process/no-bundler (modularity is a separate axis). Moves ordered by MEASURED isolation: dev-box (the template) → game → discord → art-pipeline; the recurring seam is `routes/me.js` + `settings.js`, cut by a module→core contribution hook. task→module affinity (retire `touches[]`) is the final-tier follow-on. Reconciles the numbering collision: the design docs + epic [#1388](https://example.com/builders#/task/1388) + task [#1389](https://example.com/builders#/task/1389) call this "ADR 0080", but 0080 is the LLM-cache — the module-system record is THIS file. Seeds BONGOS-V1 criterion C5 + the BV1.R## children.](<redacted>.md) | GDS / architecture |
157
- | 0084 | [Auto-derived per-task rank floor — every task is rank-scoped and the claim gate finally BITES. `tasks.requires_rank` (V3.R82) + the claim-time gate (`db.claimTask` → `INSUFFICIENT_RANK`) + the `/builder-start` hide were all sound but INERT: no create/edit path ever set `requires_rank`, so every task took the `NOT NULL DEFAULT 'xenos'` and anyone at thetes+ could claim anything. Fix: `deriveRequiredRank(touches, security_sensitive)` derives a floor by reusing the SAME protected-path matcher as the trust boundary (`permission-path-check.matchProtected`, ADR 0043) — the rank/authz/ship-grade/deploy/migration core (the paths a sub-Metic builder already can't push) floors at `metic`, game/art/docs stay `xenos`; never auto-assigns `archon`. `highestRank(floor, requested)` composes it with an Archon override that can only RAISE (a request below the floor is clamped up; `POST`/`PATCH /tasks` accept `requires_rank`, Archon-only; flipping `security_sensitive` ratchets up). Migration 145 backfills every non-terminal task (raise-only, idempotent, SQL generated faithfully from `PROTECTED_GLOBS`). Hall shows a rank pill. One sensitivity source of truth shared with the git-push floor, so claim-floor and push-floor can't drift.](<redacted>.md) | GDS / authz |
158
- | 0085 | [Builders are uncapped by default — removes the default $50/mo per-builder spend cap. `classifyCostStatus` no longer falls back to a project default: a `NULL`/`<=0` `monthly_budget_usd` now means UNCAPPED (`level:'ok'`, `budget_mtd_usd:null`, nothing blocks), and the `DEFAULT_MONTHLY_BUDGET_USD=50` constant is deleted. Reverses the default-cap half of GDS-V3 C3 (task 302): the default blocked the very work it was meant to enable — a new builder's own `session:<redacted> API cost crossed $50 within a day and froze /builder-ship + the art pipeline (builder 95 hit $69.32 MTD on day two). KEEPS the Archon override (`PATCH /builders/:id/budget`) as a manual emergency brake — a positive value still enforces warn(≥80%)/over(≥100%). Cost recording + the /public/cost-summary dashboard are unchanged (removes blocking, not observability). No schema change; project-wide guards (autonomy-gate, art HARD_STOP) unaffected.](<redacted>.md) | GDS / cost ledger |
159
- | 0086 | [Goal-scoped work hierarchy — assign goals, not tasks. Introduces a `goal` tier and a six-level hierarchy (Project → Version → Goal → Criteria → Task → Sub-task) whose structure is a per-instance config preset (`config/hierarchy.json`, owner-set at onboarding — the ADR 0062 pattern) and whose enforcement is core. A goal is a SHARED, joinable, module-scoped workspace: joining grants bounded authority to author tasks within the goal's `scope_modules` (the wall, set by a trusted rank at planning, never self-granted; widened only by an approved request — Archon for protected modules). Reuses almost everything: criteria re-parent under goals (`done_when_criteria.goal_id`; `/status` rolls up Version→Goals→Criteria→Tasks), sub-tasks stay `parent_task_id`, the per-task rank floor is the auto-derived one (ADR 0084), dependencies become a polymorphic blocked-until-done edge resolved at the claim gate (ADR 0015 — cross-goal allowed, reference-not-grant). Achievement auto-flags when a criterion's tasks ship, then a planner (Metic+) confirms via a new `/goal-review` skill (idea-triage-style), auto-flipping the goal achieved. Deterministic scope + protected-path gate at create; the ship-time grader/gate-review/main-audit stay authoritative on the real diff. Deferred to follow-ons: the reward model, sub-Metic membership in protected-scope goals (amends ADR 0043), and an AI relevance judge. Fills ADR 0083 Rule 5 ("a task is scoped to a module").](<redacted>.md) | GDS / work hierarchy |
160
- | 0087-a | [Bongos app architecture + the model-agnostic feedback handoff contract (BONGOS-V1 / C4). Records the umbrella desktop app that absorbs the Dev Box app as a module, the capture→synthesize→handoff pipeline (R04–R14), and the Rung-3 *visual* capture floor (pixels + voice + freeze-frame markup over any app) with element-level reads deferred to V2 (Accessibility requested lazily, never up front). The contract: a bundle is a self-contained `~/CloudBongos/feedback/<id>/` dir where every screenshot is referenced by ABSOLUTE FILE PATH, never base64 — so the text prompt stays ~5–11k tokens and any agent opens the frames with its own file read. One shared `latest` pointer (atomic `latest.json` + best-effort symlink, self-healing) feeds three readers that can't drift: Claude Desktop (clipboard + open folder, R12), Claude Code (`/feedback` slash command cats prompt.md + Reads the frame paths, R13), and an optional zero-dependency stdio MCP server (`get_latest_feedback` returns text + file paths, never base64, R14). Hand-rolled MCP (newline-delimited JSON-RPC, no SDK dep) keeps the install surface flat. Bongos succeeds the GDS as the primary builder surface after GDS-V4; a fourth agent is an adapter, not a redesign. $0 new recurring cost.](<redacted>.md) | Bongos app / feedback loop |
161
- | 0087-b | [Compete with Cursor on governance, not tooling](<redacted>.md) | strategy / positioning |
162
- | 0088 | [Consistent-character generative animation — a generate→grade→audit→regenerate loop built by EXTENDING the existing family pipeline (ADR 0008: `run_family` + cross-frame coherence + the `character_identity` Gemini review + `t5_anchor_refine`), not a new grader/loop (the owner's "don't reinvent the loop"). Decision (with the owner, 2026-06-26): option A — Gemini + grade/audit with a PLUGGABLE generator backend (the ADR 0062/0081 adapter pattern) — over a per-character LoRA, which is deferred as "level-2" (needs GPU training + an open model). The family pipeline is already a consistent-character multi-frame loop (walk/peck cycles ship through it) but tuned for 32×32 pixel-art; this adds a painterly `character_anim` family kind (drops palette-snap/32×32), the two genuinely-new criteria `pose_match` + order-aware transition-coherence (existing checks are order-agnostic — they'd pass a shuffled animation), a recursive Gemini inbetween layer for high frame counts (≈12–24 "on twos"/"on ones"), a painterly cutout/register post-process, and packaging as a Cloud Bongos module (ADR 0083). First consumer = the Bongo Buddha's 8 actions → widget (task 1523). Implementation epic: tasks 1549–1559.](<redacted>.md) | GDS / art pipeline |
163
- | 0089 | [Modular, multi-model adversarial grader (open-weight ensemble) — refactor the ship-gate into a central, work-type-parameterized engine (gather evidence → deterministic pre-passes → judge ensemble → normalize+aggregate → gate → persist) where each MODULE contributes only its rubric + evidence-gatherer + specialist critics through the ADR 0083 seam (`grader.workType.<key>`), answering the owner's "per-module or general?" as central-engine + module-plugged. Adds open-weight LLM judges from a DIFFERENT model family than the author (Claude stays as the quality floor, never the sole judge) to kill same-family self-preference inflation (10–25%); judges run via a `ModelProvider` adapter (`openai-compatible` → DeepInfra/OpenRouter/vLLM/Ollama, the ADR 0062 neutral-contract pattern) on a hosted pay-per-call API (~$1–5/mo, owner spend-gated). Small (2–3) audited panel — z-score/rank-normalize then median + hard-veto aggregation; an error-correlation audit routine (Cohen's κ vs owner decisions) keeps the jury failing DIFFERENTLY (the "Nine Judges, Two Effective Votes" finding) rather than assuming vendor-diversity. Deterministic route-rank + permission-path pre-passes stay ZERO-LLM and authoritative (no LLM verdict, Claude or open, can upgrade a fail over a protected path; ADR 0016/0043/0080); VLM judges advisory on art, deterministic palette/grid checks authoritative. Owner decisions 2026-06-26: central-engine + module-plugged; ensemble vote; hosted API. Reuses the worker-contract + task_grades schema + LLM cache (ADR 0077); rolls out advisory-first behind a flag. Build cluster task-pending (task 1561).](<redacted>.md) | GDS / grader + quality gate |
164
- | 0090 | [Metic gains task authoring — re-ranks `POST /tasks`, `PATCH /tasks/:id`, `POST /tasks/:id/promote`, and the `tasks/:id/{dependencies,criteria}` CRUD from Archon-only to `requireRank('metic','archon')` (owner decision 2026-06-26: a rank trusted to triage ideas is trusted to seed + shape the tasks they become). Moves the two `EXPECTED_RANKS` pins in lockstep and updates the `// rank:` annotations so the route-rank audit stays green. Keeps Archon-only: rank-setting, version-close, abandon, grader-bypass confirm, and the ADR 0086 cross-tier `POST /dependencies` + goal-scope edges. Amends ADR 0018's table row 7; sits beside ADR 0086, which remains the module-fenced authoring path for sub-Metic ranks. "Tighten later" debt explicitly accepted — no per-version/module/goal fence on a Metic's authoring.](<redacted>.md) | GDS / permissions |
165
- | 0091 | [Bounding the kernel and carving `db.js` per-domain (Config-B core decomposition) — amends ADR 0083 to extend the strangler from the four FEATURE modules into the CORE itself (BONGOS-V1 C7). Answers the three questions the audit opened: (1) WHAT IS THE KERNEL — replaces the 6-file machinery-only `fitness.js` `KERNEL_FILES` with an enumerated ~3.5k-LOC trust roster (auth · route-rank-check · permission-path-check · secret-box · pool · the new `db-kernel.js` identity/audit/rank/`withTx` slice · instance-config/branding/build-info · routes/_helpers · the module-system machinery + module-scope-map); everything else in `src/bongos/` is a domain destined to become a module or to stay inside the un-split lifecycle state machine. (2) HOW A DOMAIN OWNS ITS SQL — ratifies the proven `modules/discord/db.js` + `modules/game/db.js` pattern (own queries lifted verbatim + deleted from core, reached through the doorway `pool`/`withTx`; kernel helpers re-exported behind the same `db.*` namespace; boundary-forced twins allowed + documented) as THE per-domain carve mechanism; `db.js` shrinks toward a kernel slice + the lifecycle residue. (3) WHAT THE DOORWAY EXPOSES — classifies `module-api.js` re-exports into kernel (stays: identity/audit/rank/auth/seams/config + `withTx`), domain-leak-with-a-home (relocate now: `setBuilderBoxBlocked`→dev-box, gemini-key trio→art-pipeline), and domain-leak-pending-carve (`createTask`/`captureIdea`/`VOTABLE_TASK_STATUSES` stay as MARKED temporary re-exports until lifecycle/ideas are carved in tranche 2 — named, owned, tracked debt, not pretend-kernel). A lifecycle-invoked capability resolves through a kernel PORT, never a direct import (R73 replaces `routes/tasks.js`→`grader.js` with `resolve('grade')`). Deliverable = this decision + the seeded, dependency-wired R68–R74 sub-task shape; OTB byte-identical at every step. Tranche 2 (the other ~8 core domains) deferred per owner.](<redacted>.md) | GDS / architecture |
166
- | 0092 | [Hosting module + on-prem (warehouse) hosting target — scopes a portable Cloud Bongos `hosting` module (default-OFF, fail-to-vanilla) that brokers compute + storage workloads (inference / imagegen / embeddings / training / storage) to configured hosting *targets* through kernel seams, resolving each via `resolveOptional(port, cloudDefaultFn)` so a box-less / module-off instance degrades to cloud automatically. The warehouse GPU box is OTB's first ON-PREM target — one adapter backend declared in HOST-instance config (the ADR 0062 / 0081 core↔host + swappable-adapter pattern), shared across all Example instances, reached over an outbound tunnel the box initiates (no open ports / no static IP), sitting OUTSIDE the trust boundary like Discord (ADR 0033): it computes, it never grants rank or lands code (ADR 0016 / 0043). Inference REUSES the ADR 0089 `ModelProvider` openai-compatible adapter (the on-prem box = one more vLLM / Ollama endpoint) and adopts 0089's decisive finding that low-volume work is cheaper on pay-per-call APIs — so the GPU's honest justification narrows to GPU-bound batch generation, open-model fine-tuning / LoRA (the ADR 0088 deferred level-2), and always-busy bulk, plus the non-GPU wins of backups / storage + an unmetered sandbox; code that ships stays on Claude. Owner decisions 2026-06-27: serve all instances, conservative posture, proceed. Phased: scope use ✅ → build (task 1619) → hardware (task 1620) → software; the cost read-out (task 1621) is an independent input.](<redacted>.md) | Cloud Bongos / hosting |
167
- | 0093 | [Tranche-2 core-carve sequence (Config-B, completing C7) — amends ADR 0091, which deferred tranche 2 "per owner"; owner greenlit 2026-06-27. Tranche 1 proved the carve pattern (memory R72, grading R73) AND shipped the reusable infra (bounded kernel + `KERNEL_FILES` ratchet, doorway/`withTx`, per-domain `db.js` slice + `db-kernel.js`, the `grade` kernel port, `coreB-carve-proof.sh`), so each remaining core domain is now a PURE carve. Fixes (1) the ORDER — clean-slicing-first per the 02-module-map import-graph: economy → ideas → security → builder-settings → onboarding → sessions/BFG → autonomy/MAS → status-UI → hall-UI → lifecycle LAST; (2) economy registers the `reward` kernel PORT (twin of `grade`) so the lifecycle resolves grade+reward via seams and imports only the kernel — which is WHY economy is first and the lifecycle state machine is carved last + whole (never split, 02 §4); (3) every carve is proof-gated byte-identical and C7 is satisfied only by the final all-domains proof (R87) — tranche 1 proved the pattern, tranche 2 completes the decomposition the done-when demands; (4) the ADR-0091 §3 temporary doorway leaks retire as their domains carve (`captureIdea`→ideas R78, `createTask`/`VOTABLE_TASK_STATUSES`→lifecycle R86). Deliverable = this decision + the seeded, dependency-wired R76–R87 sub-task shape.](<redacted>.md) | GDS / architecture |
168
- | 0094 | [Repo Atlas — a self-drawing map of the codebase (idea [#457](https://example.com/builders#/idea/457)): a Builders' Hall page rendering an isometric, hover-connective, click-for-detail map in two views — CONTEXT LAYER (the always-loaded root `CLAUDE.md` + its budget gauge vs the on-demand nested guides, the docs library, the DB as live state, broken-link drift) and MODULE TRUTH (the real modules carved into `modules/<key>/` with clean boundaries vs the ~26k-line `src/bongos` core blob + the `db.js` monolith vs the planned-but-still-fused ghosts). Data = one generated `public-builders/atlas.json` from `scripts/gds/gen-atlas.js`, PURE file-reads, reusing the existing truth sources (`module-scope-map` MOVED-vs-FUSED, `fitness.js` boundary scan for clean-vs-violation + kernel leaks, `docs-entropy` stale links, the `CLAUDE.md` budget). Regenerated SERVER-SIDE on every deploy (a `deploy-prod.sh` step, the world-snapshot precedent) — never on the shipping builder's machine; gitignored build artifact; page degrades gracefully pre-generation; styled in the Cloud Bongos brand, gated to any signed-in builder, built to port to Cloud Bongos. Owner decision 2026-06-27. Follow-up task [#1641](https://example.com/builders#/task/1641) migrates the repo-map/session-index/file-map generators onto the same server-side trigger (diagrams excepted: headless browser + auth).](<redacted>.md) | Cloud Bongos / docs |
169
- | 0095-a | [Borrowed memory, retrieval & collision concepts from claude-mem (Apache-2.0) and graphify (MIT) — after evaluating both Claude-Code tools and rejecting either wholesale (Python stack / local-daemon+Chroma architecture / no rank-scoping / open-core+memecoin governance), adopt eight stack-agnostic CONCEPTS, reimplemented in-stack with no new dependency or service ("borrow concepts, not tools"): from graphify — symbol-level collision detection (path-prefix→symbol/dependency overlap in `path-match.js`, the forecast-accuracy lever ADR 0049 left open), an AST/tree-sitter repo-map upgrade via the ADR 0063 `extractFile()` seam (polyglot + symbol resolution + a dependency graph that also feeds the ADR 0094 atlas truer edges), and forward-facing "why" extraction (`# WHY:`/ADR rationale into recall + auto-injected on file open); from claude-mem (1–5) — progressive-disclosure recall with token-cost visibility (~10× fewer tokens/query, mechanism-agnostic), a hook graceful-degradation policy (infra-fail→exit 0, code-bug→exit 2), a memory-usefulness feedback loop, a `<private>` content-exclusion marker, and windowed content-hash dedup. Deliberately does NOT decide grep vs. a semantic/embedding layer — a companion owner-authored ADR owns that, and every item here composes with whichever it picks. Declines lifting either codebase, the Chroma/uv dependency, and automatic-capture-as-default (folded into the BFG as suggestion-only).](<redacted>.md) | GDS / memory + retrieval |
170
- | 0095-b | [Cross-agent context management — adopt docstrings now (meaning written into version-controlled source is the zero-cost, zero-drift core of BOTH grep and semantic findability — no separate index to pay for or keep fresh); record as DEFERRED future-decision content the `AGENTS.md` cross-tool adapter layer (one canonical instructions file + thin per-tool pointers), the greppability / Grep-Test naming conventions, the `AIDEV-NOTE` anchor-comment convention (guarded by a fitness check so it can't rot), a semantic / embeddings layer (local model + embedded store + incremental git-hook reindex, run alongside grep — never instead), and a per-project "retrieval & agent profile" chosen at init that provisions the adapter set. Preserves the cross-agent research (agents navigate by grep not embeddings; Anthropic + Sourcegraph dropped embedding indexes, Cursor only augments grep with them). The portability frame is the ADR 0062 / 0081 core↔host swappable-adapter pattern applied to the agent surface; corroborates ADR 0060's deferred-embeddings gate. One accepted decision; everything else is framing for a future GDS-V4 / Cloud Bongos planning session. Follow-on task 1654 tracks the docstring rollout.](<redacted>.md) | Cloud Bongos / context + retrieval |
171
- | 0096 | [Require a `credits_reward` (example) value before a task is workable — closes the hole where `credits_reward NOT NULL DEFAULT 0` (migration 003) let a task be promoted→claimed→shipped while paying 0 example. Enforces a present, positive reward at THREE points: the promote route (`POST /tasks/:id/promote` refuses an unrewarded flip to `ready` with an actionable `REWARD_REQUIRED` 409 that SUGGESTS a value from kind/est), the auto-promote predicate (`task_is_fully_unblocked`, migration 177 — adds `(reward_gate_exempt OR credits_reward>0)` so a deps-shipped task with no reward stays `backlog`), and a claim backstop (`db.claimTask` mirrors the `DEPS_NOT_SHIPPED` second-line-of-defence). GRANDFATHERS the ~57 already-`ready` zero-reward tasks via a `tasks.reward_gate_exempt` flag set true for every task at/past `ready` at migration time — no currently-claimable task becomes unclaimable, no `ready` task is retro-demoted (the predicate change is forward-only: triggers never re-evaluate a `ready` row). Keeps `credits_reward` AUTHOR-SET (ADR 0023 anti-gaming): suggests, never auto-applies; setting it stays `PATCH /tasks/:id`. Coordinates with the backfill task (<redacted>, task 1672) to value the grandfathered rows.](<redacted>.md) | GDS / economy + lifecycle |
172
- | 0097-a | [One active claim per session + a claim binds to a dedicated worktree (task 1642) — closes the commingled-ship hole: a single Claude Code session works one git worktree which owns one branch, so claiming a 2nd task from the same worktree lands both tasks' commits on one branch and `/builder-ship` (which publishes the whole branch) drags the unfinished task's work out under the first's ship. The only prior guard (`gateNewcomerClaim`) was Xenos-only, builder-scoped, and fail-OPEN. Adds a SESSION-scoped (keyed on `creator_session_id`, not builder — preserves the many-sessions-per-builder norm), ALL-rank, FAIL-CLOSED block at `POST /claims` (`409 session_already_has_active_claim` naming the in-flight task). NULL-session web claims (`bind_session=false`) are skipped so claim-in-browser/ship-from-any-terminal still works. The decision is a pure exported fn (`shouldRefuseSessionClaim`, DB-free unit-tested); the route middleware is thin. DB defence-in-depth: partial unique index `uniq_active_claim_per_session ON claims(creator_session_id) WHERE released_at IS NULL AND creator_session_id IS NOT NULL` (migration 180 — the real `released_at` convention, NOT the spec's nonexistent `status` column; heals any current violation before CREATE so deploy can't break). Part B: the builder-claim skill default flow now claims into a dedicated worktree (`EnterWorktree task-N`). AMENDS ADR 0049 — adds the within-session invariant 0049 left implicit; the cross-claim split contract is untouched.](<redacted>.md) | GDS / lifecycle |
173
- | 0097-b | [Retroactive reward backfill for shipped 0-reward tasks](<redacted>.md) | economy / rewards |
174
- | 0098 | [The Cloud Bongos public-mirror publish boundary as an executable manifest (`scripts/gds/publish-manifest.js`, `isPublishable()`) — makes ADR 0065 §4's prose boundary something R72 (the delayed redacted export) consumes and R73 (the no-leak proof) asserts against. Default-deny: an allowlist (portable core + platform + neutral branding + sanitized methodology docs) gated by a hard denylist that IMPORTS and partitions the three trust inventories — `PROTECTED_GLOBS` is NOT a denylist, only its topology subset (infra/deploy/CI/local-gate-hooks) is excluded; the rank/permission/ship machinery publishes AS the open-source methodology. Excludes the game + all art (the art subtree carries the ripped Pokémon FireRed reference corpus — an independent copyright reason) + instance identity/config/history + secrets. A unit test cross-checks the partition against the live `PROTECTED_GLOBS` so the carve-out can't silently drift (the gate-review "no allow-by-omission" doctrine applied to publishing). Defers the customer-0 brand pack + desktop-client source to R72/owner (excluded by default until then). No migration — a file-glob boundary has no DB state.](<redacted>.md) | Cloud Bongos / open-source governance |
175
- | 0099 | [The time-delayed, redacted public-mirror export pipeline (R72, task 1211) — the file-level work ADR 0098 deferred: `scripts/public-mirror-export.js` picks the newest commit ≥180 days old (a lagged SNAPSHOT, not a history replay), partitions its tree through `isPublishable()` (ADR 0098 — the ONLY allow-list), redacts every published file (`scripts/gds/mirror-redact.js`: domain/subdomains → example.com, repo/db/world/company token → example, owner login+aliases → example-owner, public IPv4 **and** IPv6 → REDACTED_IP with localhost/RFC-1918/RFC-5737/link-local/ULA kept) THEN the ADR-0022 secret scrubber (extracted to `modules/security/secret-scrub.js` so it's CI-safe to require without ship.js's heavy deps; ship.js re-exports it). FAIL-CLOSED + atomic: all redaction in memory, a no-leak gate over every blob, and only a fully-clean set touches the mirror FS. Two invariants carry it: (1) SELF-LEAK — the redactor is itself published so it holds NO instance literal, deriving them at runtime from the excluded `config/branding.json`; (2) the no-leak DETECTOR is STRICTER THAN + DECOUPLED FROM redaction — `findLeaks` scans raw literal SUBSTRINGS + IPv4/v6 floors, not the bounded redaction regexes, because a shared word-bounded rule silently missed `example-owner` inside a regex-literal string AND blinded the gate to its own miss. Droplet IP + owner-name aliases are sourced from env in the EXCLUDED workflow yaml. Transport reuses the ADR-0029/[#741](https://example.com/builders#/task/741) scoped-deploy-key push; scheduled weekly; DORMANT no-op until history is 180d old (~2026-11), keeping the mirror empty until R73 lands + the owner flips it public. `.mirror-provenance.json` is the lineage anchor R73 ([#1212](https://example.com/builders#/task/1212)) reuses.](<redacted>.md) | Cloud Bongos / open-source governance |
176
- | 0100 | [Two-repo instance model — a project consumes Cloud Bongos as a versioned platform dependency (resolves the R77 decision gate, task [#1607](https://example.com/builders#/task/1607)). Chooses the PHYSICAL repo topology [ADR 0062](<redacted>.md) §10 left open: an instance may be its OWN repo installing a pinned core (the `isPublishable()` subtree, [ADR 0098](<redacted>.md)) + layering its own content/branding/modules — not only a second process on one checkout (cloudbongos.com stays a supported, cheaper reskin shape). SEQUENCED + Mercury-driven: build the smallest slice to stand Mercury up as customer 2 on a PINNED core, then let its first core bump define the update channel (`bongos upgrade` is a stub today, [#1203](https://example.com/builders#/task/1203)) — don't design the channel up front; Example is NOT split (stays the single-checkout reference instance, free core updates) until a separate later call. GDS topology = PER-REPO (each project its own DB/tasks/builders/versions; a shared GDS is rejected — it re-couples what the split exists to separate). The generated mirror ([ADR 0065](<redacted>.md) §4 / [ADR 0099](<redacted>.md)) is RETAINED now, retired CONDITIONALLY once the core is physically extracted (then the core repo becomes the canonical public artifact); AGPL + governance + 6-month delay unaffected by topology. Trades free instant updates for a permanent per-project upgrade step + a bigger op footprint, weighed against the $5k budget. Seeds GDS-V4 R84→R85→R86.](<redacted>.md) | Cloud Bongos / architecture |
177
- | 0101 | [Goal hierarchy Phase 5 — the deferred ADR 0086 non-decisions, recorded with a security review (BV1.R65, task 1519): (1) an ADVISORY-FIRST relevance / anti-hack judge at goal-task create/claim that reuses the ADR 0024 grader subagent harness, flags for a Metic to review, and NEVER hard-blocks a newcomer (the deterministic scope + protected-path + auto-rank-floor gates stay the only hard walls; a fallible LLM as a hard gate would re-introduce the bottleneck ADR 0086 removed + open a gaming/DoS surface); (2) sub-Metic membership inside PROTECTED-scope goals — a genuine widening of the ADR 0043 hard floor, made purely-additive + fail-closed: an Archon grants a tightly-scoped, audit-logged membership and the ship-time gates (pre-push / grader pre-pass / gate-review / main-audit) become GOAL-AWARE on the REAL diff (allow a sub-Metic protected-path hit only if a live Archon-granted goal membership enumerates exactly that path, else the 0043 floor bites unchanged); ships LAST behind Part 1 + an owner Gate sign-off (ADR 0058) + a red-team pass; (3) the goal-reward model — leaning KARMA (not example: real money, tight budget) + CONTRIBUTION-WEIGHTED (not membership-flat: anti-squatting; reuse the shipped-task attribution + BFG signals, ADR 0027) on goal ACHIEVEMENT, with any monetary component + the exact curve owner-gated at build time. Design + security review only; NO enforcement code. Child cluster P5.1 (judge) / P5.2 (goal-aware gates + 0043 amendment) / P5.3 (reward) filed backlog.](<redacted>.md) | GDS / permissions + economy |
178
- | 0102 | [Finish bounding the kernel — flip fitness Check 12 ("kernel imports no domain") from a WARN-mode ratchet to a HARD gate by clearing all 7 tracked kernel→domain leaks (task [#1707](https://example.com/builders#/task/1707)). (1) `llm-cache.js` + `llm-pricing.js` reclassified as kernel-adjacent infra (pure/injected cost primitives, same category as the pg pool + `path-match.js` — added to `KERNEL_IMPORT_ALLOWLIST`, NOT a new module core would depend on anyway); (2) the identity/auth/admission cluster (`upsertBuilder`/`seatFirstAdminIfUnclaimed`/`getBuilderByGithubId`/`hasInvitedAccessRequest`/`createSession`/`lookupSession`/`SESSION_TTL_MS`/`RANKS`) carved from the `db.js` residue into `db-kernel.js` — `auth.js` (a KERNEL_FILE) resolves them there, `db.js` re-exports for its other callers, the `RANKS` twin now pairs with lifecycle; (3) the doorway's remaining domain re-exports pushed behind ports — Gemini-key storage → art-pipeline, `builderRollingAvgGrade` → the `grade` port; (4) `<redacted>` flipped to `violations` + `hardFail` at zero. Lands incrementally, each verified vs `fitness.js` + the unit suite + a live vanilla `docker compose up` (auth still 401s clean). Supersedes the ADR 0091 §1/§3 + 0093 WARN-mode deferral of the llm-infra carve.](<redacted>.md) | GDS / architecture |
179
- | 0103-a | [Consolidate the two parallel internal versions — GDS-V4 (frozen) folds into BONGOS-V1, now the single go-forward Cloud Bongos PLATFORM version (renamed "Cloud Bongos — platform (V1)", status `building`); its still-relevant open work carries forward. The Bongos DESKTOP APP becomes its own GOAL under BONGOS-V1 (mascot, branded download + auto-update, the cloudbongos.com landing page, feedback capture, the absorbed Dev Box switch — [ADR 0087](<redacted>.md)); the code carve `bongos-app/` → `modules/bongos-app/` is tracked under it. Prunes 19 superseded tasks (the C1 semantic-search/embeddings arm — retired by [ADR 0095](<redacted>.md)'s grep-over-embeddings decision; old V3-era grader tasks superseded by [ADR 0089](<redacted>.md); the T-NNN task-ref migration), moves 2 misfiled game-art tasks to product V2, and severs the 758→753 dependency edge so 758 stays claimable. Executed as reversible data migration `182` (only status/version/goal/edge mutations; in-flight `confirmed`/`active` tasks stay in GDS-V4 to land). `/status` now defaults to BONGOS-V1. Surfaced + fixed a 12-day silent DB-backup outage first (task [#1711](https://example.com/builders#/task/1711)).](<redacted>.md) | Cloud Bongos / methodology |
180
- | 0103-b | [Core-first extraction: Cloud Bongos is the trunk, the instance splits off](<redacted>.md) | core / extraction |
181
- | 0104 | [Trust the GDS API channel in Claude Code AUTO mode — pre-allow `Bash(node scripts/gds/api.js:*)` in the project `.claude/settings.json`. Completes task [#1051](https://example.com/builders#/task/1051)'s narrow named-script allow-list (from the task-[#1027](https://example.com/builders#/task/1027) nightmare): every autonomous GDS op without a dedicated script — idea-triage promote, create-task, blocker-resolve, criterion ratify, goal create, cost, security reports — rides the GENERIC `api.js` caller, which was never allow-listed, so auto mode's classifier hard-denied the whole autonomous GDS write surface as "[External System Writes] … beyond the user's request" (it only worked under the box's task-[#1251](https://example.com/builders#/task/1251) `bypassPermissions`). Safe because the GDS SERVER enforces `builders.rank` per-request ([ADR 0016](<redacted>.md)) — the classifier was never the gate; the rule grants nothing beyond the caller's rank, and `api.js` can only reach `/api/gds/*`. Scope-discipline stays in the per-builder `wandering` knob. Locked by `tests/box_ship_permissions.mjs`.](<redacted>.md) | harness permissions / dev box |
182
- | 0105 | [Instance/product seed data out of the core migration path — OTB scope-truth (`versions`/`goals`/`done_when_criteria`) embedded in ~16 core migrations crash-looped a vanilla self-host on a fresh DB (FK violations) or polluted it with OTB's versions/goals; task [#1264](https://example.com/builders#/task/1264) patched only the two crashers (172/173 `DO`-block guards). Structural fix (task [#1704](https://example.com/builders#/task/1704)): the 10 PURE seed files move to a declared `migrations/instance/` dir; `scripts/migrate.sh` applies instance seeds only for the OWNING instance and DEFAULTS OFF (opt-in `GDS_APPLY_INSTANCE_SEEDS=1`; hard-off under the now-revived `MEDUSA_GDS_ONLY`), so a vanilla OR branded self-host never inherits them. Both dirs merge-sort by basename so the global numeric order is preserved (018 before 019) and an OTB from-scratch replay is byte-identical (5 versions / 11 goals / 43 criteria). The unavoidable seed blocks in the mixed files 003/006 guard in place on the `app.seed_instance_data` GUC (achievements + the generic `bfg`/`discord_bot` system principals kept). A `selfhost-boot.yml` CI gate (authored; landing gated on blocker 52 — the App can't merge `.github/workflows/*`) boots a fresh `postgres:16` and asserts a clean vanilla boot with zero OTB rows, closing the C9-style un-gated-proof gap. Verified against a throwaway DB both ways.](<redacted>.md) | Cloud Bongos / architecture |
183
- | 0106 | [Goal owner/manager authority — refines ADR 0086's membership roles into a fixed hierarchy (task [#1735](https://example.com/builders#/task/1735)): the goal OWNER is `goals.created_by` (a column, not a role row — set at create, moved only by the new owner-only `POST /goals/:id/transfer`; the new owner must be Metic+); MANAGER is the `goal_members.role='lead'` value (rendered "manager" in the hall), owner-equivalent EXCEPT demotion (owner-only) and transfer; the owner's row is never demotable (`owner_role_fixed`), enforced in the pure ordered gate (`authorizeMemberRoleChange`, the authorizeGoalTaskCreate precedent) AND atomically inside the UPDATE (`setGoalMemberRole`). Creators backfill as leads via module migration `<redacted>` — deliberately NO earliest-member fallback (fail-closed; ownerless migration-160 system goals stay Archon-run). No goal delete — archive unchanged. Replaces the task's original leads-as-equals "never below one lead" invariant with the simpler fixed-owner model.](<redacted>.md) | GDS / goal hierarchy |
184
- | 0107 | [Module upstreaming — instances contribute modules into the shared core catalog (task [#1766](https://example.com/builders#/task/1766)). The REVERSE of the ADR 0100/0103 distribution arrow: an instance (Example, Mercury) builds a feature module and submits it for approval to be merged INTO the shared Cloud Bongos core, credited to author + origin project; accepted modules land in core `modules/<key>/` **default-off**, added to the `isPublishable()` allowlist, and every instance may opt in. Decisions: (1) platform-native `bongos module submit` → a rank-gated core review queue (NOT instance write-access to core; fork-and-PR is a fallback); (2) attribution is PORTABLE METADATA — `module.json` gains `author`/`origin`/`license`/`maintainer` (a record, not a cross-GDS foreign key, since each repo has its own identities per [ADR 0100](<redacted>.md) §3); (3) reward = CREDIT NOW, money later (example/cross-instance payout deferred to a separate economics call); (4) AGPL-IN with a DCO-style sign-off, NO CLA ([ADR 0065](<redacted>.md)), `isPublishable()` pre-check at submit rejects un-AGPL-able content (game/art/copyright); (5) SECURITY REVIEW is a hard accept-gate — a contributed module runs in-process in every enabling instance, no sandbox yet; (6) default-off + the publish-allowlist add IS the accept action; (7) maintenance/deprecation policy for orphaned modules (coreVersion compat). Sequenced: manifest fields + module-owned test discovery in core CI + submit-time pre-check buildable NOW; the submit→review→accept→catalog pipeline depends on R88 (extract core, [#1727](https://example.com/builders#/task/1727)) + R85 (Mercury, [#1689](https://example.com/builders#/task/1689)). Renames away from the taken "contribution seam" term (that is the in-process UI hook).](<redacted>.md) | Cloud Bongos / architecture |
185
- | 0108 | [Instance composition topology — CONFIGURABLE-ROOT: the core is consumed as a versioned npm dependency (task [#1870](https://example.com/builders#/task/1870), BV1.R115). Resolves the composition MECHANISM that [ADR 0062](<redacted>.md) §10 / [ADR 0100](<redacted>.md) §1 / [ADR 0103](<redacted>.md) §2 each left open (they are mechanism-neutral — "installs a pinned core + layers on top"; 0103's R88 record deferred "the topology/instance paths the scaffold populates on `init`"). A 2026-07-03 owner review chose configurable-root over the earlier clone-and-layer draft, weighting SCALABILITY over least-resistance: upgrades = O(1)-per-instance version bump (not O(instances×drift) merges); the boundary is enforced by the PACKAGE (can't rot under many agents) not convention+CI; a dependency forces a real core API; instance repos stay small; "root is a parameter" is the precondition for multi-instance/hosted. Decisions: (1) core installed as `@bongos/core`, host content resolved via an injected instance-root — two resolvers `resolveInstanceRoot()`/`resolveCoreRoot()` in the existing `src/instance-config.js`, defaulting to repo-root TODAY (byte-identical) and flipping `resolveCoreRoot()` to `node_modules` when packaged, no further per-site edits; the two directions are OPPOSITE (config→instance-root; the `infra/` installers `serve-internal` serves→core-root); (2) `package-core` emits a real `package/`-rooted npm package (`main`→platform-server, `bin`→bongos, deps pruned to express/pg/undici), `isPublishable()` stays the single core definition; (3) `.claude/` is MATERIALIZED into the instance by `bongos init`/`upgrade` (Claude Code reads it from the project root, never node_modules) + the ~30 skills/hooks rewritten `node scripts/gds/…`→`bongos …` (skills already ship in-core; hooks+settings are already "topology"); (4) `bongos upgrade` (R86) is an npm version bump, deploy flips `git reset --hard`→`npm ci` (Docker/`platform-server`/`selfhost-boot` already model it); (5) cross-repo migration numbering gets a namespacing + lineage discipline (two repos both adding `NNN_*.sql` collide on the shared `schema_migrations` stem key); (6) land Mercury-first — step-1 resolvers now (zero-change), package+`.claude` prove on greenfield R85, Example splits last (R89) with the trust-boundary path sites + the two-root guardrail fix (fitness.js/gen-repo-map/gen-file-map all hardcode REPO_ROOT) behind a security review. Runtime-library half is SMALL/de-risked (~8–11 readers; entrypoint seam `mountInternalSurfaces`/`buildPlatformApp`, two-roots loader, single-sourced partition already exist); the real work is delivery+ops (§3/§4/§5) — the accepted "more pain now". Clone-and-layer/submodule/monorepo rejected. Informs R85 ([#1689](https://example.com/builders#/task/1689)), R86 ([#1690](https://example.com/builders#/task/1690)), R89 ([#1728](https://example.com/builders#/task/1728)). Docs amended (not superseded): file-map, architecture, packaging-the-core + self-host recipes, ADR 0105, modules-contract; cross-refs in 0062/0100/0103/0098/branding-contract/CLAUDE.md. No migration, no new criterion; goal 26.](<redacted>.md) | Cloud Bongos / architecture |
186
- | 0109 | [The GDS API is self-describing — a standards-compliant **OpenAPI 3.1** spec generated from the live route files + a hosted `/docs` site (task [#1918](https://example.com/builders#/task/1918), BONGOS-V1, Part A of the API-docs + `/api/gds`→`/api/bongos` rename). Replaces the hand-maintained, drifted `docs/routes-permissions.md` with `scripts/gds/gen-api-docs.js`, which reuses the SAME `route-rank-check.js` introspection the ship-time rank gate ([ADR 0016](<redacted>.md)) uses — so the docs enumerate exactly the routes the server mounts and cannot drift (the [ADR 0063](<redacted>.md) generate-and-`--check` pattern). Emits `docs/api/openapi.json` (each op carries its rank in `x-rank` + a `security` ref, path params, and best-effort request-body fields from the `validateOrRespond` schema; undocumented bodies are counted) + a generated `docs/api-reference.md`. A vendored Redoc bundle renders it at `/docs` on every instance's apex (`cloudbongos.com/docs` canonical), served from the SHARED `mountInternalSurfaces` so a self-hosted instance is self-documenting for free; `GET /api/gds` returns a machine-discovery index and the 404 hint names `/docs`. Introduces the single `src/bongos/api-prefix.js` prefix constant that Part B flips to `/api/bongos`. Freshness enforced by `gen-api-docs --check` in `fitness.js` + ship-time regen; full standard lint via `npm run docs:api:lint` (`@redocly/cli`, off the critical path). Rejected: annotation-based/hand-written specs (drift), CDN renderer (self-host convention/air-gap), redocly as a hard CI dep (lean project).](<redacted>.md) | GDS / API docs |
187
- | 0110 | [Move whole-file generated navigation docs off the git graph (task [#1926](https://example.com/builders#/task/1926), BONGOS-V1, goal 24 build-pipeline-integrity). `docs/repo-map.md` (ADR 0063) + `docs/session-log-index.md` (ADR 0062 §8) were regenerated + **committed on every ship**, so parallel builder branches collided on them constantly (the [ADR 0082](<redacted>.md) reactive heal only cleans up after the fact). Applies the blessed `gen-atlas.js`/`atlas.json` precedent ([ADR 0094](<redacted>.md)): **gitignore** both whole files + regenerate them SERVER-SIDE at deploy (`deploy-prod.sh`, LOUD on failure → goal-24 `gcarry-pipe-loud`) and at `box-source-fetch` — a file not in git can never appear in a branch diff, so the conflict class is gone AT THE SOURCE. Both generators are DUAL-DUTY (each also refreshes a tracked block — the nested `CLAUDE.md` symbol blocks / the `CLAUDE.md` §13 snippet), so the CI freshness gate is SPLIT: `gen-*.js --check-block` (tracked block, still HARD) vs `--check-whole` (the gitignored file, no longer gated) — the adversarial-review fatal flaw was that the old single `--check` bundled both, so a naive relax would silently drop the block gate. `ship.js` drops the gitignored paths from its `git add` (an explicit `git add <ignored>` aborts the commit). Reactive heal (`git-merge-regen`, `.gitattributes`, ADR 0082 resolver) KEPT unchanged as a transition backstop; trust boundary untouched (no bot commits to main; no `.github/workflows/` change). Deferred to follow-ups: the OpenAPI spec (external consumers → bounded regen bot) + diagram renders (headless Chromium). Accepts: whole files fresh only post-deploy, not at merge.](<redacted>.md) | Build pipeline / generated artifacts |
188
- | 0111 | [Instance hosting + domains as a Cloud Bongos module — spec of the new **`provisioning`** module boundary + the **self-host vs managed** split (task [#1485](https://example.com/builders#/task/1485), BONGOS-V1 goal 26; consolidates owner ideas [#345](https://example.com/builders#/idea/345) + [#303](https://example.com/builders#/idea/303)). Standing up a Cloud Bongos instance still leaves an irreducibly-manual infra checklist (`bongos init` files it as kickoff tasks); this ADR specs the module that automates it. **Distinct from [ADR 0092](<redacted>.md)'s `hosting`** (that brokers compute/storage *workloads*; this provisions an *instance's own* server/DB/domain/DNS/TLS) — takes the free key `provisioning` to avoid the collision. Decisions: (1) a portable `default:false` `provisioning` module owns only the web-tier request/state surface (`provisioning_instances`/`_intents`/`_events`, own+Archon rank-gated routes, a `provisioning.instanceState` port) — it holds **no** cloud tokens; (2) the web tier only **enqueues intents**, a new control-plane CLI `scripts/gds/provision.js` (sibling of `box.js`, sole token-holder, systemd-timer-drained, idempotent/dry-run-default) executes them — the [ADR 0031](<redacted>.md)/[ADR 0016](<redacted>.md) box precedent, with the DO+Cloudflare clients extracted to a shared control-plane location so `box.js`+`provision.js` reuse them without a module→module import; (3) provisioning composes the [`cloudbongos-standup.md`](../recipes/cloudbongos-standup.md) runbook (server co-tenant-or-droplet → DB → migrate → env → service → DNS → TLS → Caddy), with GitHub-OAuth-app + first-sign-in demarcated as guided/manual; (4) **bring-your-own-domain first** (registrar-API purchase deferred) + **Caddy on-demand ACME TLS** for new hosts (removes the manual Cloudflare Origin-Cert leg; existing apexes keep theirs); (5) **self-host and managed run the identical `@bongos/core` artifact** ([ADR 0108](<redacted>.md)) — the only differences are *whose* credentials the control plane holds and *who* operates it; managed = self-host + <redacted> infra + a central fleet control plane driving the same `bongos upgrade` channel, AGPL unchanged ([ADR 0065](<redacted>.md) §2, no open-core); (6) cheapest-first cost DNA (co-tenant $0 → dedicated droplet only when justified; snapshot-park idle; billing deferred); (7) self-host arm buildable now on the shipped configurable-root substrate, managed arm gated on R85 Mercury ([#1689](https://example.com/builders#/task/1689)) + R86 upgrade channel ([#1690](https://example.com/builders#/task/1690)). Seeds 8 follow-up tasks via `scripts/gds/seed-provisioning-tasks.js`.](<redacted>.md) | Cloud Bongos / architecture |
189
- | 0112 | [Private goals — a `visibility` flag (`public` default \| `private`) + a two-sided `goal_membership_requests` queue (task [#1951](https://example.com/builders#/task/1951), BONGOS-V1). A goal manager can make a goal **private**; on a private goal non-members can't self-join — they are **invited** (a consented, pending row the invitee accepts from their inbox — distinct from the immediate `POST /goals/:id/members` add, kept byte-identical) or they **request** to join, approved by the goal's owner/manager/Archon (the [ADR 0106](<redacted>.md) `authorizeGoalManagement` wall reused wholesale). Both directions are one `goal_membership_requests` table keyed by `kind ∈ {invite, request}`, `status ∈ {pending, accepted, rejected, cancelled}`, with a **partial-unique `(goal_id, builder_id) WHERE status='pending'`** (the `access_requests` precedent — one open ask per person+goal). `GET /goals/inbox` returns each builder's actionable slice — invitations to them + join-requests on goals they own/manage + their own outstanding requests — powering a **home-page inbox**. **Private = join-gated, not hidden** (owner decision): a private goal still shows its title + progress; true concealment would need its own ADR. Purely additive (default `public` = today's open-join, unchanged) + revertible; ships in four phases (schema/db → routes → goals-page UI → home inbox). Rejected: fully-hidden goals (large filter surface for the stated intent), invite-as-force-add (owner asked for accept-from-inbox), two separate tables (shared lifecycle + mutual-exclusion index), requests on public goals (redundant — just join).](<redacted>.md) | GDS / goal hierarchy |
190
- | 0113 | [Per-builder art sandbox (task [#885](https://example.com/builders#/task/885)): descope to the sprite/animation Studio only. Task 885's 8-part breakdown (walkable offline preview, static sandbox server + systemd unit, `cf-tunnel.js` multi-ingress, box-provisioning wiring, reset-to-main script, retire staging) went stale on two axes: (1) Parts A1/B were already shipped, one day after 885 was filed, under [ADR 0044](<redacted>.md) (real degraded-DB game server at `sandbox-<login>.example.com` over the existing box tunnel) + [ADR 0052](<redacted>.md) (game-only entry, every rank) — a better design than the static-file/offline-mode one 885 proposed; (2) the touches\[\] paths point at pre-carve locations (`public/game/...`, `src/bongos/cf-tunnel.js`, `src/bongos/routes/box.js`) that moved to `modules/game/...`/`scripts/gds/cf-tunnel.js`/`modules/dev-box/...` under the ADR 0083/0093 module carve. Part C (retire staging) also contradicts the standing 2026-05-29 defer-not-delete decision in `docs/recipes/staging-environment.md`. Keeps only the Studio — a sprite/animation reviewer at `/studio`, layered onto the *existing* sandbox as a static page under `modules/game/public/` (the `/art` gallery precedent in `src/preview-server.js`; no new server/tunnel/unit) that auto-discovers sprites from `manifest.json` and extracts the right-facing mirror logic from `Player.js` into a shared `spriteFacing.js` — filed as one new child task instead of re-minting the stale 8.](<redacted>.md) | Builder experience / dev box |
191
- | 0114 | [Instance docs-discovery contract — pins how an agent locates its doc surfaces from config alone, with no hard-coded host. Three layers: (1) `config/branding.json` `domains.{publicOrigin,buildersOrigin,statusOrigin}` is the source of truth — the hall is the canonical doc-reading surface, `/docs` on `publicOrigin` renders the API reference, the status page is the off-box outage fallback; (2) `GET <publicOrigin>/api/bongos` (the machine-discovery index, + permanent `/api/gds` alias) is the zero-config entry point and now re-advertises `hall`/`status`/`public` from `branding().domains` so a second instance bootstraps its surfaces from a single unauthenticated call; (3) a resolution order for agents. Formalizes existing keys (no new required field); CLAUDE.md's preamble references it instead of hand-waving one config key.](<redacted>.md) | Portability / docs-discovery |
192
- | 0115 | [Scheduled-routines portability (self-host + multi-builder) — pins the model for the ~17 cron routines under `.claude/scheduled-tasks/`. Contract: every routine is INSTANCE-WIDE (one scheduled run per instance, never one cron per builder — a routine's per-builder effect comes from iterating the builder set in its body). A manifest `config/scheduled-routines.json` declares each routine's scope / default-enabled / cadence / requiresAutonomy: read-only ops default ON, autonomous build routines (overnight-builder, overnight-code-review, idea-triage-nightly) default OFF behind the autonomy flag, BFG Part C stays separately gated. A fresh instance inherits the set + manifest and the operator opts in/out there without editing a routine; builders don't schedule their own (governed by per-builder settings). Spike deliverable; flags the prose `example`/`~/.config/otb` hardcoding as a follow-up sweep. No behavior change today.](<redacted>.md) | Portability / autonomy |
193
- | 0116 | [API error envelope + code taxonomy — the contract (R01) for goal 36's `api-error-envelope` criterion (task [#1988](https://example.com/builders#/task/1988), BONGOS-V1). An audit found **352 distinct `error:'…'` string literals** returned across 400/401/403/404/409/413/422/429/500/502/503 in ~4 different response shapes — a caller cannot write one error-handling path. Decision: **one envelope everywhere** — `{error:{code,message,details?}}` — where the specific `code` slug is **preserved** (a caller keying on `task_not_found` keeps working) and the 352 codes **collapse by category, not by discard**. A kernel registry `src/bongos/api-errors.js` defines ~12 canonical categories (each with a default HTTP status + `retriable` flag) and a pure/total `classify(code)` — an `OVERRIDES` table for the ~90 exceptions (auth codes, preconditions-as-conflicts, `*_too_large`) plus a suffix/prefix heuristic for the long tail (`*_failed`→internal, `*_not_found`→404, `bad_*`/`*_required`→400, `already_*`/`*_taken`→409, `*_unconfigured`→503); an unknown/future code falls back to `internal` (500). Ships the `ApiError` carrier + `envelope()` formatter + `statusForCode(code,override)` (a site keeps its current status; the sweep changes shape, not status). **Contract-only — nothing imports it yet, zero behaviour change.** R02 ([#1989](https://example.com/builders#/task/1989)) wires the middleware + `res.fail()`; R03 ([#1990](https://example.com/builders#/task/1990)) fails the build on a new hand-rolled flat error; R04 ([#1991](https://example.com/builders#/task/1991)) sweeps the ~219 handlers behind that gate. Rejected: flat `{error,message}` (today's mess, no room to grow), numeric codes (opaque), collapsing to 12 codes only (loses signal, breaks callers), per-module registries (defeats "one shape").](<redacted>.md) | API contract / goal 36 |
194
- | 0117 | [API versioning + deprecation policy — the policy (R05) for goal 36's `api-versioning` criterion (task [#1992](https://example.com/builders#/task/1992), BONGOS-V1). The API (212 endpoints at `/api/bongos` + permanent `/api/gds` alias) had no version marker, no breaking-change definition, no deprecation window — any change could silently break a self-hoster/shipped Dev Box binary/status dashboard. Decision: (1) an explicit path segment `/api/bongos/v1/…` is the **contract** version, distinct from `info.version` (the package semver); (2) today's unversioned paths become **permanent aliases pinned to v1** — the load-bearing rule is *an unversioned path never silently jumps major versions*, so when v2 mounts at `/v2/` the alias stays on v1 and breaking changes reach only opted-in callers; (3) breaking = remove/rename/retype a field/endpoint/error-code, new required field, tightened validation, narrowed auth, changed default — non-breaking = additive fields/endpoints/optional-params + new error codes *within an existing ADR-0116 category*; (4) a superseded version lives **≥180 days** with RFC 8594 `Deprecation`/`Sunset` headers, the unversioned alias exempt (permanent); (5) the version is discoverable via OpenAPI `servers` + `x-api-version` + the machine-discovery index + `/docs`. **Policy only** — R06 ([#1993](https://example.com/builders#/task/1993)) mounts `/v1` + aliases the unversioned paths (day-one no-op). Rejected: header-based versioning (invisible in URL, un-curl-able), semver-in-path (too fine), reusing the package semver (conflates build with contract), sunsetting the unversioned alias (breaks un-updatable binaries).](<redacted>.md) | API contract / goal 36 |
195
- | 0118 | [API contract source-of-truth: code-first, schema-as-data — the spike (R07) for goal 36 (task [#1994](https://example.com/builders#/task/1994), BONGOS-V1). Decision: STAY code-first; the per-route `validate()` schema object (already structured data: type/required/enum/length caps in `_helpers.js`) is the SINGLE source — the OpenAPI spec, runtime validation, and the typed client are all DERIVED from it, none hand-authored. `gen-api-docs.js` already generates the spec from route source (ADR 0109) but shallowly (212 endpoints; 76 write ops with a requestBody, 24 without; `components.schemas` empty); enrich `extractBody()` to emit full typed schemas from the validate() rules. One schema, three consumers (spec + validation + client) = single-source WITHIN code-first, no drift. Refines R08 (enrich generator → generate+publish client), R09 (migrate callers — also repairs the R04 flat-error reads), R10 (external on-ramp), R11 (reject unknown fields + query params, same schema), R12 (build check: write route ⇒ spec-bound schema). Rejected: schema-first (rewrite the whole 212-endpoint route layer), hand-maintained spec (drift, per ADR 0109), framework swap to Fastify/tRPC (too invasive).](<redacted>.md) | API contract / goal 36 |
131
+ | 0061 | [Context-layer decomposition: a CLAUDE.md content charter + nested per-module `CLAUDE.md` (load on demand) to fight the AI-authored "context cliff" — keep the always-loaded root lean, push module depth to where it's used](0061-context-layer-decomposition.md) | GDS / methodology + context |
132
+ | 0062 | [Medusa: decouple the GDS into an instance-model build platform — the cut-line (portable core / host instance / gated feature-modules), the branding-contract schema, the feature-module selection mechanism; folds in the CLAUDE.md core/host split + the A→B→C context plan](0062-medusa-instance-model-decoupling.md) | GDS / Medusa decoupling |
133
+ | 0063 | [Generated code symbol skeleton (the repo map): a deterministic, zero-dependency, CI-regenerated altitude map — top exported symbols by reference frequency + per-dir signatures — that feeds the factual half of the nested `CLAUDE.md` docs so they can't drift (Path B; complements the prose recall layer with code)](0063-generated-repo-map-symbol-skeleton.md) | GDS / context + repo map |
134
+ | 0064 | [Rename the platform **Medusa → Cloud Bongos** (owner directive): whole-platform rename, history preserved — canonical config identity + forward docs renamed, historical records (ADR 0062 body, logs, migrations, the R50–R74 task descriptions) kept as-is; amends ADR 0062](0064-rename-medusa-to-cloud-bongos.md) | Cloud Bongos / branding |
135
+ | 0065 | [Cloud Bongos is **AGPL-3.0, non-profit, and AI-first**: AGPL closes the hosted-closed-fork (SaaS) loophole — the only license consistent with the anti-monopoly goal; non-profit + capture-resistant governance (never monetized from contributors); AI-first as a documented norm, not a technical gate; defines the public-mirror boundary (portable core + platform published; host identity/secrets/content + a ~6-month redaction window never). Rejects MIT/Apache (capture), GPL (SaaS loophole), BSL/open-core (not OSS), CLA-relicensing (capture). The C9 keystone — gates R69–R73](0065-agpl-nonprofit-ai-first-governance.md) | Cloud Bongos / open-source governance |
136
+ | 0066 | [Generated file-map sections: generate the `.claude/skills/` + `.claude/scheduled-tasks/` trees of `docs/file-map.md` from disk, with per-entry notes in a sidecar (`docs/file-map.notes.json`); `--check` fails CI on drift or an undescribed skill/routine (Path C), so a new skill can't land un-mapped and the doc-hygiene follow-up task is retired. Leaves the curated `scripts/gds/` highlight list hand-written.](0066-generated-file-map-sections.md) | GDS / context + file map |
137
+ | 0067 | [Fast-forward the checkout to origin/main at session start when safe (clean `main` full clone, strictly behind) — the warn-only→pull-when-safe reversal of the SessionStart freshness step; still warns in every unsafe case (feature-branch worktree, dirty, diverged, partial clone); event-driven freshness, not a faster cron](0067-session-start-fast-forward-pull.md) | GDS / dev boxes + methodology |
138
+ | 0068 | [Shared Gemini art key delivered from the GDS to newcomers](0068-shared-art-key-for-newcomers.md) | GDS / art pipeline |
139
+ | 0069 | [Builders-hall live updates move from a Colyseus WebSocket room to an SSE endpoint (`GET /api/gds/live`) so the hall refreshes live on a game-less Cloud Bongos boot (R55, C8). Supersedes ADR 0030's §2 transport only — the `NOTIFY` + `live-channel.js` signal layer is unchanged; same opaque zero-leak `nudge`, `requireBuilder` gate, per-IP cap kept inline (no `src/rooms/` import). Deletes the room + join-guard; the hall stops loading `/vendor/colyseus.js`. EventSource auto-reconnect replaces the manual rejoin backoff.](0069-hall-live-channel-sse-decouple.md) | GDS / builders hall |
140
+ | 0070 | [Deterministic delivery of in-session cards (hook-delivered, read-only vs mutating)](0070-deterministic-session-card-delivery.md) | GDS / context |
141
+ | 0071 | [Dev-box teardown confirms the DigitalOcean droplet is actually deleted BEFORE marking the GDS row 'destroyed' (idempotent + retried; a failed delete parks at 'error' with droplet_id preserved, never a silent zombie); adds a bidirectional `reconcile-drift` sweep (force-delete zombie droplets no live row claims, mark ghost rows whose droplet vanished) on an hourly timer, with an in-flight young-droplet guard. Fixes the running-but-'destroyed' zombie that froze a box on old code.](0071-box-confirm-before-destroyed-and-drift-reconcile.md) | GDS / dev boxes |
142
+ | 0072-a | [Bongos app ships Mac-signed + notarized first; Windows code signing deferred (BV1.R01, C1). Reuses the Dev Box app's signing identity verbatim — the same Apple Developer ID + the inert-until-armed, `main`-ref-gated (SR-13/SR-14) Mac block — rather than minting a new cert; declines a Windows cert for now, riding the existing Windows-only blocker [#20](https://example.com/builders#/blocker/20) (Azure Trusted Signing vs EV). One cert decision serves both apps once they merge into the umbrella. $0 new recurring cost; Windows users keep the "Run anyway" first-run friction until blocker [#20](https://example.com/builders#/blocker/20) resolves.](0072-bongos-app-mac-signed-first-windows-deferred.md) | Bongos app / packaging |
143
+ | 0072-b | [Dev-box code staleness: make it visible, never auto-reset](0072-dev-box-code-staleness-visibility.md) | GDS / dev boxes |
144
+ | 0073-a | [The secrets scan excludes TruffleHog's generic URI detector. It flagged a `scheme://userinfo@host` SSRF-guard rejection-test fixture (and, fatally, a URL quoted in a fix commit's message — TruffleHog scans commit messages, which `--exclude-paths` cannot reach), reddening every PR off main once a dev-box tunnel host stopped resolving (ADR 0071). Excluding the URI detector on both scan surfaces loses no real coverage — connection-string secrets (postgres://, mysql://, …) keep their dedicated detectors. Lesson: exclude noisy detectors at the detector level, and keep commit messages free of credential-shaped URLs.](0073-secrets-scan-exclude-uri-detector.md) | GDS / CI gates |
145
+ | 0073-b | [A consistent "the system needs an input from you" signal, and pragmatic BYOK for the per-builder Gemini key](0073-builder-needs-signal-and-byok-gemini-key.md) | GDS / builder signals + keys |
146
+ | 0074 | [The memory map stays light (budget guard + auto-tidy + drawer)](0074-memory-map-stays-light.md) | GDS / memory |
147
+ | 0075 | [Per-system cost attribution + the savings counterfactual ledger (System 1 of the LLM-reduction redesign). Corrects LLM pricing to flat Opus 4.8 rates (no 1M premium); splits the single `session` `cost_log` row into call-site-tagged rows (`session:<redacted>, `cron:<routine>:*` via `OTB_COST_CRON_ROUTINE`) each carrying a per-tier USD split (migration 123); adds a `cost_baseline` table (migration 124) where every later system records a `priceUsage()`-computed counterfactual (never client-supplied) so savings are `SUM(baseline−actual)` by system; surfaces `by_source`/`by_tier` (cache_read share) / `savings` as additive blocks on the unchanged `/public/cost-summary`, with the audit folding them in plus a `savings_without_baseline` tripwire. Turns the 82%-cache_read finding from an inference into a queryable column.](0075-per-system-cost-attribution.md) | GDS / cost ledger |
148
+ | 0076 | [In-session deterministic compaction (System 2 — the only lever on the 82% marathon-cache_read pool, est 20-40%). A pure classifier (`.claude/hooks/eviction-policy.js`) finds provably-stale `tool_result` blocks (superseded reads, duplicate reads, old uncited bulk dumps) and replaces each with a one-line recovery stub; never touches user/assistant/reasoning turns, the context-pack, worktree guidance, the last N turns, or the newest read of a file; a backward-reference liveness scan keeps anything a later turn cites. Batched ONLY at the PreCompact boundary (per-turn would invalidate the prompt cache); recovery is retrieval-first via the stub → `/recall` (search.js) / re-Read / tool-rerun loop, with a one-time directive that prevents re-fetch loops. Wired behind `OTB_INSESSION_COMPACT`, OFF by default (wire, don't arm). Spike 1347 measured ~24% realized headroom on real transcripts.](0076-in-session-deterministic-compaction.md) | GDS / context + cost |
149
+ | 0077 | [Content-addressed LLM cache (System 5 — the residual safety net, est 2-5%). Memoizes idempotent model runs at the single shared spawn primitive (`grader.runSubagent`): an identical (normalized-prompt, model, task_type, git-tree-SHA) invocation over UNCHANGED inputs replays a stored result at ZERO new tokens. Validity is bound to the git SHA of the embedded inputs (self-invalidating — a real change always re-runs); the per-task-type normalizer registry strips only provably-volatile lines and is the correctness surface. Stores only on exit_code===0 (never a timeout); the wrap (`runSubagentCached`) is opt-in and behavior-preserving; a hit returns cost_usd:0 and records the saving into `cost_baseline` (ADR 0075). First plug-ins: architect-audit (tree-scoped) + onboarding-analyze (immutable session id). v1 is exact-match; semantic match deferred.](0077-content-addressed-llm-cache.md) | GDS / cost |
150
+ | 0078 | [Deterministic-first cron harness (System 4 — stop wrapping a committed script in an Opus session, est 3-6%). A routine declares `mode: deterministic\|hybrid\|judgment` + `steps:` in SKILL.md frontmatter; `run-routine.js` runs deterministic routines as plain node with NO model launched (hybrid escalates to `claude -p` only on a `needs_judgment` payload). peer-vote-tally + diagram-drift converted (their scripts already self-file); main-audit (trust-boundary core, ADR 0043) + dep-audit-nightly parked (their security scripts don't self-file — converting would drop alerting). A fitness check (`checkDeterministicRoutines`) fails CI if a deterministic body carries judgment/mutation instructions (git push/commit, deploy.sh, Edit/Write, synthesis — negated boundary mentions allowed) or its prose commands diverge from its steps, so the boundary can't rot back into a wrapped session. Cron-line repoint is an operator action.](0078-deterministic-first-cron.md) | GDS / cron + cost |
151
+ | 0079 | [Value-gated autonomous engagement (System 3 — the autonomous-spend lever, est 8-14%; **SHIPPED INERT**). Merges three spawn decisions behind one off-by-default flag (`OTB_AUTONOMY_ENABLED`): a $0 deterministic precheck (`autonomy-gate.js`) that NO-GOs a nightly routine on no-signal nights or a spend-brake (audited in `autonomy_runs`, migration 126); a cascade router (`cascade-dispatch.js`) that drafts a spawn on the cheapest sufficient model and escalates only on an objective signal, never above the builder's ceiling; and a safe_autofix lane that collapses review→triage→rebuild into one fix-and-ship run — permitted ONLY when high-confidence + single-file + not a protected path + the grader is LIVE (never while GRADER_BYPASS is on). BUILT DISABLED per owner directive: decide() returns GO and autofixAllowed() returns false by default, so wiring changes nothing until armed alongside the grader rework. Does NOT touch marathon spend (that's System 2 / ADR 0076).](0079-value-gated-autonomous-engagement.md) | GDS / autonomy + cost |
152
+ | 0080 | [LLM-cache API transport + trust model (System 5 — the transport that finally lets the content-addressed cache of ADR 0077 hit). Its two consumers (`architect-audit.js`, `onboarding-analyze.js`) run as CLI/cron on builder machines where the `pg` pool is unreachable (peer-socket only), so every cache op silently degraded to miss/no-store and the cache never hit organically (eval task 1372: 0 rows, 0 hits despite correct logic). Fix: expose the cache over the GDS API — `POST /api/gds/llm-cache/lookup` + `/store` in `src/bongos/routes/llm-cache.js`, each mapping 1:1 onto the existing `llm-cache.js` DB helper (the route never reimplements SQL; `/store` mounts its own 512 KB body parser since a full subagent stdout exceeds the global 64 KB cap). `llm-cache.js` self-routes: pool first (memoized `poolReachable()` — a `false` is never memoized so a boot blip can't permanently disable the server cache), else an injected `apiCall` fallback (`setApiFallback`), so the always-on server core never statically requires the CLI layer (preserving the core↔host boundary the fitness gate guards). Best-effort end-to-end: any pool/network/auth error degrades to miss/no-store and never throws. Trust model (load-bearing): a store writes a `result_stdout` a later run replays at $0 and trusts, so an open store endpoint is a content-injection vector into the planning pipeline and a metrics-integrity vector into the public savings number — hence rank-gated, reusing the ADR 0016 server-enforced trust boundary.](0080-llm-cache-api-transport-and-trust-model.md) | GDS / LLM cache + cost |
153
+ | 0081 | [Tool-agnostic design layer — the repo (design tokens + component code + exported assets) is the single source of truth; Claude Design, Figma, and any future design tool are SWAPPABLE ADAPTERS against that neutral contract, not hardwired integrations (the ADR 0062 core↔host pattern applied to design — vendor-lock-in insurance the owner asked for). Splits a `ui` discipline out of `artist`/graphics (UI = design-as-code, graded by diff + screenshot; graphics = pixel art, graded by vision). Code-canonical round-trip: structured UI round-trips as code, freeform canvas art is a committed asset; a talented human can still edit directly in Figma and the change round-trips into the repo via a normal GDS task (the escape hatch). One ship pipeline + ONE multimodal grader with a single regen loop, parameterized by type (rubric + evidence chosen by task type) — retires the standalone Gemini vision path and the two-grader-set maintenance trap. Claude Design adapter = code-native /design-sync + Claude Code handoff; Figma adapter = human-edit→code + code→Figma sync. Grader/ship changes ride the gate-review hard floor (ADR 0043). Implementation cluster tasks 1376–1380.](0081-tool-agnostic-design-layer.md) | GDS / design pipeline |
154
+ | 0082-a | [Server-side auto-resolution of generated-file & append-index merge conflicts: the publish-reconciler runs a temp-clone `git merge origin/main` with the `otb-regen` driver + a `docs/adr/README.md merge=union` attribute active, then pushes the resolution so a PR that conflicts ONLY on generated indexes or the ADR append-index self-heals server-side (GitHub's own merge can't run the custom driver) — the PR #300/[#167](https://example.com/builders#/task/167) stall. Pushes a commit (all required checks re-run), aborts to a human on any genuine hand-conflict, best-effort + kill-switchable; zero trust-boundary change (rejects the alternative of lowering deploy access to `thetes`).](0082-server-side-merge-conflict-auto-resolution.md) | GDS / CI + merge |
155
+ | 0082-b | [Productized key-storage provisioning (server-generated master key, zero SSH)](0082-productized-key-storage-provisioning.md) | security / key storage |
156
+ | 0083 | [Practical build plan for the modular architecture (strangler migration) — the decision record that turns the scoped design research (`docs/design/modular-architecture/`, 5 docs) into a concrete, CI-guarded, one-module-at-a-time build. Builds the whole product into `core + modules`: a ~3.5k-LOC kernel + ~16 modules, each a vertical slice reached through ONE published, semver'd doorway (`src/module-api.js` — the only core file a module may import); a loader that discovers/validates/mounts `modules/*/module.json` behind kernel-composed auth and replaces the closed `KNOWN_MODULES` registry in `src/modules.js`; modules talk only through kernel-mediated seams (ports + events), never by importing each other. Boundaries are held by fitness functions (a violation is a red build), not discipline — five new `fitness.js` checks (core-never-imports-a-module, <redacted>, module-route-rank-gate via parameterizing `route-rank-check.js`, core-carries-no-module-code, module-migrations-additive+namespaced). Module-owned migrations generalize the working `migrations/game/` enabled-gated precedent to `modules/<key>/migrations/`. Reorganize, NOT rewrite (the foundation is sound, the filing is wrong); stay single-package/single-process/no-bundler (modularity is a separate axis). Moves ordered by MEASURED isolation: dev-box (the template) → game → discord → art-pipeline; the recurring seam is `routes/me.js` + `settings.js`, cut by a module→core contribution hook. task→module affinity (retire `touches[]`) is the final-tier follow-on. Reconciles the numbering collision: the design docs + epic [#1388](https://example.com/builders#/task/1388) + task [#1389](https://example.com/builders#/task/1389) call this "ADR 0080", but 0080 is the LLM-cache — the module-system record is THIS file. Seeds BONGOS-V1 criterion C5 + the BV1.R## children.](0083-modular-architecture-strangler-build-plan.md) | GDS / architecture |
157
+ | 0084 | [Auto-derived per-task rank floor — every task is rank-scoped and the claim gate finally BITES. `tasks.requires_rank` (V3.R82) + the claim-time gate (`db.claimTask` → `INSUFFICIENT_RANK`) + the `/builder-start` hide were all sound but INERT: no create/edit path ever set `requires_rank`, so every task took the `NOT NULL DEFAULT 'xenos'` and anyone at thetes+ could claim anything. Fix: `deriveRequiredRank(touches, security_sensitive)` derives a floor by reusing the SAME protected-path matcher as the trust boundary (`permission-path-check.matchProtected`, ADR 0043) — the rank/authz/ship-grade/deploy/migration core (the paths a sub-Metic builder already can't push) floors at `metic`, game/art/docs stay `xenos`; never auto-assigns `archon`. `highestRank(floor, requested)` composes it with an Archon override that can only RAISE (a request below the floor is clamped up; `POST`/`PATCH /tasks` accept `requires_rank`, Archon-only; flipping `security_sensitive` ratchets up). Migration 145 backfills every non-terminal task (raise-only, idempotent, SQL generated faithfully from `PROTECTED_GLOBS`). Hall shows a rank pill. One sensitivity source of truth shared with the git-push floor, so claim-floor and push-floor can't drift.](0084-auto-derived-per-task-rank-floor.md) | GDS / authz |
158
+ | 0085 | [Builders are uncapped by default — removes the default $50/mo per-builder spend cap. `classifyCostStatus` no longer falls back to a project default: a `NULL`/`<=0` `monthly_budget_usd` now means UNCAPPED (`level:'ok'`, `budget_mtd_usd:null`, nothing blocks), and the `DEFAULT_MONTHLY_BUDGET_USD=50` constant is deleted. Reverses the default-cap half of GDS-V3 C3 (task 302): the default blocked the very work it was meant to enable — a new builder's own `session:<redacted> API cost crossed $50 within a day and froze /builder-ship + the art pipeline (builder 95 hit $69.32 MTD on day two). KEEPS the Archon override (`PATCH /builders/:id/budget`) as a manual emergency brake — a positive value still enforces warn(≥80%)/over(≥100%). Cost recording + the /public/cost-summary dashboard are unchanged (removes blocking, not observability). No schema change; project-wide guards (autonomy-gate, art HARD_STOP) unaffected.](0085-remove-default-per-builder-spend-cap.md) | GDS / cost ledger |
159
+ | 0086 | [Goal-scoped work hierarchy — assign goals, not tasks. Introduces a `goal` tier and a six-level hierarchy (Project → Version → Goal → Criteria → Task → Sub-task) whose structure is a per-instance config preset (`config/hierarchy.json`, owner-set at onboarding — the ADR 0062 pattern) and whose enforcement is core. A goal is a SHARED, joinable, module-scoped workspace: joining grants bounded authority to author tasks within the goal's `scope_modules` (the wall, set by a trusted rank at planning, never self-granted; widened only by an approved request — Archon for protected modules). Reuses almost everything: criteria re-parent under goals (`done_when_criteria.goal_id`; `/status` rolls up Version→Goals→Criteria→Tasks), sub-tasks stay `parent_task_id`, the per-task rank floor is the auto-derived one (ADR 0084), dependencies become a polymorphic blocked-until-done edge resolved at the claim gate (ADR 0015 — cross-goal allowed, reference-not-grant). Achievement auto-flags when a criterion's tasks ship, then a planner (Metic+) confirms via a new `/goal-review` skill (idea-triage-style), auto-flipping the goal achieved. Deterministic scope + protected-path gate at create; the ship-time grader/gate-review/main-audit stay authoritative on the real diff. Deferred to follow-ons: the reward model, sub-Metic membership in protected-scope goals (amends ADR 0043), and an AI relevance judge. Fills ADR 0083 Rule 5 ("a task is scoped to a module").](0086-goal-scoped-work-hierarchy.md) | GDS / work hierarchy |
160
+ | 0087-a | [Bongos app architecture + the model-agnostic feedback handoff contract (BONGOS-V1 / C4). Records the umbrella desktop app that absorbs the Dev Box app as a module, the capture→synthesize→handoff pipeline (R04–R14), and the Rung-3 *visual* capture floor (pixels + voice + freeze-frame markup over any app) with element-level reads deferred to V2 (Accessibility requested lazily, never up front). The contract: a bundle is a self-contained `~/CloudBongos/feedback/<id>/` dir where every screenshot is referenced by ABSOLUTE FILE PATH, never base64 — so the text prompt stays ~5–11k tokens and any agent opens the frames with its own file read. One shared `latest` pointer (atomic `latest.json` + best-effort symlink, self-healing) feeds three readers that can't drift: Claude Desktop (clipboard + open folder, R12), Claude Code (`/feedback` slash command cats prompt.md + Reads the frame paths, R13), and an optional zero-dependency stdio MCP server (`get_latest_feedback` returns text + file paths, never base64, R14). Hand-rolled MCP (newline-delimited JSON-RPC, no SDK dep) keeps the install surface flat. Bongos succeeds the GDS as the primary builder surface after GDS-V4; a fourth agent is an adapter, not a redesign. $0 new recurring cost.](0087-bongos-app-architecture-and-handoff-contract.md) | Bongos app / feedback loop |
161
+ | 0087-b | [Compete with Cursor on governance, not tooling](0087-compete-on-governance-not-tooling-cursor.md) | strategy / positioning |
162
+ | 0088 | [Consistent-character generative animation — a generate→grade→audit→regenerate loop built by EXTENDING the existing family pipeline (ADR 0008: `run_family` + cross-frame coherence + the `character_identity` Gemini review + `t5_anchor_refine`), not a new grader/loop (the owner's "don't reinvent the loop"). Decision (with the owner, 2026-06-26): option A — Gemini + grade/audit with a PLUGGABLE generator backend (the ADR 0062/0081 adapter pattern) — over a per-character LoRA, which is deferred as "level-2" (needs GPU training + an open model). The family pipeline is already a consistent-character multi-frame loop (walk/peck cycles ship through it) but tuned for 32×32 pixel-art; this adds a painterly `character_anim` family kind (drops palette-snap/32×32), the two genuinely-new criteria `pose_match` + order-aware transition-coherence (existing checks are order-agnostic — they'd pass a shuffled animation), a recursive Gemini inbetween layer for high frame counts (≈12–24 "on twos"/"on ones"), a painterly cutout/register post-process, and packaging as a Cloud Bongos module (ADR 0083). First consumer = the Bongo Buddha's 8 actions → widget (task 1523). Implementation epic: tasks 1549–1559.](0088-consistent-character-generative-animation.md) | GDS / art pipeline |
163
+ | 0089 | [Modular, multi-model adversarial grader (open-weight ensemble) — refactor the ship-gate into a central, work-type-parameterized engine (gather evidence → deterministic pre-passes → judge ensemble → normalize+aggregate → gate → persist) where each MODULE contributes only its rubric + evidence-gatherer + specialist critics through the ADR 0083 seam (`grader.workType.<key>`), answering the owner's "per-module or general?" as central-engine + module-plugged. Adds open-weight LLM judges from a DIFFERENT model family than the author (Claude stays as the quality floor, never the sole judge) to kill same-family self-preference inflation (10–25%); judges run via a `ModelProvider` adapter (`openai-compatible` → DeepInfra/OpenRouter/vLLM/Ollama, the ADR 0062 neutral-contract pattern) on a hosted pay-per-call API (~$1–5/mo, owner spend-gated). Small (2–3) audited panel — z-score/rank-normalize then median + hard-veto aggregation; an error-correlation audit routine (Cohen's κ vs owner decisions) keeps the jury failing DIFFERENTLY (the "Nine Judges, Two Effective Votes" finding) rather than assuming vendor-diversity. Deterministic route-rank + permission-path pre-passes stay ZERO-LLM and authoritative (no LLM verdict, Claude or open, can upgrade a fail over a protected path; ADR 0016/0043/0080); VLM judges advisory on art, deterministic palette/grid checks authoritative. Owner decisions 2026-06-26: central-engine + module-plugged; ensemble vote; hosted API. Reuses the worker-contract + task_grades schema + LLM cache (ADR 0077); rolls out advisory-first behind a flag. Build cluster task-pending (task 1561).](0089-modular-multi-model-adversarial-grader.md) | GDS / grader + quality gate |
164
+ | 0090 | [Metic gains task authoring — re-ranks `POST /tasks`, `PATCH /tasks/:id`, `POST /tasks/:id/promote`, and the `tasks/:id/{dependencies,criteria}` CRUD from Archon-only to `requireRank('metic','archon')` (owner decision 2026-06-26: a rank trusted to triage ideas is trusted to seed + shape the tasks they become). Moves the two `EXPECTED_RANKS` pins in lockstep and updates the `// rank:` annotations so the route-rank audit stays green. Keeps Archon-only: rank-setting, version-close, abandon, grader-bypass confirm, and the ADR 0086 cross-tier `POST /dependencies` + goal-scope edges. Amends ADR 0018's table row 7; sits beside ADR 0086, which remains the module-fenced authoring path for sub-Metic ranks. "Tighten later" debt explicitly accepted — no per-version/module/goal fence on a Metic's authoring.](0090-metic-task-authoring.md) | GDS / permissions |
165
+ | 0091 | [Bounding the kernel and carving `db.js` per-domain (Config-B core decomposition) — amends ADR 0083 to extend the strangler from the four FEATURE modules into the CORE itself (BONGOS-V1 C7). Answers the three questions the audit opened: (1) WHAT IS THE KERNEL — replaces the 6-file machinery-only `fitness.js` `KERNEL_FILES` with an enumerated ~3.5k-LOC trust roster (auth · route-rank-check · permission-path-check · secret-box · pool · the new `db-kernel.js` identity/audit/rank/`withTx` slice · instance-config/branding/build-info · routes/_helpers · the module-system machinery + module-scope-map); everything else in `src/bongos/` is a domain destined to become a module or to stay inside the un-split lifecycle state machine. (2) HOW A DOMAIN OWNS ITS SQL — ratifies the proven `modules/discord/db.js` + `modules/game/db.js` pattern (own queries lifted verbatim + deleted from core, reached through the doorway `pool`/`withTx`; kernel helpers re-exported behind the same `db.*` namespace; boundary-forced twins allowed + documented) as THE per-domain carve mechanism; `db.js` shrinks toward a kernel slice + the lifecycle residue. (3) WHAT THE DOORWAY EXPOSES — classifies `module-api.js` re-exports into kernel (stays: identity/audit/rank/auth/seams/config + `withTx`), domain-leak-with-a-home (relocate now: `setBuilderBoxBlocked`→dev-box, gemini-key trio→art-pipeline), and domain-leak-pending-carve (`createTask`/`captureIdea`/`VOTABLE_TASK_STATUSES` stay as MARKED temporary re-exports until lifecycle/ideas are carved in tranche 2 — named, owned, tracked debt, not pretend-kernel). A lifecycle-invoked capability resolves through a kernel PORT, never a direct import (R73 replaces `routes/tasks.js`→`grader.js` with `resolve('grade')`). Deliverable = this decision + the seeded, dependency-wired R68–R74 sub-task shape; OTB byte-identical at every step. Tranche 2 (the other ~8 core domains) deferred per owner.](0091-bounding-the-kernel-and-db-carve.md) | GDS / architecture |
166
+ | 0092 | [Hosting module + on-prem (warehouse) hosting target — scopes a portable Cloud Bongos `hosting` module (default-OFF, fail-to-vanilla) that brokers compute + storage workloads (inference / imagegen / embeddings / training / storage) to configured hosting *targets* through kernel seams, resolving each via `resolveOptional(port, cloudDefaultFn)` so a box-less / module-off instance degrades to cloud automatically. The warehouse GPU box is OTB's first ON-PREM target — one adapter backend declared in HOST-instance config (the ADR 0062 / 0081 core↔host + swappable-adapter pattern), shared across all Example instances, reached over an outbound tunnel the box initiates (no open ports / no static IP), sitting OUTSIDE the trust boundary like Discord (ADR 0033): it computes, it never grants rank or lands code (ADR 0016 / 0043). Inference REUSES the ADR 0089 `ModelProvider` openai-compatible adapter (the on-prem box = one more vLLM / Ollama endpoint) and adopts 0089's decisive finding that low-volume work is cheaper on pay-per-call APIs — so the GPU's honest justification narrows to GPU-bound batch generation, open-model fine-tuning / LoRA (the ADR 0088 deferred level-2), and always-busy bulk, plus the non-GPU wins of backups / storage + an unmetered sandbox; code that ships stays on Claude. Owner decisions 2026-06-27: serve all instances, conservative posture, proceed. Phased: scope use ✅ → build (task 1619) → hardware (task 1620) → software; the cost read-out (task 1621) is an independent input.](0092-hosting-module-and-on-prem-target.md) | Cloud Bongos / hosting |
167
+ | 0093 | [Tranche-2 core-carve sequence (Config-B, completing C7) — amends ADR 0091, which deferred tranche 2 "per owner"; owner greenlit 2026-06-27. Tranche 1 proved the carve pattern (memory R72, grading R73) AND shipped the reusable infra (bounded kernel + `KERNEL_FILES` ratchet, doorway/`withTx`, per-domain `db.js` slice + `db-kernel.js`, the `grade` kernel port, `coreB-carve-proof.sh`), so each remaining core domain is now a PURE carve. Fixes (1) the ORDER — clean-slicing-first per the 02-module-map import-graph: economy → ideas → security → builder-settings → onboarding → sessions/BFG → autonomy/MAS → status-UI → hall-UI → lifecycle LAST; (2) economy registers the `reward` kernel PORT (twin of `grade`) so the lifecycle resolves grade+reward via seams and imports only the kernel — which is WHY economy is first and the lifecycle state machine is carved last + whole (never split, 02 §4); (3) every carve is proof-gated byte-identical and C7 is satisfied only by the final all-domains proof (R87) — tranche 1 proved the pattern, tranche 2 completes the decomposition the done-when demands; (4) the ADR-0091 §3 temporary doorway leaks retire as their domains carve (`captureIdea`→ideas R78, `createTask`/`VOTABLE_TASK_STATUSES`→lifecycle R86). Deliverable = this decision + the seeded, dependency-wired R76–R87 sub-task shape.](0093-tranche-2-core-carve-sequence.md) | GDS / architecture |
168
+ | 0094 | [Repo Atlas — a self-drawing map of the codebase (idea [#457](https://example.com/builders#/idea/457)): a Builders' Hall page rendering an isometric, hover-connective, click-for-detail map in two views — CONTEXT LAYER (the always-loaded root `CLAUDE.md` + its budget gauge vs the on-demand nested guides, the docs library, the DB as live state, broken-link drift) and MODULE TRUTH (the real modules carved into `modules/<key>/` with clean boundaries vs the ~26k-line `src/bongos` core blob + the `db.js` monolith vs the planned-but-still-fused ghosts). Data = one generated `public-builders/atlas.json` from `scripts/gds/gen-atlas.js`, PURE file-reads, reusing the existing truth sources (`module-scope-map` MOVED-vs-FUSED, `fitness.js` boundary scan for clean-vs-violation + kernel leaks, `docs-entropy` stale links, the `CLAUDE.md` budget). Regenerated SERVER-SIDE on every deploy (a `deploy-prod.sh` step, the world-snapshot precedent) — never on the shipping builder's machine; gitignored build artifact; page degrades gracefully pre-generation; styled in the Cloud Bongos brand, gated to any signed-in builder, built to port to Cloud Bongos. Owner decision 2026-06-27. Follow-up task [#1641](https://example.com/builders#/task/1641) migrates the repo-map/session-index/file-map generators onto the same server-side trigger (diagrams excepted: headless browser + auth).](0094-repo-atlas-self-drawing-codebase-map.md) | Cloud Bongos / docs |
169
+ | 0095-a | [Borrowed memory, retrieval & collision concepts from claude-mem (Apache-2.0) and graphify (MIT) — after evaluating both Claude-Code tools and rejecting either wholesale (Python stack / local-daemon+Chroma architecture / no rank-scoping / open-core+memecoin governance), adopt eight stack-agnostic CONCEPTS, reimplemented in-stack with no new dependency or service ("borrow concepts, not tools"): from graphify — symbol-level collision detection (path-prefix→symbol/dependency overlap in `path-match.js`, the forecast-accuracy lever ADR 0049 left open), an AST/tree-sitter repo-map upgrade via the ADR 0063 `extractFile()` seam (polyglot + symbol resolution + a dependency graph that also feeds the ADR 0094 atlas truer edges), and forward-facing "why" extraction (`# WHY:`/ADR rationale into recall + auto-injected on file open); from claude-mem (1–5) — progressive-disclosure recall with token-cost visibility (~10× fewer tokens/query, mechanism-agnostic), a hook graceful-degradation policy (infra-fail→exit 0, code-bug→exit 2), a memory-usefulness feedback loop, a `<private>` content-exclusion marker, and windowed content-hash dedup. Deliberately does NOT decide grep vs. a semantic/embedding layer — a companion owner-authored ADR owns that, and every item here composes with whichever it picks. Declines lifting either codebase, the Chroma/uv dependency, and automatic-capture-as-default (folded into the BFG as suggestion-only).](0095-borrowed-memory-and-retrieval-concepts.md) | GDS / memory + retrieval |
170
+ | 0095-b | [Cross-agent context management — adopt docstrings now (meaning written into version-controlled source is the zero-cost, zero-drift core of BOTH grep and semantic findability — no separate index to pay for or keep fresh); record as DEFERRED future-decision content the `AGENTS.md` cross-tool adapter layer (one canonical instructions file + thin per-tool pointers), the greppability / Grep-Test naming conventions, the `AIDEV-NOTE` anchor-comment convention (guarded by a fitness check so it can't rot), a semantic / embeddings layer (local model + embedded store + incremental git-hook reindex, run alongside grep — never instead), and a per-project "retrieval & agent profile" chosen at init that provisions the adapter set. Preserves the cross-agent research (agents navigate by grep not embeddings; Anthropic + Sourcegraph dropped embedding indexes, Cursor only augments grep with them). The portability frame is the ADR 0062 / 0081 core↔host swappable-adapter pattern applied to the agent surface; corroborates ADR 0060's deferred-embeddings gate. One accepted decision; everything else is framing for a future GDS-V4 / Cloud Bongos planning session. Follow-on task 1654 tracks the docstring rollout.](0095-cross-agent-context-management.md) | Cloud Bongos / context + retrieval |
171
+ | 0096 | [Require a `credits_reward` (example) value before a task is workable — closes the hole where `credits_reward NOT NULL DEFAULT 0` (migration 003) let a task be promoted→claimed→shipped while paying 0 example. Enforces a present, positive reward at THREE points: the promote route (`POST /tasks/:id/promote` refuses an unrewarded flip to `ready` with an actionable `REWARD_REQUIRED` 409 that SUGGESTS a value from kind/est), the auto-promote predicate (`task_is_fully_unblocked`, migration 177 — adds `(reward_gate_exempt OR credits_reward>0)` so a deps-shipped task with no reward stays `backlog`), and a claim backstop (`db.claimTask` mirrors the `DEPS_NOT_SHIPPED` second-line-of-defence). GRANDFATHERS the ~57 already-`ready` zero-reward tasks via a `tasks.reward_gate_exempt` flag set true for every task at/past `ready` at migration time — no currently-claimable task becomes unclaimable, no `ready` task is retro-demoted (the predicate change is forward-only: triggers never re-evaluate a `ready` row). Keeps `credits_reward` AUTHOR-SET (ADR 0023 anti-gaming): suggests, never auto-applies; setting it stays `PATCH /tasks/:id`. Coordinates with the backfill task (<redacted>, task 1672) to value the grandfathered rows.](0096-require-reward-before-workable.md) | GDS / economy + lifecycle |
172
+ | 0097-a | [One active claim per session + a claim binds to a dedicated worktree (task 1642) — closes the commingled-ship hole: a single Claude Code session works one git worktree which owns one branch, so claiming a 2nd task from the same worktree lands both tasks' commits on one branch and `/builder-ship` (which publishes the whole branch) drags the unfinished task's work out under the first's ship. The only prior guard (`gateNewcomerClaim`) was Xenos-only, builder-scoped, and fail-OPEN. Adds a SESSION-scoped (keyed on `creator_session_id`, not builder — preserves the many-sessions-per-builder norm), ALL-rank, FAIL-CLOSED block at `POST /claims` (`409 session_already_has_active_claim` naming the in-flight task). NULL-session web claims (`bind_session=false`) are skipped so claim-in-browser/ship-from-any-terminal still works. The decision is a pure exported fn (`shouldRefuseSessionClaim`, DB-free unit-tested); the route middleware is thin. DB defence-in-depth: partial unique index `uniq_active_claim_per_session ON claims(creator_session_id) WHERE released_at IS NULL AND creator_session_id IS NOT NULL` (migration 180 — the real `released_at` convention, NOT the spec's nonexistent `status` column; heals any current violation before CREATE so deploy can't break). Part B: the builder-claim skill default flow now claims into a dedicated worktree (`EnterWorktree task-N`). AMENDS ADR 0049 — adds the within-session invariant 0049 left implicit; the cross-claim split contract is untouched.](0097-one-active-claim-per-session-and-worktree-binding.md) | GDS / lifecycle |
173
+ | 0097-b | [Retroactive reward backfill for shipped 0-reward tasks](0097-retroactive-reward-backfill.md) | economy / rewards |
174
+ | 0098 | [The Cloud Bongos public-mirror publish boundary as an executable manifest (`scripts/gds/publish-manifest.js`, `isPublishable()`) — makes ADR 0065 §4's prose boundary something R72 (the delayed redacted export) consumes and R73 (the no-leak proof) asserts against. Default-deny: an allowlist (portable core + platform + neutral branding + sanitized methodology docs) gated by a hard denylist that IMPORTS and partitions the three trust inventories — `PROTECTED_GLOBS` is NOT a denylist, only its topology subset (infra/deploy/CI/local-gate-hooks) is excluded; the rank/permission/ship machinery publishes AS the open-source methodology. Excludes the game + all art (the art subtree carries the ripped Pokémon FireRed reference corpus — an independent copyright reason) + instance identity/config/history + secrets. A unit test cross-checks the partition against the live `PROTECTED_GLOBS` so the carve-out can't silently drift (the gate-review "no allow-by-omission" doctrine applied to publishing). Defers the customer-0 brand pack + desktop-client source to R72/owner (excluded by default until then). No migration — a file-glob boundary has no DB state.](0098-public-mirror-publish-manifest.md) | Cloud Bongos / open-source governance |
175
+ | 0099 | [The time-delayed, redacted public-mirror export pipeline (R72, task 1211) — the file-level work ADR 0098 deferred: `scripts/public-mirror-export.js` picks the newest commit ≥180 days old (a lagged SNAPSHOT, not a history replay), partitions its tree through `isPublishable()` (ADR 0098 — the ONLY allow-list), redacts every published file (`scripts/gds/mirror-redact.js`: domain/subdomains → example.com, repo/db/world/company token → example, owner login+aliases → example-owner, public IPv4 **and** IPv6 → REDACTED_IP with localhost/RFC-1918/RFC-5737/link-local/ULA kept) THEN the ADR-0022 secret scrubber (extracted to `modules/security/secret-scrub.js` so it's CI-safe to require without ship.js's heavy deps; ship.js re-exports it). FAIL-CLOSED + atomic: all redaction in memory, a no-leak gate over every blob, and only a fully-clean set touches the mirror FS. Two invariants carry it: (1) SELF-LEAK — the redactor is itself published so it holds NO instance literal, deriving them at runtime from the excluded `config/branding.json`; (2) the no-leak DETECTOR is STRICTER THAN + DECOUPLED FROM redaction — `findLeaks` scans raw literal SUBSTRINGS + IPv4/v6 floors, not the bounded redaction regexes, because a shared word-bounded rule silently missed `example-owner` inside a regex-literal string AND blinded the gate to its own miss. Droplet IP + owner-name aliases are sourced from env in the EXCLUDED workflow yaml. Transport reuses the ADR-0029/[#741](https://example.com/builders#/task/741) scoped-deploy-key push; scheduled weekly; DORMANT no-op until history is 180d old (~2026-11), keeping the mirror empty until R73 lands + the owner flips it public. `.mirror-provenance.json` is the lineage anchor R73 ([#1212](https://example.com/builders#/task/1212)) reuses.](0099-delayed-redacted-mirror-export.md) | Cloud Bongos / open-source governance |
176
+ | 0100 | [Two-repo instance model — a project consumes Cloud Bongos as a versioned platform dependency (resolves the R77 decision gate, task [#1607](https://example.com/builders#/task/1607)). Chooses the PHYSICAL repo topology [ADR 0062](0062-medusa-instance-model-decoupling.md) §10 left open: an instance may be its OWN repo installing a pinned core (the `isPublishable()` subtree, [ADR 0098](0098-public-mirror-publish-manifest.md)) + layering its own content/branding/modules — not only a second process on one checkout (cloudbongos.com stays a supported, cheaper reskin shape). SEQUENCED + Mercury-driven: build the smallest slice to stand Mercury up as customer 2 on a PINNED core, then let its first core bump define the update channel (`bongos upgrade` is a stub today, [#1203](https://example.com/builders#/task/1203)) — don't design the channel up front; Example is NOT split (stays the single-checkout reference instance, free core updates) until a separate later call. GDS topology = PER-REPO (each project its own DB/tasks/builders/versions; a shared GDS is rejected — it re-couples what the split exists to separate). The generated mirror ([ADR 0065](0065-agpl-nonprofit-ai-first-governance.md) §4 / [ADR 0099](0099-delayed-redacted-mirror-export.md)) is RETAINED now, retired CONDITIONALLY once the core is physically extracted (then the core repo becomes the canonical public artifact); AGPL + governance + 6-month delay unaffected by topology. Trades free instant updates for a permanent per-project upgrade step + a bigger op footprint, weighed against the $5k budget. Seeds GDS-V4 R84→R85→R86.](0100-two-repo-instance-model.md) | Cloud Bongos / architecture |
177
+ | 0101 | [Goal hierarchy Phase 5 — the deferred ADR 0086 non-decisions, recorded with a security review (BV1.R65, task 1519): (1) an ADVISORY-FIRST relevance / anti-hack judge at goal-task create/claim that reuses the ADR 0024 grader subagent harness, flags for a Metic to review, and NEVER hard-blocks a newcomer (the deterministic scope + protected-path + auto-rank-floor gates stay the only hard walls; a fallible LLM as a hard gate would re-introduce the bottleneck ADR 0086 removed + open a gaming/DoS surface); (2) sub-Metic membership inside PROTECTED-scope goals — a genuine widening of the ADR 0043 hard floor, made purely-additive + fail-closed: an Archon grants a tightly-scoped, audit-logged membership and the ship-time gates (pre-push / grader pre-pass / gate-review / main-audit) become GOAL-AWARE on the REAL diff (allow a sub-Metic protected-path hit only if a live Archon-granted goal membership enumerates exactly that path, else the 0043 floor bites unchanged); ships LAST behind Part 1 + an owner Gate sign-off (ADR 0058) + a red-team pass; (3) the goal-reward model — leaning KARMA (not example: real money, tight budget) + CONTRIBUTION-WEIGHTED (not membership-flat: anti-squatting; reuse the shipped-task attribution + BFG signals, ADR 0027) on goal ACHIEVEMENT, with any monetary component + the exact curve owner-gated at build time. Design + security review only; NO enforcement code. Child cluster P5.1 (judge) / P5.2 (goal-aware gates + 0043 amendment) / P5.3 (reward) filed backlog.](0101-goal-hierarchy-phase-5-judge-membership-reward.md) | GDS / permissions + economy |
178
+ | 0102 | [Finish bounding the kernel — flip fitness Check 12 ("kernel imports no domain") from a WARN-mode ratchet to a HARD gate by clearing all 7 tracked kernel→domain leaks (task [#1707](https://example.com/builders#/task/1707)). (1) `llm-cache.js` + `llm-pricing.js` reclassified as kernel-adjacent infra (pure/injected cost primitives, same category as the pg pool + `path-match.js` — added to `KERNEL_IMPORT_ALLOWLIST`, NOT a new module core would depend on anyway); (2) the identity/auth/admission cluster (`upsertBuilder`/`seatFirstAdminIfUnclaimed`/`getBuilderByGithubId`/`hasInvitedAccessRequest`/`createSession`/`lookupSession`/`SESSION_TTL_MS`/`RANKS`) carved from the `db.js` residue into `db-kernel.js` — `auth.js` (a KERNEL_FILE) resolves them there, `db.js` re-exports for its other callers, the `RANKS` twin now pairs with lifecycle; (3) the doorway's remaining domain re-exports pushed behind ports — Gemini-key storage → art-pipeline, `builderRollingAvgGrade` → the `grade` port; (4) `<redacted>` flipped to `violations` + `hardFail` at zero. Lands incrementally, each verified vs `fitness.js` + the unit suite + a live vanilla `docker compose up` (auth still 401s clean). Supersedes the ADR 0091 §1/§3 + 0093 WARN-mode deferral of the llm-infra carve.](0102-finish-bounding-the-kernel.md) | GDS / architecture |
179
+ | 0103-a | [Consolidate the two parallel internal versions — GDS-V4 (frozen) folds into BONGOS-V1, now the single go-forward Cloud Bongos PLATFORM version (renamed "Cloud Bongos — platform (V1)", status `building`); its still-relevant open work carries forward. The Bongos DESKTOP APP becomes its own GOAL under BONGOS-V1 (mascot, branded download + auto-update, the cloudbongos.com landing page, feedback capture, the absorbed Dev Box switch — [ADR 0087](0087-bongos-app-architecture-and-handoff-contract.md)); the code carve `bongos-app/` → `modules/bongos-app/` is tracked under it. Prunes 19 superseded tasks (the C1 semantic-search/embeddings arm — retired by [ADR 0095](0095-cross-agent-context-management.md)'s grep-over-embeddings decision; old V3-era grader tasks superseded by [ADR 0089](0089-modular-multi-model-adversarial-grader.md); the T-NNN task-ref migration), moves 2 misfiled game-art tasks to product V2, and severs the 758→753 dependency edge so 758 stays claimable. Executed as reversible data migration `182` (only status/version/goal/edge mutations; in-flight `confirmed`/`active` tasks stay in GDS-V4 to land). `/status` now defaults to BONGOS-V1. Surfaced + fixed a 12-day silent DB-backup outage first (task [#1711](https://example.com/builders#/task/1711)).](0103-gdsv4-bongos-consolidation.md) | Cloud Bongos / methodology |
180
+ | 0103-b | [Core-first extraction: Cloud Bongos is the trunk, the instance splits off](0103-core-first-extraction-cloud-bongos-trunk.md) | core / extraction |
181
+ | 0104 | [Trust the GDS API channel in Claude Code AUTO mode — pre-allow `Bash(node scripts/gds/api.js:*)` in the project `.claude/settings.json`. Completes task [#1051](https://example.com/builders#/task/1051)'s narrow named-script allow-list (from the task-[#1027](https://example.com/builders#/task/1027) nightmare): every autonomous GDS op without a dedicated script — idea-triage promote, create-task, blocker-resolve, criterion ratify, goal create, cost, security reports — rides the GENERIC `api.js` caller, which was never allow-listed, so auto mode's classifier hard-denied the whole autonomous GDS write surface as "[External System Writes] … beyond the user's request" (it only worked under the box's task-[#1251](https://example.com/builders#/task/1251) `bypassPermissions`). Safe because the GDS SERVER enforces `builders.rank` per-request ([ADR 0016](0016-trust-boundary-server-enforced-permissions.md)) — the classifier was never the gate; the rule grants nothing beyond the caller's rank, and `api.js` can only reach `/api/gds/*`. Scope-discipline stays in the per-builder `wandering` knob. Locked by `tests/box_ship_permissions.mjs`.](0104-trust-gds-api-channel-in-auto-mode.md) | harness permissions / dev box |
182
+ | 0105 | [Instance/product seed data out of the core migration path — OTB scope-truth (`versions`/`goals`/`done_when_criteria`) embedded in ~16 core migrations crash-looped a vanilla self-host on a fresh DB (FK violations) or polluted it with OTB's versions/goals; task [#1264](https://example.com/builders#/task/1264) patched only the two crashers (172/173 `DO`-block guards). Structural fix (task [#1704](https://example.com/builders#/task/1704)): the 10 PURE seed files move to a declared `migrations/instance/` dir; `scripts/migrate.sh` applies instance seeds only for the OWNING instance and DEFAULTS OFF (opt-in `GDS_APPLY_INSTANCE_SEEDS=1`; hard-off under the now-revived `MEDUSA_GDS_ONLY`), so a vanilla OR branded self-host never inherits them. Both dirs merge-sort by basename so the global numeric order is preserved (018 before 019) and an OTB from-scratch replay is byte-identical (5 versions / 11 goals / 43 criteria). The unavoidable seed blocks in the mixed files 003/006 guard in place on the `app.seed_instance_data` GUC (achievements + the generic `bfg`/`discord_bot` system principals kept). A `selfhost-boot.yml` CI gate (authored; landing gated on blocker 52 — the App can't merge `.github/workflows/*`) boots a fresh `postgres:16` and asserts a clean vanilla boot with zero OTB rows, closing the C9-style un-gated-proof gap. Verified against a throwaway DB both ways.](0105-instance-seed-migrations-out-of-core.md) | Cloud Bongos / architecture |
183
+ | 0106 | [Goal owner/manager authority — refines ADR 0086's membership roles into a fixed hierarchy (task [#1735](https://example.com/builders#/task/1735)): the goal OWNER is `goals.created_by` (a column, not a role row — set at create, moved only by the new owner-only `POST /goals/:id/transfer`; the new owner must be Metic+); MANAGER is the `goal_members.role='lead'` value (rendered "manager" in the hall), owner-equivalent EXCEPT demotion (owner-only) and transfer; the owner's row is never demotable (`owner_role_fixed`), enforced in the pure ordered gate (`authorizeMemberRoleChange`, the authorizeGoalTaskCreate precedent) AND atomically inside the UPDATE (`setGoalMemberRole`). Creators backfill as leads via module migration `<redacted>` — deliberately NO earliest-member fallback (fail-closed; ownerless migration-160 system goals stay Archon-run). No goal delete — archive unchanged. Replaces the task's original leads-as-equals "never below one lead" invariant with the simpler fixed-owner model.](0106-goal-owner-manager-authority.md) | GDS / goal hierarchy |
184
+ | 0107 | [Module upstreaming — instances contribute modules into the shared core catalog (task [#1766](https://example.com/builders#/task/1766)). The REVERSE of the ADR 0100/0103 distribution arrow: an instance (Example, Mercury) builds a feature module and submits it for approval to be merged INTO the shared Cloud Bongos core, credited to author + origin project; accepted modules land in core `modules/<key>/` **default-off**, added to the `isPublishable()` allowlist, and every instance may opt in. Decisions: (1) platform-native `bongos module submit` → a rank-gated core review queue (NOT instance write-access to core; fork-and-PR is a fallback); (2) attribution is PORTABLE METADATA — `module.json` gains `author`/`origin`/`license`/`maintainer` (a record, not a cross-GDS foreign key, since each repo has its own identities per [ADR 0100](0100-two-repo-instance-model.md) §3); (3) reward = CREDIT NOW, money later (example/cross-instance payout deferred to a separate economics call); (4) AGPL-IN with a DCO-style sign-off, NO CLA ([ADR 0065](0065-agpl-nonprofit-ai-first-governance.md)), `isPublishable()` pre-check at submit rejects un-AGPL-able content (game/art/copyright); (5) SECURITY REVIEW is a hard accept-gate — a contributed module runs in-process in every enabling instance, no sandbox yet; (6) default-off + the publish-allowlist add IS the accept action; (7) maintenance/deprecation policy for orphaned modules (coreVersion compat). Sequenced: manifest fields + module-owned test discovery in core CI + submit-time pre-check buildable NOW; the submit→review→accept→catalog pipeline depends on R88 (extract core, [#1727](https://example.com/builders#/task/1727)) + R85 (Mercury, [#1689](https://example.com/builders#/task/1689)). Renames away from the taken "contribution seam" term (that is the in-process UI hook).](0107-module-upstreaming-contribution-to-core-catalog.md) | Cloud Bongos / architecture |
185
+ | 0108 | [Instance composition topology — CONFIGURABLE-ROOT: the core is consumed as a versioned npm dependency (task [#1870](https://example.com/builders#/task/1870), BV1.R115). Resolves the composition MECHANISM that [ADR 0062](0062-medusa-instance-model-decoupling.md) §10 / [ADR 0100](0100-two-repo-instance-model.md) §1 / [ADR 0103](0103-core-first-extraction-cloud-bongos-trunk.md) §2 each left open (they are mechanism-neutral — "installs a pinned core + layers on top"; 0103's R88 record deferred "the topology/instance paths the scaffold populates on `init`"). A 2026-07-03 owner review chose configurable-root over the earlier clone-and-layer draft, weighting SCALABILITY over least-resistance: upgrades = O(1)-per-instance version bump (not O(instances×drift) merges); the boundary is enforced by the PACKAGE (can't rot under many agents) not convention+CI; a dependency forces a real core API; instance repos stay small; "root is a parameter" is the precondition for multi-instance/hosted. Decisions: (1) core installed as `@bongos/core`, host content resolved via an injected instance-root — two resolvers `resolveInstanceRoot()`/`resolveCoreRoot()` in the existing `src/instance-config.js`, defaulting to repo-root TODAY (byte-identical) and flipping `resolveCoreRoot()` to `node_modules` when packaged, no further per-site edits; the two directions are OPPOSITE (config→instance-root; the `infra/` installers `serve-internal` serves→core-root); (2) `package-core` emits a real `package/`-rooted npm package (`main`→platform-server, `bin`→bongos, deps pruned to express/pg/undici), `isPublishable()` stays the single core definition; (3) `.claude/` is MATERIALIZED into the instance by `bongos init`/`upgrade` (Claude Code reads it from the project root, never node_modules) + the ~30 skills/hooks rewritten `node scripts/gds/…`→`bongos …` (skills already ship in-core; hooks+settings are already "topology"); (4) `bongos upgrade` (R86) is an npm version bump, deploy flips `git reset --hard`→`npm ci` (Docker/`platform-server`/`selfhost-boot` already model it); (5) cross-repo migration numbering gets a namespacing + lineage discipline (two repos both adding `NNN_*.sql` collide on the shared `schema_migrations` stem key); (6) land Mercury-first — step-1 resolvers now (zero-change), package+`.claude` prove on greenfield R85, Example splits last (R89) with the trust-boundary path sites + the two-root guardrail fix (fitness.js/gen-repo-map/gen-file-map all hardcode REPO_ROOT) behind a security review. Runtime-library half is SMALL/de-risked (~8–11 readers; entrypoint seam `mountInternalSurfaces`/`buildPlatformApp`, two-roots loader, single-sourced partition already exist); the real work is delivery+ops (§3/§4/§5) — the accepted "more pain now". Clone-and-layer/submodule/monorepo rejected. Informs R85 ([#1689](https://example.com/builders#/task/1689)), R86 ([#1690](https://example.com/builders#/task/1690)), R89 ([#1728](https://example.com/builders#/task/1728)). Docs amended (not superseded): file-map, architecture, packaging-the-core + self-host recipes, ADR 0105, modules-contract; cross-refs in 0062/0100/0103/0098/branding-contract/CLAUDE.md. No migration, no new criterion; goal 26.](0108-instance-composition-configurable-root.md) | Cloud Bongos / architecture |
186
+ | 0109 | [The GDS API is self-describing — a standards-compliant **OpenAPI 3.1** spec generated from the live route files + a hosted `/docs` site (task [#1918](https://example.com/builders#/task/1918), BONGOS-V1, Part A of the API-docs + `/api/gds`→`/api/bongos` rename). Replaces the hand-maintained, drifted `docs/routes-permissions.md` with `scripts/gds/gen-api-docs.js`, which reuses the SAME `route-rank-check.js` introspection the ship-time rank gate ([ADR 0016](0016-trust-boundary-server-enforced-permissions.md)) uses — so the docs enumerate exactly the routes the server mounts and cannot drift (the [ADR 0063](0063-generated-repo-map-symbol-skeleton.md) generate-and-`--check` pattern). Emits `docs/api/openapi.json` (each op carries its rank in `x-rank` + a `security` ref, path params, and best-effort request-body fields from the `validateOrRespond` schema; undocumented bodies are counted) + a generated `docs/api-reference.md`. A vendored Redoc bundle renders it at `/docs` on every instance's apex (`cloudbongos.com/docs` canonical), served from the SHARED `mountInternalSurfaces` so a self-hosted instance is self-documenting for free; `GET /api/gds` returns a machine-discovery index and the 404 hint names `/docs`. Introduces the single `src/bongos/api-prefix.js` prefix constant that Part B flips to `/api/bongos`. Freshness enforced by `gen-api-docs --check` in `fitness.js` + ship-time regen; full standard lint via `npm run docs:api:lint` (`@redocly/cli`, off the critical path). Rejected: annotation-based/hand-written specs (drift), CDN renderer (self-host convention/air-gap), redocly as a hard CI dep (lean project).](0109-self-describing-openapi-and-hosted-docs.md) | GDS / API docs |
187
+ | 0110 | [Move whole-file generated navigation docs off the git graph (task [#1926](https://example.com/builders#/task/1926), BONGOS-V1, goal 24 build-pipeline-integrity). `docs/repo-map.md` (ADR 0063) + `docs/session-log-index.md` (ADR 0062 §8) were regenerated + **committed on every ship**, so parallel builder branches collided on them constantly (the [ADR 0082](0082-server-side-merge-conflict-auto-resolution.md) reactive heal only cleans up after the fact). Applies the blessed `gen-atlas.js`/`atlas.json` precedent ([ADR 0094](0094-repo-atlas-self-drawing-codebase-map.md)): **gitignore** both whole files + regenerate them SERVER-SIDE at deploy (`deploy-prod.sh`, LOUD on failure → goal-24 `gcarry-pipe-loud`) and at `box-source-fetch` — a file not in git can never appear in a branch diff, so the conflict class is gone AT THE SOURCE. Both generators are DUAL-DUTY (each also refreshes a tracked block — the nested `CLAUDE.md` symbol blocks / the `CLAUDE.md` §13 snippet), so the CI freshness gate is SPLIT: `gen-*.js --check-block` (tracked block, still HARD) vs `--check-whole` (the gitignored file, no longer gated) — the adversarial-review fatal flaw was that the old single `--check` bundled both, so a naive relax would silently drop the block gate. `ship.js` drops the gitignored paths from its `git add` (an explicit `git add <ignored>` aborts the commit). Reactive heal (`git-merge-regen`, `.gitattributes`, ADR 0082 resolver) KEPT unchanged as a transition backstop; trust boundary untouched (no bot commits to main; no `.github/workflows/` change). Deferred to follow-ups: the OpenAPI spec (external consumers → bounded regen bot) + diagram renders (headless Chromium). Accepts: whole files fresh only post-deploy, not at merge.](0110-untrack-whole-file-generated-nav-docs.md) | Build pipeline / generated artifacts |
188
+ | 0111 | [Instance hosting + domains as a Cloud Bongos module — spec of the new **`provisioning`** module boundary + the **self-host vs managed** split (task [#1485](https://example.com/builders#/task/1485), BONGOS-V1 goal 26; consolidates owner ideas [#345](https://example.com/builders#/idea/345) + [#303](https://example.com/builders#/idea/303)). Standing up a Cloud Bongos instance still leaves an irreducibly-manual infra checklist (`bongos init` files it as kickoff tasks); this ADR specs the module that automates it. **Distinct from [ADR 0092](0092-hosting-module-and-on-prem-target.md)'s `hosting`** (that brokers compute/storage *workloads*; this provisions an *instance's own* server/DB/domain/DNS/TLS) — takes the free key `provisioning` to avoid the collision. Decisions: (1) a portable `default:false` `provisioning` module owns only the web-tier request/state surface (`provisioning_instances`/`_intents`/`_events`, own+Archon rank-gated routes, a `provisioning.instanceState` port) — it holds **no** cloud tokens; (2) the web tier only **enqueues intents**, a new control-plane CLI `scripts/gds/provision.js` (sibling of `box.js`, sole token-holder, systemd-timer-drained, idempotent/dry-run-default) executes them — the [ADR 0031](0031-cloud-dev-environments-for-builders.md)/[ADR 0016](0016-trust-boundary-server-enforced-permissions.md) box precedent, with the DO+Cloudflare clients extracted to a shared control-plane location so `box.js`+`provision.js` reuse them without a module→module import; (3) provisioning composes the [`cloudbongos-standup.md`](../recipes/cloudbongos-standup.md) runbook (server co-tenant-or-droplet → DB → migrate → env → service → DNS → TLS → Caddy), with GitHub-OAuth-app + first-sign-in demarcated as guided/manual; (4) **bring-your-own-domain first** (registrar-API purchase deferred) + **Caddy on-demand ACME TLS** for new hosts (removes the manual Cloudflare Origin-Cert leg; existing apexes keep theirs); (5) **self-host and managed run the identical `@bongos/core` artifact** ([ADR 0108](0108-instance-composition-configurable-root.md)) — the only differences are *whose* credentials the control plane holds and *who* operates it; managed = self-host + <redacted> infra + a central fleet control plane driving the same `bongos upgrade` channel, AGPL unchanged ([ADR 0065](0065-agpl-nonprofit-ai-first-governance.md) §2, no open-core); (6) cheapest-first cost DNA (co-tenant $0 → dedicated droplet only when justified; snapshot-park idle; billing deferred); (7) self-host arm buildable now on the shipped configurable-root substrate, managed arm gated on R85 Mercury ([#1689](https://example.com/builders#/task/1689)) + R86 upgrade channel ([#1690](https://example.com/builders#/task/1690)). Seeds 8 follow-up tasks via `scripts/gds/seed-provisioning-tasks.js`.](0111-instance-hosting-provisioning-module.md) | Cloud Bongos / architecture |
189
+ | 0112 | [Private goals — a `visibility` flag (`public` default \| `private`) + a two-sided `goal_membership_requests` queue (task [#1951](https://example.com/builders#/task/1951), BONGOS-V1). A goal manager can make a goal **private**; on a private goal non-members can't self-join — they are **invited** (a consented, pending row the invitee accepts from their inbox — distinct from the immediate `POST /goals/:id/members` add, kept byte-identical) or they **request** to join, approved by the goal's owner/manager/Archon (the [ADR 0106](0106-goal-owner-manager-authority.md) `authorizeGoalManagement` wall reused wholesale). Both directions are one `goal_membership_requests` table keyed by `kind ∈ {invite, request}`, `status ∈ {pending, accepted, rejected, cancelled}`, with a **partial-unique `(goal_id, builder_id) WHERE status='pending'`** (the `access_requests` precedent — one open ask per person+goal). `GET /goals/inbox` returns each builder's actionable slice — invitations to them + join-requests on goals they own/manage + their own outstanding requests — powering a **home-page inbox**. **Private = join-gated, not hidden** (owner decision): a private goal still shows its title + progress; true concealment would need its own ADR. Purely additive (default `public` = today's open-join, unchanged) + revertible; ships in four phases (schema/db → routes → goals-page UI → home inbox). Rejected: fully-hidden goals (large filter surface for the stated intent), invite-as-force-add (owner asked for accept-from-inbox), two separate tables (shared lifecycle + mutual-exclusion index), requests on public goals (redundant — just join).](0112-private-goals-invitations-join-requests.md) | GDS / goal hierarchy |
190
+ | 0113 | [Per-builder art sandbox (task [#885](https://example.com/builders#/task/885)): descope to the sprite/animation Studio only. Task 885's 8-part breakdown (walkable offline preview, static sandbox server + systemd unit, `cf-tunnel.js` multi-ingress, box-provisioning wiring, reset-to-main script, retire staging) went stale on two axes: (1) Parts A1/B were already shipped, one day after 885 was filed, under [ADR 0044](0044-per-box-live-game-preview.md) (real degraded-DB game server at `sandbox-<login>.example.com` over the existing box tunnel) + [ADR 0052](0052-sandbox-for-everyone-game-only-preview.md) (game-only entry, every rank) — a better design than the static-file/offline-mode one 885 proposed; (2) the touches\[\] paths point at pre-carve locations (`public/game/...`, `src/bongos/cf-tunnel.js`, `src/bongos/routes/box.js`) that moved to `modules/game/...`/`scripts/gds/cf-tunnel.js`/`modules/dev-box/...` under the ADR 0083/0093 module carve. Part C (retire staging) also contradicts the standing 2026-05-29 defer-not-delete decision in `docs/recipes/staging-environment.md`. Keeps only the Studio — a sprite/animation reviewer at `/studio`, layered onto the *existing* sandbox as a static page under `modules/game/public/` (the `/art` gallery precedent in `src/preview-server.js`; no new server/tunnel/unit) that auto-discovers sprites from `manifest.json` and extracts the right-facing mirror logic from `Player.js` into a shared `spriteFacing.js` — filed as one new child task instead of re-minting the stale 8.](0113-sandbox-studio-descope-superseded-by-0044-0052.md) | Builder experience / dev box |
191
+ | 0114 | [Instance docs-discovery contract — pins how an agent locates its doc surfaces from config alone, with no hard-coded host. Three layers: (1) `config/branding.json` `domains.{publicOrigin,buildersOrigin,statusOrigin}` is the source of truth — the hall is the canonical doc-reading surface, `/docs` on `publicOrigin` renders the API reference, the status page is the off-box outage fallback; (2) `GET <publicOrigin>/api/bongos` (the machine-discovery index, + permanent `/api/gds` alias) is the zero-config entry point and now re-advertises `hall`/`status`/`public` from `branding().domains` so a second instance bootstraps its surfaces from a single unauthenticated call; (3) a resolution order for agents. Formalizes existing keys (no new required field); CLAUDE.md's preamble references it instead of hand-waving one config key.](0114-instance-docs-discovery-contract.md) | Portability / docs-discovery |
192
+ | 0115 | [Scheduled-routines portability (self-host + multi-builder) — pins the model for the ~17 cron routines under `.claude/scheduled-tasks/`. Contract: every routine is INSTANCE-WIDE (one scheduled run per instance, never one cron per builder — a routine's per-builder effect comes from iterating the builder set in its body). A manifest `config/scheduled-routines.json` declares each routine's scope / default-enabled / cadence / requiresAutonomy: read-only ops default ON, autonomous build routines (overnight-builder, overnight-code-review, idea-triage-nightly) default OFF behind the autonomy flag, BFG Part C stays separately gated. A fresh instance inherits the set + manifest and the operator opts in/out there without editing a routine; builders don't schedule their own (governed by per-builder settings). Spike deliverable; flags the prose `example`/`~/.config/otb` hardcoding as a follow-up sweep. No behavior change today.](0115-scheduled-routines-portability.md) | Portability / autonomy |
193
+ | 0116 | [API error envelope + code taxonomy — the contract (R01) for goal 36's `api-error-envelope` criterion (task [#1988](https://example.com/builders#/task/1988), BONGOS-V1). An audit found **352 distinct `error:'…'` string literals** returned across 400/401/403/404/409/413/422/429/500/502/503 in ~4 different response shapes — a caller cannot write one error-handling path. Decision: **one envelope everywhere** — `{error:{code,message,details?}}` — where the specific `code` slug is **preserved** (a caller keying on `task_not_found` keeps working) and the 352 codes **collapse by category, not by discard**. A kernel registry `src/bongos/api-errors.js` defines ~12 canonical categories (each with a default HTTP status + `retriable` flag) and a pure/total `classify(code)` — an `OVERRIDES` table for the ~90 exceptions (auth codes, preconditions-as-conflicts, `*_too_large`) plus a suffix/prefix heuristic for the long tail (`*_failed`→internal, `*_not_found`→404, `bad_*`/`*_required`→400, `already_*`/`*_taken`→409, `*_unconfigured`→503); an unknown/future code falls back to `internal` (500). Ships the `ApiError` carrier + `envelope()` formatter + `statusForCode(code,override)` (a site keeps its current status; the sweep changes shape, not status). **Contract-only — nothing imports it yet, zero behaviour change.** R02 ([#1989](https://example.com/builders#/task/1989)) wires the middleware + `res.fail()`; R03 ([#1990](https://example.com/builders#/task/1990)) fails the build on a new hand-rolled flat error; R04 ([#1991](https://example.com/builders#/task/1991)) sweeps the ~219 handlers behind that gate. Rejected: flat `{error,message}` (today's mess, no room to grow), numeric codes (opaque), collapsing to 12 codes only (loses signal, breaks callers), per-module registries (defeats "one shape").](0116-api-error-envelope-and-code-taxonomy.md) | API contract / goal 36 |
194
+ | 0117 | [API versioning + deprecation policy — the policy (R05) for goal 36's `api-versioning` criterion (task [#1992](https://example.com/builders#/task/1992), BONGOS-V1). The API (212 endpoints at `/api/bongos` + permanent `/api/gds` alias) had no version marker, no breaking-change definition, no deprecation window — any change could silently break a self-hoster/shipped Dev Box binary/status dashboard. Decision: (1) an explicit path segment `/api/bongos/v1/…` is the **contract** version, distinct from `info.version` (the package semver); (2) today's unversioned paths become **permanent aliases pinned to v1** — the load-bearing rule is *an unversioned path never silently jumps major versions*, so when v2 mounts at `/v2/` the alias stays on v1 and breaking changes reach only opted-in callers; (3) breaking = remove/rename/retype a field/endpoint/error-code, new required field, tightened validation, narrowed auth, changed default — non-breaking = additive fields/endpoints/optional-params + new error codes *within an existing ADR-0116 category*; (4) a superseded version lives **≥180 days** with RFC 8594 `Deprecation`/`Sunset` headers, the unversioned alias exempt (permanent); (5) the version is discoverable via OpenAPI `servers` + `x-api-version` + the machine-discovery index + `/docs`. **Policy only** — R06 ([#1993](https://example.com/builders#/task/1993)) mounts `/v1` + aliases the unversioned paths (day-one no-op). Rejected: header-based versioning (invisible in URL, un-curl-able), semver-in-path (too fine), reusing the package semver (conflates build with contract), sunsetting the unversioned alias (breaks un-updatable binaries).](0117-api-versioning-and-deprecation-policy.md) | API contract / goal 36 |
195
+ | 0118 | [API contract source-of-truth: code-first, schema-as-data — the spike (R07) for goal 36 (task [#1994](https://example.com/builders#/task/1994), BONGOS-V1). Decision: STAY code-first; the per-route `validate()` schema object (already structured data: type/required/enum/length caps in `_helpers.js`) is the SINGLE source — the OpenAPI spec, runtime validation, and the typed client are all DERIVED from it, none hand-authored. `gen-api-docs.js` already generates the spec from route source (ADR 0109) but shallowly (212 endpoints; 76 write ops with a requestBody, 24 without; `components.schemas` empty); enrich `extractBody()` to emit full typed schemas from the validate() rules. One schema, three consumers (spec + validation + client) = single-source WITHIN code-first, no drift. Refines R08 (enrich generator → generate+publish client), R09 (migrate callers — also repairs the R04 flat-error reads), R10 (external on-ramp), R11 (reject unknown fields + query params, same schema), R12 (build check: write route ⇒ spec-bound schema). Rejected: schema-first (rewrite the whole 212-endpoint route layer), hand-maintained spec (drift, per ADR 0109), framework swap to Fastify/tRPC (too invasive).](0118-api-contract-source-of-truth-code-first.md) | API contract / goal 36 |
196
196
  | 0119 | [API pagination contract — the contract (R13) for goal 36's `api-pagination` criterion (task [#2000](https://example.com/builders#/task/2000), BONGOS-V1). Three styles coexisted (limit-only, limit+offset, none) and some public lists (leaderboard, blockers, versions) returned everything (unbounded = cost/DoS surface). Decision: ONE convention — `?limit=&offset=` through a shared `parsePagination(req,{defaultLimit,maxLimit})` helper in `_helpers.js`: limit default 50 / hard-cap 200 (so no list is unbounded), offset default 0, bad/oversized values CLAMPED not rejected (list reads stay forgiving — the strict counterpart is write-validation R11). Response convention `{ items, page:{ limit, offset, total? } }` via `pageMeta()`. Chose limit/offset over cursor (already the in-use style, no opaque-token/stable-sort infra, trivial to curl; cursor left as a per-endpoint escape hatch for churning lists). Opt-in/non-breaking today; R14 ([#2001](https://example.com/builders#/task/2001)) adopts it across every list + caps the unbounded public lists. Rejected: cursor-as-default (disproportionate), page-number (awkward), leaving lists unbounded (the problem), 400-on-bad-limit (a read should be forgiving).](0119-api-pagination-contract.md) | API contract / goal 36 |
197
- | 0120 | [Pay example on land (not on confirm) + a builder-owned rebase gate (task [#2035](https://example.com/builders#/task/2035), BONGOS-V1). Credits were awarded at the `confirmed` transition — before the code is on `main` — so stranded/never-pushed work paid out with no consequence, and the builder (the only one who can rebase it) had no reason to. Decision: (1) move BOTH the per-task credit AND the cost-plus session reward to the `shipped` (merged-to-`main`) transition; (2) the publish-reconciler flags a confirmed task `needs_rebase` (attributed to the claim holder) once auto-land is exhausted — merge conflict, failing required check vs current `main`, or no pushed branch; (3) a `needs_rebase` task blocks that builder's NEW claims (`REBASE_REQUIRED`) but not their existing/in-flight ones; (4) a one-time migration claws back the `task.confirmed` credits on the current confirmed backlog and re-gates it. Happy path unaffected (a clean PR auto-lands in minutes and pays then); the teeth bite only on stranding. Rejected: gate-only (stranded work still paid), soft-warn (ignorable), grandfather the backlog (leaves today's stranded-and-paid state), per-task-credit-only (owner chose fullest alignment).](<redacted>.md) | economy + lifecycle |
198
- | 0122 | [Onboarding diagram renders: untracked + rendered at deploy from a host-provisioned Chrome (task [#2027](https://example.com/builders#/task/2027), BONGOS-V1) — completes the diagram half [ADR 0110](<redacted>.md) deferred. The `.svg`/`.png` renders were committed on every render-capable ship and kept colliding on parallel branches; ADR 0110 deferred them because `mermaid-cli`/puppeteer downloads its own Chromium on first run (a multi-minute fetch that stalls the 2 GB droplet). Decision: provision Google Chrome once on the host; `render-diagrams.sh` auto-detects an installed browser (or honors `PUPPETEER_EXECUTABLE_PATH`) and sets `PUPPETEER_SKIP_DOWNLOAD` so it never fetches (falls back to the download only on a dev laptop), passing a committed `puppeteer-config.json` with `--no-sandbox` (Ubuntu 24.04 restricts unprivileged user namespaces) and pinning `mermaid-cli@11`; deploy-prod.sh + box-source-fetch render from the tracked `.mmd` (LOUD `::ERROR::`, best-effort); `git rm --cached` + gitignore the `*.svg`/`*.png` (the `.mmd` + `assertions.json` stay tracked, so the nightly diagram-drift cron is unaffected); ship runs `gen-diagrams --no-render`. Verified rendering all 5 diagrams on the prod droplet before landing. Rejected: on-deploy Chromium download (the droplet stall), snap chromium (flaky headless), keep-tracked + strengthen the ADR 0082 resolver (hides the collision).](<redacted>.md) | internal / ADR 0110 |
199
- | 0121 | [Greenfield vs. brownfield onboarding — scope `bongos init --adopt` (task [#1986](https://example.com/builders#/task/1986), BONGOS-V1, goal 35's `adopt-existing-repo` criterion). Today the creation flow is greenfield-only: `scripts/gds/init.js` opens *"stand up a NEW instance"*, refuses to run where `config/branding.json` exists (`--force` overwrites), and seeds a synthetic `"V1 — Prove the loop"`; it inspects nothing about an existing tree. Decision: TWO explicit modes over goal 35's one canonical step sequence (task 1981), branching at exactly the SCAFFOLD leg — greenfield (`bongos init`, today) vs brownfield (`bongos init --adopt`); every other leg (provision, OAuth, verify, brand) identical, so the CLI (1982) + hall wizard (1983) forms inherit adopt for free. `--adopt` must: (1) LAYER not clobber — add the pinned `@bongos/core` dep + lean `config/` + `.claude/` additively, merge-or-report on pre-existing files, never reuse the `--force`-overwrites path; (2) reflect REAL state — capture the repo's actual version/goal instead of the synthetic V1, import the existing backlog (GitHub issues / TODO / roadmap) into GDS tasks; (3) DETECT the existing stack + layout so modules / repo-map / fitness-boundary fit; (4) a CONFLICT PRE-FLIGHT report (migration namespace, fitness boundary, protected-path / rank gates, existing CI), `--dry-run` default, gated behind an accept step. Scope-only — no adopt code ships; seeds a 5-task dependency-wired breakdown. Rejected: overloading `--force` to merge (footgun), a separate `bongos adopt` command (leg duplication / drift), auto-import with no pre-flight, building adopt-mode now (1986 is scope-only).](<redacted>.md) | Cloud Bongos / onboarding |
200
- | 0123 | [Idle-swept dev boxes lose uncommitted work: an on-box autosave-push, not a VM snapshot (task [#837](https://example.com/builders#/task/837) SPIKE). `box.js`'s idle sweep hard-deletes the droplet once a box reads idle — nothing on that path checks git status, and park's snapshot was deliberately removed from the automated path for cost. Decision: a new on-box cron (sibling to `box-heartbeat.sh`) that, when `/workspace` is dirty, commits + pushes to the box's own `claude/<name>` claim branch using the same one-shot-credentialed-then-scrubbed pattern `box-source-fetch.sh` already uses for fetch — bounding loss to the autosave interval (~5 min) instead of unbounded. Open dependency: confirm/mint a push-scoped box credential (today's `BOX_SOURCE_FULL_TOKEN` is fetch-only by convention). Rejected: reintroducing snapshot-before-destroy (the exact cost the team removed), a control-plane git-status check before sweep (needs new box-exec capability, would pin dirty boxes active forever), relying on `/builder-end`'s manual dirty-check alone (doesn't fire on the automatic sweep path at all). Scope-only — no autosave code ships; seeds a follow-up implementation task.](<redacted>.md) | internal / dev-box |
201
- | 0124 | [Typed API response bodies — follow-up to ADR 0118 for goal 36's `api-client-sdk` (task [#2061](https://example.com/builders#/task/2061), from idea 519, BONGOS-V1). Task 2050 typed every request body but success RESPONSES stayed the `ApiResponse` (`= any`) alias — routes declare no response schema. Decision: infer the success shape STATICALLY from each route's inline `res.json({ … })` object literal (reusing the request extractor's `captureBalanced`/`splitTopLevel` machinery; never executes a route) → a `<OperationId>Response` component on the `200` `content`, and the client returns `Promise<ThatShape>`. Only KEYS are provable (values type `unknown` unless trivially safe — a boolean literal, or the ADR-0119 `pageMeta()` `page` block → a shared `PageInfo`). Degrade-gracefully: a bare value (`res.json(row)`), a top-level spread, an empty `{}`, or a computed key keeps `ApiResponse` — we only ever narrow `any`, never mistype. 162/221 endpoints now typed; runtime JS unchanged (only `openapi.json` + `index.d.ts`). Rejected: a per-route `responseSchema` export (duplicates the `res.json` truth + 200-file churn, anti-code-first), executing routes to observe responses (breaks the zero-execution contract), tracing variables to type values (brittle data-flow analysis), `additionalProperties:false` on responses (a false compile error if an unparsed branch adds a key).](<redacted>.md) | API contract / goal 36 |
202
- | 0125 | [The control plane scaffolds + pushes a standalone instance's repo (task [#2055](https://example.com/builders#/task/2055), BONGOS-V1, goal 35's `project-creation-flow`). `provision.js` used to THROW if a standalone instance's repo wasn't already hand-cloned at `/srv/cloudbongos/<slug>` — a manual dead-end between `bongos init` and provision. Decision: the provisioning runner SCAFFOLDS the repo itself (`scaffoldStandaloneRepo`) before the existing code-sync/db/service steps: clone the empty `target_ref` (forcing branch `main` on the unborn HEAD) → build the core tarball (`package-core.js`) → materialize `config/` + `.claude/` + a `file:`-pinned `package.json` + lockfile via `bongos init --vendor-core` (task 2053, `--no-seed`) → commit + push (with a `node_modules`-excluding `.gitignore`; vendored tarball + lockfile stay tracked). Idempotent (skips once `package.json` exists) + dry-run-safe (injected `exec`/`writeFile`). Git transport reuses the existing `github-deploy` SSH alias (no secret in any command) — the one operational grant is that the control plane's deploy key gets WRITE on each instance repo (per-repo, like the platform repo). `deploy-instance.sh` already existed at `scripts/deploy/` (ADR 0108 §4 / task 1884; the task description predated it) — it's the ongoing consumer deploy, not the first-time scaffold; no `infra/` duplicate created. Rejected: a control-plane PAT/App-token over HTTPS (token-leak risk in the URL + a new broad secret class — a GitHub App is the scalable follow-up), keeping the "clone it yourself first" throw (the dead-end itself), scaffolding from a template fork (a template repo to maintain vs `--vendor-core`'s single-source tree).](<redacted>.md) | Cloud Bongos / onboarding |
203
- | 0126 | [Dedicated cloudbongos.com control-plane droplet (task [#2063](https://example.com/builders#/task/2063), BONGOS-V1, goal 26). cloudbongos.com ran as a co-tenant on the OTB game droplet with the account-wide DO+Cloudflare tokens co-located on that same shared box — the powerful tokens sat next to the game and third-party co-hosted instances. Owner pulled the infra separation forward (accepting a 2nd droplet): a new dedicated droplet `REDACTED_IP` (nyc3, 2GB, tag `cloudbongos-control`, own `cloudbongos_ed25519` SSH) runs cloudbongos.com + `builders.`/`status.` AND is the sole token-holding control plane (`/etc/cloudbongos/box.env` holds the new account-wide CF token IP-locked to it + the DO token — where `provision.js` provisions subdomains). Co-hosting box keeps the game + `emersonian-circles.`/`staging.`; the co-tenant unit was decommissioned at cutover. Lift-and-shift (same repo clone, not the ADR 0108 pinned-core standalone). De-risks R89. Follow-ups: new-box deploy automation ([#2064](https://example.com/builders#/task/2064)) + strip all cloud tokens off the co-hosting box ([#2065](https://example.com/builders#/task/2065)). IPv6 egress disabled so the IPv4-locked CF token isn't spuriously rejected.](<redacted>.md) | infra / Cloud Bongos |
204
- | 0127 | [Cross-box fleet observability on the control-plane droplet (task [#2059](https://example.com/builders#/task/2059), BONGOS-V1). The Grafana/Prometheus stack (task 2020) exports Bongos-API metrics = *platform* observability, so it was re-homed off `metrics.example.com` onto the control-plane box at `metrics.cloudbongos.com`, watching the whole fleet. After ADR 0126 split the fleet across two droplets, Prometheus (on the control-plane box) scrapes `cloudbongos` locally and the co-hosting box's `example`+`emersonian` over the private VPC via `socat` TCP forwarders (`10.108.0.2:9101/9102`), ufw-locked to the control-plane IP. socat (raw relay) preserves the exporter's loopback-no-token semantics → no token, no game restart; the VPC+ufw is the boundary; the `METRICS_TOKEN` Bearer path is reserved for future off-VPC instances. Dashboards gained a `project` template var. Trade-off: socat exposes the whole app port (not just `/metrics`) to the trusted control-plane box over the VPC — accepted (that box already holds the account-wide tokens). Follow-up: token/DNS-SD discovery for provisioned instances.](<redacted>.md) | infra / Cloud Bongos |
205
- | 0128 | [Provisioning runner privilege + DB-auth model (task [#2066](https://example.com/builders#/task/2066), BONGOS-V1, goal 35's `project-creation-flow`). [ADR 0111](<redacted>.md)'s `provision.js` was built + unit-tested but NEVER run end-to-end (existing instances were hand-wired); the first real greenfield standup ([#2053](https://example.com/builders#/task/2053)–[#2056](https://example.com/builders#/task/2056)) exposed that the runner (`User=lars`, non-root) can't do its own mutating steps: `/etc` writes (`EACCES`), `systemctl` reloads, and `createdb`/`migrate` (peer-auth-as-lars, but lars isn't a usable PG role — the app uses `DATABASE_URL`). Decision: a `PROVISION_SUDO=1` knob (alias `PROVISION_PRIVILEGED`), **default OFF** (bare commands + direct `fs` writes — tests/root/co-tenant-legacy byte-unchanged); ON escalates exactly those steps — `/etc` writes via temp-file + `sudo install -D -m <mode>`, `sudo systemctl`/`sudo rm`, and `createdb`/`migrate` as `sudo -u postgres` (migrate passes env via `env …`+`bash …` to survive sudo's env reset). Dry-run always logs the exact command. The runner unit gains `Environment=PROVISION_SUDO=1` and **must** flip `NoNewPrivileges` false (a `true` value sets `PR_SET_NO_NEW_PRIVS`, silently blocking the setuid `sudo`). Preconditions: passwordless sudo for the runner user + a postgres-readable checkout. Rejected: granting `lars` a PG role + writable `/etc` (non-portable, weakens the trust boundary), running the whole runner as root (over-privileged vs per-step sudo), createdb/migrate via the app `DATABASE_URL` (valid alt, but the runner holds no app DB creds today). The dedicated-droplet cloud-init path is left unchanged (root context, out of scope).](<redacted>.md) | infra / Cloud Bongos |
206
- | 0129 | [cloudbongos.com control-plane box deploys by a pull timer, not the CI push (task [#2064](https://example.com/builders#/task/2064), BONGOS-V1, follows ADR 0126). After the dedicated-droplet migration, the co-hosting box's CI deploy only touched example.com — nothing pulled `main` onto the new box, so cloudbongos.com drifted. Chosen: a pull-based `systemd` timer (`infra/cloudbongos-deploy.{service,timer}` → `scripts/deploy/deploy-cloudbongos.sh`: fetch+reset → npm ci if lockfile changed → `MEDUSA_GDS_ONLY` GDS-only migrate of the `cloudbongos` DB → restart `cloudbongos` → 3002 healthz), every ~2 min with a fast no-op path. Rejected extending the CI push: that would store THIS box's prod SSH key as a GitHub Actions secret — more sprawl, against ADR 0126's blast-radius reduction; the pull uses the box's existing `github-deploy` key and touches no shared secret. Two divergences from `deploy-prod.sh`: the executed script is a stable copy at `/home/lars/deploy.sh` (so `git reset` can't corrupt the running script), and the SEC [#863](https://example.com/builders#/task/863) audit gate is non-fatal here (the cloudbongos DB can't resolve OTB author ranks; the authoritative gate runs on the co-hosting box + CI against the same main). One-time operator bootstrap; self-heals after.](<redacted>.md) | infra / Cloud Bongos |
207
- | 0130 | [provision.js remote-exec over SSH — the control plane orchestrates co-tenant workloads on the co-hosting box (task [#2065](https://example.com/builders#/task/2065), BONGOS-V1, follows ADR 0126). To reach ZERO cloud tokens on the co-hosting box, the provisioning runner must move to the token-holding control plane yet still drive co-tenants that RUN on the co-hosting box. Decision: thread a target through `provision.js`'s injected executors — box-target `exec`/`writeFile` run box-mutating steps (createdb, migrate, /etc, systemctl, Caddy, healthz) on `PROVISION_REMOTE_HOST` over `ssh <host> bash -s` (stdin-fed, no quoting; files base64→`sudo tee`); `controlExec`/`controlWriteFile` keep the token step (Cloudflare DNS hook) + the standalone repo scaffold LOCAL where the token/toolchain live. Default OFF (unset ⇒ byte-identical local, today's behavior). `box.js` needs no change (pure DO/CF API — runs from the control plane with the tokens). `PROVISION_PUBLIC_IP` = the co-hosting box IP (co-tenant A records point where they run). Ships a control-plane runner unit (`infra/provision-intent-runner-cloudbongos.{service,timer}`, later superseded by `cohost-provision-intent-runner.*` — ADR 0132). Creates a control-plane→co-hosting SSH trust edge (token-holder commands token-free box, never reverse). Activation (SSH trust · runner move · token removal+rotation · rollback-DB drop) is a sequenced OWNER op — blocker attached to 2065.](<redacted>.md) | infra / Cloud Bongos |
208
- | 0131 | [Rank-scoped skill visibility — feasibility spike (task [#1650](https://example.com/builders#/task/1650)). Wanted: a session lists only the skills the builder's rank can use, via a SessionStart hook filtering the skill menu — pure UX declutter (the server rank gate + each skill's early self-check still enforce). Finding: Claude Code hooks CANNOT filter/hide/rewrite the skill listing — SessionStart offers only `additionalContext` (inject text) + `reloadSkills` (add-only), no hook subtracts a skill, and hooks can't mutate settings at runtime. A HARD menu hide needs a pre-launch step (the launcher that already fetches rank) writing per-builder `skillOverrides:"off"` into `.claude/settings.local.json` before `claude` starts (misses plugin skills; workspace-trust-gated). Recommendation: don't build the heavy launcher change for a cosmetic gain; if some declutter is wanted now, use the existing hook's `additionalContext` as a soft nudge; keep the server gate + self-checks. Owner to decide.](<redacted>.md) | dx / skills |
209
- | 0132 | [Migrate the co-hosting fleet to the control plane over an SSH DB tunnel (task [#2071](https://example.com/builders#/task/2071), BONGOS-V1, goal 26; extends ADR 0130, completes 2065 / blocker 64). Stripping the co-hosting box's tokens proved NOT a switch-flip: it runs FIVE token-consuming timers (the 4 `box.js` dev-box lifecycle sweeps + `provision-intent-runner`), each reading its queue from the box's LOCAL `example` Postgres (firewalled to localhost) — so a control-plane runner can't reach the queues, and ADR 0130 moved only box-step execution, not queue reading. Also `box.js` was never migrated. Decision: a persistent SSH tunnel (control-plane `127.0.0.1:5433` → co-hosting `127.0.0.1:5432`, `infra/cohost-db-tunnel.service`) + all five runners re-homed as `cohost-*` control-plane variants reading the example queues via `DATABASE_URL` through the tunnel (no public Postgres exposure), calling DO/CF with the control-plane tokens; provisioning remote-execs box steps per ADR 0130. DB auth: a scram password on the EXISTING `lars` role (identical privileges the runners already use; reachable only via `127.0.0.1`), stored only in chmod-600 `/etc/cloudbongos/cohost-fleet.env`. `box.js` unchanged (pure DO/CF API + Discord/DNS-hook execs, all on the control plane). `cohost-provision-intent-runner.*` supersedes 2065's `provision-intent-runner-cloudbongos.*`. Rejected: exposing Postgres over the VPC (opens a TCP surface + `pg_hba`/cred work; droplets not guaranteed same-VPC); a token-signing broker on the co-hosting box (leaves tokens' blast radius there — opposite of the goal); moving the queue tables to the cloudbongos DB (the OTB web tier can't reach it either — firewall symmetry). Units ship additive + not-enabled, self-healed install-only. Activation (SSH trust · DB password · tunnel · enable `cohost-*` + disable co-hosting timers same window · verify via drift-reconcile · bake · owner token rotate+strip · drop rollback DB) is a sequenced operator-op.](<redacted>.md) | infra / Cloud Bongos |
210
- | 0133 | [One-click GitHub sign-in for onboarding via the GitHub App Manifest flow (task [#2080](https://example.com/builders#/task/2080), BONGOS-V1, goal 35 / criterion 165). The last onboarding friction was wiring GitHub sign-in — the owner had to hand-create an OAuth App + paste a client secret, unscriptable because GitHub has NO create-OAuth-App API. Decision: use GitHub's App **Manifest** flow. The signed-in owner clicks one pre-filled link; GitHub creates the App and returns a temporary `code`; the control-plane runner (`provision.js`, sole token-holder) exchanges it for `client_id`/`client_secret`, writes them to `/etc/<slug>/web.env` (the `oauth-secret.js` path), restarts, and verifies `/healthz` auth_configured — the secret never touches the browser, chat, or web tier. Spans the trust boundary: web tier mints a one-time `state` + stores a `provisioning_oauth_manifests` row + a PUBLIC callback stores GitHub's code (`state` is the auth); the control-plane runner drains `code_received` rows on its existing timer tick (gated on instance=active). Sign-in code unchanged — a GitHub App's user token hits the same `GET /user`; GitHub ignores the extra `scope` for Apps. Back-compat: existing OAuth-app instances untouched; manifest is the new default, manual path kept as fallback. All three onboarding surfaces (hall wizard one-click button, `bongos onboard`, `/new-project`) updated in lockstep.](<redacted>.md) | onboarding / auth |
211
- | 0134 | [Private-first npm distribution for the core + client (task [#2089](https://example.com/builders#/task/2089), BONGOS-V1, goal 35 / criterion 165; refines [#2025](https://example.com/builders#/task/2025), builds on ADR 0108). Owner wants the npm/CLI onboarding commands ready but private pre-launch, choosing paid private npm over free GitHub Packages. npm rule forces the shape: unscoped names are always public, only SCOPED packages can be private (paid plan) — so the private form stays scoped `@bongos/core` + `@bongos/client` (zero rename; org already owned). Decision: publish both as scoped PRIVATE packages now via `publishConfig:{access:"restricted"}` baked into the synthesized core `package.json` (`package-core.js buildPackageJson`) + the generated client `package.json` (`gen-api-client.js`); `package-core.js --publish` dry-runs by default, real publish needs `--publish --live` + owner `npm login` (tooling holds no token). Consumers auth via env-fed `.npmrc` `_authToken=<redacted> Vendored-tarball install (task 2053) stays the working default; provision/init registry auto-pull is a follow-up. Going public at launch is one flag (`npm access public`) or task 2025's unscoped vanity `cloudbongos` — 2025 becomes the go-public step, not superseded. The daily `bongos` command is name-independent throughout. Runbook: docs/recipes/private-npm-distribution.md. **CLIENT HALF SUPERSEDED** ([task 1003742](https://cloudbongos.com/builders#/task/1003742), 2026-09-09): `@bongos/client` is not an npm package at all — published once at 0.0.2 on 2026-07-07, never updated while the API moved 500+ core versions, and ZERO consumers the whole time (no package.json in the core or any instance repo ever declared it; every consumer vendors a copy, and `@cloudbongos/cli` could not have depended on a private package anyway). Removed from the registry; the generator no longer emits a `publishConfig` and its README documents vendoring. The generated artifact stays (ADR 0118). Core half unaffected.](<redacted>.md) | distribution / npm |
212
- | 0135 | [Module upstream submission: the review queue lives in the submitting instance's own DB, interim (task [#1770](https://example.com/builders#/task/1770), BONGOS-V1, builds on ADR 0107). ADR 0107 §1 says `bongos module submit` files a packaged module into a rank-gated review queue "on the core" — but the extracted core (`github.com/example-owner/cloud-bongos`) is a private GIT REPO, not a running service (no HTTP API), and per ADR 0100 §3 each project runs its OWN GDS DB, so there is no shared cross-repo queue to file into; R85 (the forcing-function second consumer) was also abandoned. Decision: the queue is a table (`module_submissions`, migration `<redacted>.sql`) in THIS instance's own Bongos DB. `bongos module submit <key>` and the hall's submit button both call the SAME route, `POST /modules/:key/submit` (metic+archon), which re-runs the ADR 0107 §4 pre-check against its OWN on-disk copy (authoritative — a local CLI pass is a courtesy) and refuses without a sign-off; `GET /modules/submissions` lists the queue for task 1771 (core-side review & accept, still backlog) to read from. Rejected: a live call to a not-yet-built core endpoint (guesses at 1771's contract); a GitHub Issue/PR against the private core repo (loses the structured queue, no standing credential); a dry-run-only local-file client mirroring do-api.js (a worse queue than a DB table this instance already operates). Forward-compatible: when a real core endpoint exists, filing becomes a network hop with no change to the CLI/UI contract.](<redacted>.md) | modules / upstreaming |
213
- | 0136 | [Update-channel subscription policy (task [#2150](https://example.com/builders#/task/2150), BONGOS-V1, goal 26; completes the update-consumption workstream on ADR 0100 §2 + ADR 0134 + task 2149). Answers "how do projects subscribe to regular @bongos/core updates?" with the owner-chosen policy: **patch-only by default, health-gated with auto-rollback, opt-in, autonomy-gated.** Per-instance `update_channel` = `pinned` (never) / `patch` (higher z within x.y — DEFAULT) / `minor` (higher y within the major); a MAJOR is never automatic, prereleases never auto-targeted — the pure math is `scripts/gds/update-channel.js` (`resolveChannelTarget`), unit-tested in `tests/update_channel.mjs`. The registry (npm view, ADR 0134) is the "what's newest" source. A new `mode:deterministic` instance-wide routine `core-update-subscription` reads the opt-in roster `config/update-subscriptions.json` (ships EMPTY) and runs `bongos upgrade --to <v> --registry` per subscribed instance with auto-rollback ON (task 2149). Triple-gated: autonomy flag OFF by default (ADR 0115) + empty roster + health-gated. Upstream auto-publish (whether main merges auto-publish a patch) is deliberately SPLIT to its own follow-up task, so goal 26 closes on the consumption half; the subscription just consumes whatever the owner publishes. Scope is local same-host instances; multi-host fleet orchestration is the deferred fleet-control-plane epic (task 1948). Rejected: a `provisioning_instances` DB column (presumes the still-unbuilt fleet-registry schema; a config manifest is migration-free + ADR-0115-shaped), minor-by-default (less conservative), deciding auto-publish here (orthogonal, would block consumption).](<redacted>.md) | distribution / updates |
214
- | 0137 | [Upstream publish policy: `@bongos/core` patches are published MANUALLY (owner-gated), NOT auto-published on merge (task [#2158](https://example.com/builders#/task/2158), BONGOS-V1, goal 26; the SUPPLY half split out of ADR 0136 §5 / task 2150, builds on ADR 0134). Owner chose manual over auto. Decision: the owner runs `package-core.js --publish --live` to cut a release (optional `core-v<version>` git tag as the auditable marker — triggers no CI); `CORE_VERSION` (src/module-api.js) is bumped by hand in-task with a changelog line (provenance stays in git, not a release-tool side effect); NO npm write-token at rest (preserves ADR 0134 tokenless tooling — owner auths at publish time; only the read token exists); the no-leak/mirror-redact gate is already fail-closed inside `package-core.js`, so `--publish` cannot upload a leaking artifact. The ADR 0136 subscription consumes whatever is published; freshness is owner-paced by design. Rejected: auto-publish-on-merge (needs a standing npm write-token as a CI secret — reverses the tokenless posture for little gain at low, deliberate release volume). Revisit as a tag-triggered CI publish when the managed-fleet epic (task 1948) makes the manual step a real bottleneck. Ritual: docs/recipes/private-npm-distribution.md "Cutting a core release".](<redacted>.md) | distribution / npm |
215
- | 0138 | [A core-bundled module's external npm deps are declared per-module; the consumer composes them, core stays lean (task [#2160](https://example.com/builders#/task/2160), BONGOS-V1, goal 26; R89 cutover rehearsal finding F2, builds on ADR 0108 + ADR 0083). The discord module ships inside `@bongos/core` but `package-core.js` declares only express+pg, so a consumer that enables discord crashed at boot on missing `discord.js` (the only such case; discord is `default:false`). Owner chose lean-core over hoisting. Decision: core deps stay express+pg; a bundled module declares its own npm deps in `module.json` `dependencies` (schema-validated in `manifest-schema.js`; discord declares `discord.js`); greenfield `bongos init` composes the ENABLED modules' deps into the instance package.json (`enabledModuleDeps`→`buildInstancePackageJson`, existing pin wins, core always set); the R89 cutover preserves the monolith's existing closure ∪ core. Rejected: hoisting `discord.js` into core `optionalDependencies` (installs for EVERY consumer though discord is opt-in — cuts against ADR 0108 lean-core); `peerDependencies` (optional peers aren't auto-installed → crash remains; required peers = hoisting-with-warnings).](<redacted>.md) | modules / distribution |
197
+ | 0120 | [Pay example on land (not on confirm) + a builder-owned rebase gate (task [#2035](https://example.com/builders#/task/2035), BONGOS-V1). Credits were awarded at the `confirmed` transition — before the code is on `main` — so stranded/never-pushed work paid out with no consequence, and the builder (the only one who can rebase it) had no reason to. Decision: (1) move BOTH the per-task credit AND the cost-plus session reward to the `shipped` (merged-to-`main`) transition; (2) the publish-reconciler flags a confirmed task `needs_rebase` (attributed to the claim holder) once auto-land is exhausted — merge conflict, failing required check vs current `main`, or no pushed branch; (3) a `needs_rebase` task blocks that builder's NEW claims (`REBASE_REQUIRED`) but not their existing/in-flight ones; (4) a one-time migration claws back the `task.confirmed` credits on the current confirmed backlog and re-gates it. Happy path unaffected (a clean PR auto-lands in minutes and pays then); the teeth bite only on stranding. Rejected: gate-only (stranded work still paid), soft-warn (ignorable), grandfather the backlog (leaves today's stranded-and-paid state), per-task-credit-only (owner chose fullest alignment).](0120-pay-on-land-and-builder-owned-rebase-gate.md) | economy + lifecycle |
198
+ | 0122 | [Onboarding diagram renders: untracked + rendered at deploy from a host-provisioned Chrome (task [#2027](https://example.com/builders#/task/2027), BONGOS-V1) — completes the diagram half [ADR 0110](0110-untrack-whole-file-generated-nav-docs.md) deferred. The `.svg`/`.png` renders were committed on every render-capable ship and kept colliding on parallel branches; ADR 0110 deferred them because `mermaid-cli`/puppeteer downloads its own Chromium on first run (a multi-minute fetch that stalls the 2 GB droplet). Decision: provision Google Chrome once on the host; `render-diagrams.sh` auto-detects an installed browser (or honors `PUPPETEER_EXECUTABLE_PATH`) and sets `PUPPETEER_SKIP_DOWNLOAD` so it never fetches (falls back to the download only on a dev laptop), passing a committed `puppeteer-config.json` with `--no-sandbox` (Ubuntu 24.04 restricts unprivileged user namespaces) and pinning `mermaid-cli@11`; deploy-prod.sh + box-source-fetch render from the tracked `.mmd` (LOUD `::ERROR::`, best-effort); `git rm --cached` + gitignore the `*.svg`/`*.png` (the `.mmd` + `assertions.json` stay tracked, so the nightly diagram-drift cron is unaffected); ship runs `gen-diagrams --no-render`. Verified rendering all 5 diagrams on the prod droplet before landing. Rejected: on-deploy Chromium download (the droplet stall), snap chromium (flaky headless), keep-tracked + strengthen the ADR 0082 resolver (hides the collision).](0122-onboarding-diagram-renders-deploy-rendered-off-git.md) | internal / ADR 0110 |
199
+ | 0121 | [Greenfield vs. brownfield onboarding — scope `bongos init --adopt` (task [#1986](https://example.com/builders#/task/1986), BONGOS-V1, goal 35's `adopt-existing-repo` criterion). Today the creation flow is greenfield-only: `scripts/gds/init.js` opens *"stand up a NEW instance"*, refuses to run where `config/branding.json` exists (`--force` overwrites), and seeds a synthetic `"V1 — Prove the loop"`; it inspects nothing about an existing tree. Decision: TWO explicit modes over goal 35's one canonical step sequence (task 1981), branching at exactly the SCAFFOLD leg — greenfield (`bongos init`, today) vs brownfield (`bongos init --adopt`); every other leg (provision, OAuth, verify, brand) identical, so the CLI (1982) + hall wizard (1983) forms inherit adopt for free. `--adopt` must: (1) LAYER not clobber — add the pinned `@bongos/core` dep + lean `config/` + `.claude/` additively, merge-or-report on pre-existing files, never reuse the `--force`-overwrites path; (2) reflect REAL state — capture the repo's actual version/goal instead of the synthetic V1, import the existing backlog (GitHub issues / TODO / roadmap) into GDS tasks; (3) DETECT the existing stack + layout so modules / repo-map / fitness-boundary fit; (4) a CONFLICT PRE-FLIGHT report (migration namespace, fitness boundary, protected-path / rank gates, existing CI), `--dry-run` default, gated behind an accept step. Scope-only — no adopt code ships; seeds a 5-task dependency-wired breakdown. Rejected: overloading `--force` to merge (footgun), a separate `bongos adopt` command (leg duplication / drift), auto-import with no pre-flight, building adopt-mode now (1986 is scope-only).](0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md) | Cloud Bongos / onboarding |
200
+ | 0123 | [Idle-swept dev boxes lose uncommitted work: an on-box autosave-push, not a VM snapshot (task [#837](https://example.com/builders#/task/837) SPIKE). `box.js`'s idle sweep hard-deletes the droplet once a box reads idle — nothing on that path checks git status, and park's snapshot was deliberately removed from the automated path for cost. Decision: a new on-box cron (sibling to `box-heartbeat.sh`) that, when `/workspace` is dirty, commits + pushes to the box's own `claude/<name>` claim branch using the same one-shot-credentialed-then-scrubbed pattern `box-source-fetch.sh` already uses for fetch — bounding loss to the autosave interval (~5 min) instead of unbounded. Open dependency: confirm/mint a push-scoped box credential (today's `BOX_SOURCE_FULL_TOKEN` is fetch-only by convention). Rejected: reintroducing snapshot-before-destroy (the exact cost the team removed), a control-plane git-status check before sweep (needs new box-exec capability, would pin dirty boxes active forever), relying on `/builder-end`'s manual dirty-check alone (doesn't fire on the automatic sweep path at all). Scope-only — no autosave code ships; seeds a follow-up implementation task.](0123-box-idle-sweep-autosave-before-destroy.md) | internal / dev-box |
201
+ | 0124 | [Typed API response bodies — follow-up to ADR 0118 for goal 36's `api-client-sdk` (task [#2061](https://example.com/builders#/task/2061), from idea 519, BONGOS-V1). Task 2050 typed every request body but success RESPONSES stayed the `ApiResponse` (`= any`) alias — routes declare no response schema. Decision: infer the success shape STATICALLY from each route's inline `res.json({ … })` object literal (reusing the request extractor's `captureBalanced`/`splitTopLevel` machinery; never executes a route) → a `<OperationId>Response` component on the `200` `content`, and the client returns `Promise<ThatShape>`. Only KEYS are provable (values type `unknown` unless trivially safe — a boolean literal, or the ADR-0119 `pageMeta()` `page` block → a shared `PageInfo`). Degrade-gracefully: a bare value (`res.json(row)`), a top-level spread, an empty `{}`, or a computed key keeps `ApiResponse` — we only ever narrow `any`, never mistype. 162/221 endpoints now typed; runtime JS unchanged (only `openapi.json` + `index.d.ts`). Rejected: a per-route `responseSchema` export (duplicates the `res.json` truth + 200-file churn, anti-code-first), executing routes to observe responses (breaks the zero-execution contract), tracing variables to type values (brittle data-flow analysis), `additionalProperties:false` on responses (a false compile error if an unparsed branch adds a key).](0124-typed-api-response-bodies.md) | API contract / goal 36 |
202
+ | 0125 | [The control plane scaffolds + pushes a standalone instance's repo (task [#2055](https://example.com/builders#/task/2055), BONGOS-V1, goal 35's `project-creation-flow`). `provision.js` used to THROW if a standalone instance's repo wasn't already hand-cloned at `/srv/cloudbongos/<slug>` — a manual dead-end between `bongos init` and provision. Decision: the provisioning runner SCAFFOLDS the repo itself (`scaffoldStandaloneRepo`) before the existing code-sync/db/service steps: clone the empty `target_ref` (forcing branch `main` on the unborn HEAD) → build the core tarball (`package-core.js`) → materialize `config/` + `.claude/` + a `file:`-pinned `package.json` + lockfile via `bongos init --vendor-core` (task 2053, `--no-seed`) → commit + push (with a `node_modules`-excluding `.gitignore`; vendored tarball + lockfile stay tracked). Idempotent (skips once `package.json` exists) + dry-run-safe (injected `exec`/`writeFile`). Git transport reuses the existing `github-deploy` SSH alias (no secret in any command) — the one operational grant is that the control plane's deploy key gets WRITE on each instance repo (per-repo, like the platform repo). `deploy-instance.sh` already existed at `scripts/deploy/` (ADR 0108 §4 / task 1884; the task description predated it) — it's the ongoing consumer deploy, not the first-time scaffold; no `infra/` duplicate created. Rejected: a control-plane PAT/App-token over HTTPS (token-leak risk in the URL + a new broad secret class — a GitHub App is the scalable follow-up), keeping the "clone it yourself first" throw (the dead-end itself), scaffolding from a template fork (a template repo to maintain vs `--vendor-core`'s single-source tree).](0125-control-plane-scaffolds-standalone-instance-repo.md) | Cloud Bongos / onboarding |
203
+ | 0126 | [Dedicated cloudbongos.com control-plane droplet (task [#2063](https://example.com/builders#/task/2063), BONGOS-V1, goal 26). cloudbongos.com ran as a co-tenant on the OTB game droplet with the account-wide DO+Cloudflare tokens co-located on that same shared box — the powerful tokens sat next to the game and third-party co-hosted instances. Owner pulled the infra separation forward (accepting a 2nd droplet): a new dedicated droplet `REDACTED_IP` (nyc3, 2GB, tag `cloudbongos-control`, own `cloudbongos_ed25519` SSH) runs cloudbongos.com + `builders.`/`status.` AND is the sole token-holding control plane (`/etc/cloudbongos/box.env` holds the new account-wide CF token IP-locked to it + the DO token — where `provision.js` provisions subdomains). Co-hosting box keeps the game + `emersonian-circles.`/`staging.`; the co-tenant unit was decommissioned at cutover. Lift-and-shift (same repo clone, not the ADR 0108 pinned-core standalone). De-risks R89. Follow-ups: new-box deploy automation ([#2064](https://example.com/builders#/task/2064)) + strip all cloud tokens off the co-hosting box ([#2065](https://example.com/builders#/task/2065)). IPv6 egress disabled so the IPv4-locked CF token isn't spuriously rejected.](0126-dedicated-cloudbongos-control-plane-droplet.md) | infra / Cloud Bongos |
204
+ | 0127 | [Cross-box fleet observability on the control-plane droplet (task [#2059](https://example.com/builders#/task/2059), BONGOS-V1). The Grafana/Prometheus stack (task 2020) exports Bongos-API metrics = *platform* observability, so it was re-homed off `metrics.example.com` onto the control-plane box at `metrics.cloudbongos.com`, watching the whole fleet. After ADR 0126 split the fleet across two droplets, Prometheus (on the control-plane box) scrapes `cloudbongos` locally and the co-hosting box's `example`+`emersonian` over the private VPC via `socat` TCP forwarders (`10.108.0.2:9101/9102`), ufw-locked to the control-plane IP. socat (raw relay) preserves the exporter's loopback-no-token semantics → no token, no game restart; the VPC+ufw is the boundary; the `METRICS_TOKEN` Bearer path is reserved for future off-VPC instances. Dashboards gained a `project` template var. Trade-off: socat exposes the whole app port (not just `/metrics`) to the trusted control-plane box over the VPC — accepted (that box already holds the account-wide tokens). Follow-up: token/DNS-SD discovery for provisioned instances.](0127-cross-box-fleet-observability.md) | infra / Cloud Bongos |
205
+ | 0128 | [Provisioning runner privilege + DB-auth model (task [#2066](https://example.com/builders#/task/2066), BONGOS-V1, goal 35's `project-creation-flow`). [ADR 0111](0111-instance-hosting-provisioning-module.md)'s `provision.js` was built + unit-tested but NEVER run end-to-end (existing instances were hand-wired); the first real greenfield standup ([#2053](https://example.com/builders#/task/2053)–[#2056](https://example.com/builders#/task/2056)) exposed that the runner (`User=lars`, non-root) can't do its own mutating steps: `/etc` writes (`EACCES`), `systemctl` reloads, and `createdb`/`migrate` (peer-auth-as-lars, but lars isn't a usable PG role — the app uses `DATABASE_URL`). Decision: a `PROVISION_SUDO=1` knob (alias `PROVISION_PRIVILEGED`), **default OFF** (bare commands + direct `fs` writes — tests/root/co-tenant-legacy byte-unchanged); ON escalates exactly those steps — `/etc` writes via temp-file + `sudo install -D -m <mode>`, `sudo systemctl`/`sudo rm`, and `createdb`/`migrate` as `sudo -u postgres` (migrate passes env via `env …`+`bash …` to survive sudo's env reset). Dry-run always logs the exact command. The runner unit gains `Environment=PROVISION_SUDO=1` and **must** flip `NoNewPrivileges` false (a `true` value sets `PR_SET_NO_NEW_PRIVS`, silently blocking the setuid `sudo`). Preconditions: passwordless sudo for the runner user + a postgres-readable checkout. Rejected: granting `lars` a PG role + writable `/etc` (non-portable, weakens the trust boundary), running the whole runner as root (over-privileged vs per-step sudo), createdb/migrate via the app `DATABASE_URL` (valid alt, but the runner holds no app DB creds today). The dedicated-droplet cloud-init path is left unchanged (root context, out of scope).](0128-provisioning-runner-privilege-and-db-auth-model.md) | infra / Cloud Bongos |
206
+ | 0129 | [cloudbongos.com control-plane box deploys by a pull timer, not the CI push (task [#2064](https://example.com/builders#/task/2064), BONGOS-V1, follows ADR 0126). After the dedicated-droplet migration, the co-hosting box's CI deploy only touched example.com — nothing pulled `main` onto the new box, so cloudbongos.com drifted. Chosen: a pull-based `systemd` timer (`infra/cloudbongos-deploy.{service,timer}` → `scripts/deploy/deploy-cloudbongos.sh`: fetch+reset → npm ci if lockfile changed → `MEDUSA_GDS_ONLY` GDS-only migrate of the `cloudbongos` DB → restart `cloudbongos` → 3002 healthz), every ~2 min with a fast no-op path. Rejected extending the CI push: that would store THIS box's prod SSH key as a GitHub Actions secret — more sprawl, against ADR 0126's blast-radius reduction; the pull uses the box's existing `github-deploy` key and touches no shared secret. Two divergences from `deploy-prod.sh`: the executed script is a stable copy at `/home/lars/deploy.sh` (so `git reset` can't corrupt the running script), and the SEC [#863](https://example.com/builders#/task/863) audit gate is non-fatal here (the cloudbongos DB can't resolve OTB author ranks; the authoritative gate runs on the co-hosting box + CI against the same main). One-time operator bootstrap; self-heals after.](0129-cloudbongos-control-plane-pull-deploy.md) | infra / Cloud Bongos |
207
+ | 0130 | [provision.js remote-exec over SSH — the control plane orchestrates co-tenant workloads on the co-hosting box (task [#2065](https://example.com/builders#/task/2065), BONGOS-V1, follows ADR 0126). To reach ZERO cloud tokens on the co-hosting box, the provisioning runner must move to the token-holding control plane yet still drive co-tenants that RUN on the co-hosting box. Decision: thread a target through `provision.js`'s injected executors — box-target `exec`/`writeFile` run box-mutating steps (createdb, migrate, /etc, systemctl, Caddy, healthz) on `PROVISION_REMOTE_HOST` over `ssh <host> bash -s` (stdin-fed, no quoting; files base64→`sudo tee`); `controlExec`/`controlWriteFile` keep the token step (Cloudflare DNS hook) + the standalone repo scaffold LOCAL where the token/toolchain live. Default OFF (unset ⇒ byte-identical local, today's behavior). `box.js` needs no change (pure DO/CF API — runs from the control plane with the tokens). `PROVISION_PUBLIC_IP` = the co-hosting box IP (co-tenant A records point where they run). Ships a control-plane runner unit (`infra/provision-intent-runner-cloudbongos.{service,timer}`, later superseded by `cohost-provision-intent-runner.*` — ADR 0132). Creates a control-plane→co-hosting SSH trust edge (token-holder commands token-free box, never reverse). Activation (SSH trust · runner move · token removal+rotation · rollback-DB drop) is a sequenced OWNER op — blocker attached to 2065.](0130-provision-remote-exec-over-ssh.md) | infra / Cloud Bongos |
208
+ | 0131 | [Rank-scoped skill visibility — feasibility spike (task [#1650](https://example.com/builders#/task/1650)). Wanted: a session lists only the skills the builder's rank can use, via a SessionStart hook filtering the skill menu — pure UX declutter (the server rank gate + each skill's early self-check still enforce). Finding: Claude Code hooks CANNOT filter/hide/rewrite the skill listing — SessionStart offers only `additionalContext` (inject text) + `reloadSkills` (add-only), no hook subtracts a skill, and hooks can't mutate settings at runtime. A HARD menu hide needs a pre-launch step (the launcher that already fetches rank) writing per-builder `skillOverrides:"off"` into `.claude/settings.local.json` before `claude` starts (misses plugin skills; workspace-trust-gated). Recommendation: don't build the heavy launcher change for a cosmetic gain; if some declutter is wanted now, use the existing hook's `additionalContext` as a soft nudge; keep the server gate + self-checks. Owner to decide.](0131-rank-scoped-skill-visibility.md) | dx / skills |
209
+ | 0132 | [Migrate the co-hosting fleet to the control plane over an SSH DB tunnel (task [#2071](https://example.com/builders#/task/2071), BONGOS-V1, goal 26; extends ADR 0130, completes 2065 / blocker 64). Stripping the co-hosting box's tokens proved NOT a switch-flip: it runs FIVE token-consuming timers (the 4 `box.js` dev-box lifecycle sweeps + `provision-intent-runner`), each reading its queue from the box's LOCAL `example` Postgres (firewalled to localhost) — so a control-plane runner can't reach the queues, and ADR 0130 moved only box-step execution, not queue reading. Also `box.js` was never migrated. Decision: a persistent SSH tunnel (control-plane `127.0.0.1:5433` → co-hosting `127.0.0.1:5432`, `infra/cohost-db-tunnel.service`) + all five runners re-homed as `cohost-*` control-plane variants reading the example queues via `DATABASE_URL` through the tunnel (no public Postgres exposure), calling DO/CF with the control-plane tokens; provisioning remote-execs box steps per ADR 0130. DB auth: a scram password on the EXISTING `lars` role (identical privileges the runners already use; reachable only via `127.0.0.1`), stored only in chmod-600 `/etc/cloudbongos/cohost-fleet.env`. `box.js` unchanged (pure DO/CF API + Discord/DNS-hook execs, all on the control plane). `cohost-provision-intent-runner.*` supersedes 2065's `provision-intent-runner-cloudbongos.*`. Rejected: exposing Postgres over the VPC (opens a TCP surface + `pg_hba`/cred work; droplets not guaranteed same-VPC); a token-signing broker on the co-hosting box (leaves tokens' blast radius there — opposite of the goal); moving the queue tables to the cloudbongos DB (the OTB web tier can't reach it either — firewall symmetry). Units ship additive + not-enabled, self-healed install-only. Activation (SSH trust · DB password · tunnel · enable `cohost-*` + disable co-hosting timers same window · verify via drift-reconcile · bake · owner token rotate+strip · drop rollback DB) is a sequenced operator-op.](0132-cohost-fleet-control-plane-migration.md) | infra / Cloud Bongos |
210
+ | 0133 | [One-click GitHub sign-in for onboarding via the GitHub App Manifest flow (task [#2080](https://example.com/builders#/task/2080), BONGOS-V1, goal 35 / criterion 165). The last onboarding friction was wiring GitHub sign-in — the owner had to hand-create an OAuth App + paste a client secret, unscriptable because GitHub has NO create-OAuth-App API. Decision: use GitHub's App **Manifest** flow. The signed-in owner clicks one pre-filled link; GitHub creates the App and returns a temporary `code`; the control-plane runner (`provision.js`, sole token-holder) exchanges it for `client_id`/`client_secret`, writes them to `/etc/<slug>/web.env` (the `oauth-secret.js` path), restarts, and verifies `/healthz` auth_configured — the secret never touches the browser, chat, or web tier. Spans the trust boundary: web tier mints a one-time `state` + stores a `provisioning_oauth_manifests` row + a PUBLIC callback stores GitHub's code (`state` is the auth); the control-plane runner drains `code_received` rows on its existing timer tick (gated on instance=active). Sign-in code unchanged — a GitHub App's user token hits the same `GET /user`; GitHub ignores the extra `scope` for Apps. Back-compat: existing OAuth-app instances untouched; manifest is the new default, manual path kept as fallback. All three onboarding surfaces (hall wizard one-click button, `bongos onboard`, `/new-project`) updated in lockstep.](0133-github-app-manifest-onboarding-signin.md) | onboarding / auth |
211
+ | 0134 | [Private-first npm distribution for the core + client (task [#2089](https://example.com/builders#/task/2089), BONGOS-V1, goal 35 / criterion 165; refines [#2025](https://example.com/builders#/task/2025), builds on ADR 0108). Owner wants the npm/CLI onboarding commands ready but private pre-launch, choosing paid private npm over free GitHub Packages. npm rule forces the shape: unscoped names are always public, only SCOPED packages can be private (paid plan) — so the private form stays scoped `@bongos/core` + `@bongos/client` (zero rename; org already owned). Decision: publish both as scoped PRIVATE packages now via `publishConfig:{access:"restricted"}` baked into the synthesized core `package.json` (`package-core.js buildPackageJson`) + the generated client `package.json` (`gen-api-client.js`); `package-core.js --publish` dry-runs by default, real publish needs `--publish --live` + owner `npm login` (tooling holds no token). Consumers auth via env-fed `.npmrc` `_authToken=<redacted> Vendored-tarball install (task 2053) stays the working default; provision/init registry auto-pull is a follow-up. Going public at launch is one flag (`npm access public`) or task 2025's unscoped vanity `cloudbongos` — 2025 becomes the go-public step, not superseded. The daily `bongos` command is name-independent throughout. Runbook: docs/recipes/private-npm-distribution.md. **CLIENT HALF SUPERSEDED** ([task 1003742](https://cloudbongos.com/builders#/task/1003742), 2026-09-09): `@bongos/client` is not an npm package at all — published once at 0.0.2 on 2026-07-07, never updated while the API moved 500+ core versions, and ZERO consumers the whole time (no package.json in the core or any instance repo ever declared it; every consumer vendors a copy, and `@cloudbongos/cli` could not have depended on a private package anyway). Removed from the registry; the generator no longer emits a `publishConfig` and its README documents vendoring. The generated artifact stays (ADR 0118). Core half unaffected.](0134-private-first-npm-distribution.md) | distribution / npm |
212
+ | 0135 | [Module upstream submission: the review queue lives in the submitting instance's own DB, interim (task [#1770](https://example.com/builders#/task/1770), BONGOS-V1, builds on ADR 0107). ADR 0107 §1 says `bongos module submit` files a packaged module into a rank-gated review queue "on the core" — but the extracted core (`github.com/example-owner/cloud-bongos`) is a private GIT REPO, not a running service (no HTTP API), and per ADR 0100 §3 each project runs its OWN GDS DB, so there is no shared cross-repo queue to file into; R85 (the forcing-function second consumer) was also abandoned. Decision: the queue is a table (`module_submissions`, migration `<redacted>.sql`) in THIS instance's own Bongos DB. `bongos module submit <key>` and the hall's submit button both call the SAME route, `POST /modules/:key/submit` (metic+archon), which re-runs the ADR 0107 §4 pre-check against its OWN on-disk copy (authoritative — a local CLI pass is a courtesy) and refuses without a sign-off; `GET /modules/submissions` lists the queue for task 1771 (core-side review & accept, still backlog) to read from. Rejected: a live call to a not-yet-built core endpoint (guesses at 1771's contract); a GitHub Issue/PR against the private core repo (loses the structured queue, no standing credential); a dry-run-only local-file client mirroring do-api.js (a worse queue than a DB table this instance already operates). Forward-compatible: when a real core endpoint exists, filing becomes a network hop with no change to the CLI/UI contract.](0135-module-upstream-submission-interim-queue.md) | modules / upstreaming |
213
+ | 0136 | [Update-channel subscription policy (task [#2150](https://example.com/builders#/task/2150), BONGOS-V1, goal 26; completes the update-consumption workstream on ADR 0100 §2 + ADR 0134 + task 2149). Answers "how do projects subscribe to regular @bongos/core updates?" with the owner-chosen policy: **patch-only by default, health-gated with auto-rollback, opt-in, autonomy-gated.** Per-instance `update_channel` = `pinned` (never) / `patch` (higher z within x.y — DEFAULT) / `minor` (higher y within the major); a MAJOR is never automatic, prereleases never auto-targeted — the pure math is `scripts/gds/update-channel.js` (`resolveChannelTarget`), unit-tested in `tests/update_channel.mjs`. The registry (npm view, ADR 0134) is the "what's newest" source. A new `mode:deterministic` instance-wide routine `core-update-subscription` reads the opt-in roster `config/update-subscriptions.json` (ships EMPTY) and runs `bongos upgrade --to <v> --registry` per subscribed instance with auto-rollback ON (task 2149). Triple-gated: autonomy flag OFF by default (ADR 0115) + empty roster + health-gated. Upstream auto-publish (whether main merges auto-publish a patch) is deliberately SPLIT to its own follow-up task, so goal 26 closes on the consumption half; the subscription just consumes whatever the owner publishes. Scope is local same-host instances; multi-host fleet orchestration is the deferred fleet-control-plane epic (task 1948). Rejected: a `provisioning_instances` DB column (presumes the still-unbuilt fleet-registry schema; a config manifest is migration-free + ADR-0115-shaped), minor-by-default (less conservative), deciding auto-publish here (orthogonal, would block consumption).](0136-update-channel-subscription-policy.md) | distribution / updates |
214
+ | 0137 | [Upstream publish policy: `@bongos/core` patches are published MANUALLY (owner-gated), NOT auto-published on merge (task [#2158](https://example.com/builders#/task/2158), BONGOS-V1, goal 26; the SUPPLY half split out of ADR 0136 §5 / task 2150, builds on ADR 0134). Owner chose manual over auto. Decision: the owner runs `package-core.js --publish --live` to cut a release (optional `core-v<version>` git tag as the auditable marker — triggers no CI); `CORE_VERSION` (src/module-api.js) is bumped by hand in-task with a changelog line (provenance stays in git, not a release-tool side effect); NO npm write-token at rest (preserves ADR 0134 tokenless tooling — owner auths at publish time; only the read token exists); the no-leak/mirror-redact gate is already fail-closed inside `package-core.js`, so `--publish` cannot upload a leaking artifact. The ADR 0136 subscription consumes whatever is published; freshness is owner-paced by design. Rejected: auto-publish-on-merge (needs a standing npm write-token as a CI secret — reverses the tokenless posture for little gain at low, deliberate release volume). Revisit as a tag-triggered CI publish when the managed-fleet epic (task 1948) makes the manual step a real bottleneck. Ritual: docs/recipes/private-npm-distribution.md "Cutting a core release".](0137-upstream-publish-policy-manual-owner-gated.md) | distribution / npm |
215
+ | 0138 | [A core-bundled module's external npm deps are declared per-module; the consumer composes them, core stays lean (task [#2160](https://example.com/builders#/task/2160), BONGOS-V1, goal 26; R89 cutover rehearsal finding F2, builds on ADR 0108 + ADR 0083). The discord module ships inside `@bongos/core` but `package-core.js` declares only express+pg, so a consumer that enables discord crashed at boot on missing `discord.js` (the only such case; discord is `default:false`). Owner chose lean-core over hoisting. Decision: core deps stay express+pg; a bundled module declares its own npm deps in `module.json` `dependencies` (schema-validated in `manifest-schema.js`; discord declares `discord.js`); greenfield `bongos init` composes the ENABLED modules' deps into the instance package.json (`enabledModuleDeps`→`buildInstancePackageJson`, existing pin wins, core always set); the R89 cutover preserves the monolith's existing closure ∪ core. Rejected: hoisting `discord.js` into core `optionalDependencies` (installs for EVERY consumer though discord is opt-in — cuts against ADR 0108 lean-core); `peerDependencies` (optional peers aren't auto-installed → crash remains; required peers = hoisting-with-warnings).](0138-bundled-module-deps-declared-per-module.md) | modules / distribution |
216
216
 
217
217
  | 0139 | [Migrate the platform work-history (tasks/goals/credits/builder identities) from the Example instance DB to the cloudbongos DB — the DATA half of the CB↔OTB split (companion to the R89 code half). Owner-locked: clean move (platform history leaves OTB), game credits stay, identity keyed by `builders.github_id` (balance surfaces on the existing GitHub sign-in, no federation hub), currency number moves + label adapts, near-empty target = clean load. Needs its own ADR because it deliberately crosses the goal-41 "authority stays per-project" line + ADR 0107 §3's deferral. WS-0 (re-provision cloudbongos.com onto core 1.17.3 first) … WS-7; the mover (WS-2) is bespoke — bigint PK renumber + full FK remap, the polymorphic `dependencies` table per-kind, `credit_log` load-with-triggers-off then rebuild `total_credits` from `SUM(delta)`, preserve the `session.token_reward` idempotency index. EXECUTION is owner-gated (WS-4+); nothing runs against production until greenlit; prove on a throwaway copy first.](<redacted>.md) | platform / migration |
218
- | 0143 | [Onboarding repo picker (adopt) + auto-create (fresh): the `/create` wizard's Home step no longer makes the user hand-type `owner/repo` or manually create an empty repo on github.com (task [#1002099](https://cloudbongos.com/builders#/task/1002099), goal 35). Owner chose to **broaden the OAuth scope** over a central GitHub App (no install step, single sign-in), accepting it is less least-privilege. Within that: scope = **`public_repo`, not `repo`** (no OAuth scope is "create-only"; `repo` grants R/W to ALL the user's code and we now PERSIST the token — `public_repo` still lists/creates public repos with a gentler consent + smaller blast radius; can't do private, revisit only if needed); **opt-in per sign-in** via `?repos=1` (a `gds_oauth_repos` intent cookie → `captureRepoToken`; hall + CLI stay `read:user`; one pure `scopeForWebFlow`); **lazy consent** (asked at step 2, not the gate); token stored **encrypted (secret-box), short TTL (1h), lazily swept**, in **core auth** not the provisioning module (it's auth's OWN handshake token, not a feature key à la ADR 0102; provisioning's charter forbids tokens/cloud-API, ADR 0111 §2 / ADR 0016); v1 **`affiliation=owner`** personal repos only (org policy deferred); **graceful degrade** — secret-box unconfigured → `503` → wizard keeps the manual inputs + manual done-step, onboarding never regresses. New: `github_oauth_tokens` (core_192), `src/bongos/github-repos.js`, `src/bongos/routes/github.js` (`GET/POST /github/repos`, requireBuilder + rate-limited). Not the App-manifest flow (ADR 0133 — sign-in-only, per-instance, no repo perms).](<redacted>.md) | onboarding / auth |
218
+ | 0143 | [Onboarding repo picker (adopt) + auto-create (fresh): the `/create` wizard's Home step no longer makes the user hand-type `owner/repo` or manually create an empty repo on github.com (task [#1002099](https://cloudbongos.com/builders#/task/1002099), goal 35). Owner chose to **broaden the OAuth scope** over a central GitHub App (no install step, single sign-in), accepting it is less least-privilege. Within that: scope = **`public_repo`, not `repo`** (no OAuth scope is "create-only"; `repo` grants R/W to ALL the user's code and we now PERSIST the token — `public_repo` still lists/creates public repos with a gentler consent + smaller blast radius; can't do private, revisit only if needed); **opt-in per sign-in** via `?repos=1` (a `gds_oauth_repos` intent cookie → `captureRepoToken`; hall + CLI stay `read:user`; one pure `scopeForWebFlow`); **lazy consent** (asked at step 2, not the gate); token stored **encrypted (secret-box), short TTL (1h), lazily swept**, in **core auth** not the provisioning module (it's auth's OWN handshake token, not a feature key à la ADR 0102; provisioning's charter forbids tokens/cloud-API, ADR 0111 §2 / ADR 0016); v1 **`affiliation=owner`** personal repos only (org policy deferred); **graceful degrade** — secret-box unconfigured → `503` → wizard keeps the manual inputs + manual done-step, onboarding never regresses. New: `github_oauth_tokens` (core_192), `src/bongos/github-repos.js`, `src/bongos/routes/github.js` (`GET/POST /github/repos`, requireBuilder + rate-limited). Not the App-manifest flow (ADR 0133 — sign-in-only, per-instance, no repo perms).](0143-onboarding-repo-picker-oauth-scope.md) | onboarding / auth |
219
219
  | 0140 | [cloudbongos.com re-provisioned to the ADR 0108 standalone model, DB-PRESERVING — closing ADR 0139's skipped WS-0 (executed 2026-07-12). The site was a lift-and-shift clone of the `example` repo frozen on stale code — every `/builders/*` asset 404'd — because its pull-deploy silently no-op'd against the unchanged game repo (`origin` was `example-owner/example`, not a cloudbongos instance); it had never run the restored code. WS-0's original fresh-DB re-provision would now WIPE the migrated history/balances, so a build-alongside-then-flip variant was used: a dedicated private `cloudbongos/cloudbongos-instance` repo (lean config over the neutral brand + a pinned `@bongos/core@1.17.3`), materialized beside the untouched old checkout, proven on a spare port against the live DB (migrate = clean no-op; leaderboard read migrated balances), then `cloudbongos.service` flipped to the instance + engine and the ADR 0129 pull-timer repointed at the instance repo. Engine source stays authoritative at `example-owner/cloud-bongos` (not relocated); verified DB backup + retained old checkout/unit/deploy-script give instant rollback. Enabled org-wide deploy keys on the `cloudbongos` org so the box's read-only deploy key could attach.](<redacted>.md) | infra / Cloud Bongos |
220
- | 0141 | [Cross-site identity — cloudbongos.com becomes a federated identity provider (the "hub") so one platform account reaches any hosted project's builders hall, browser AND CLI, with no second GitHub sign-in (Goal 41 design of record; task [#1002082](https://cloudbongos.com/builders#/task/1002082); implemented across [#1002106](https://cloudbongos.com/builders#/task/1002106)–[#1002115](https://cloudbongos.com/builders#/task/1002115)). Core principle: **identity is federated, authority is not** — the hub carries identity ONLY (github_id, login, display name, avatar), never rank/credits/admission, so [ADR 0016](<redacted>.md)'s self-owned-authority boundary is preserved by construction. Federation is opt-in + reversible via a config switch (`auth.idp`): a hosted instance provisioned with `auth.idp` + `<PREFIX>_HUB_*` routes web sign-in through the hub's `/sso/authorize`→`/sso/token` (OAuth authorization-code, single-use 60s codes, server-to-server client-secret redemption) and CLI sign-in through the hub's device flow + an audience-scoped Ed25519-signed assertion verified offline at a new `/auth/device/exchange`; a self-hosted instance leaves `auth.idp` unset and keeps owning its own GitHub app (the direct-GitHub path, unchanged fallback). Adds a hub `platform-identity` module (accounts / sso_clients / sso_authorization_codes / projects / project_memberships / builder_project_activity), a project→hub membership check-in inside upsertBuilder (coarse role, never rank), a My Projects view with project-executed join/leave, a curated public Explore showcase, per-builder cross-project activity rollups, and an Archon-only cross-project scouting view (upgrading the coarse [ADR 0127](<redacted>.md) fleet gauges to a github_id-keyed store). Two credential types by design: per-client hashed secret (instance→hub server-to-server auth) + a hub Ed25519 keypair (CLI assertions, asymmetric so one instance can't forge for another). Was scoped as "ADR 0135"; that number was reassigned, so it lands here as 0141.](<redacted>.md) | auth / Cloud Bongos identity |
221
- | 0142 | [Project context is prose-only; the structured host-identity contract is branding (audit H11 / finding F7; task [#1002224](https://example.com/builders#/task/1002224), goal 1000042). Branding flows as a structured neutral⊕instance→validated-resolver contract because CODE reads those strings; project context (vision/world/visual-style in `docs/project-context.md`, scaffolded from the shipped `docs/project-context.template.md`) is deliberately narrative — read by AI agents, never parsed by code — so it stays prose-only with no parallel resolver/schema/env layer (YAGNI: no consumer; branding already covers code-read strings; the publish boundary + the CLAUDE.md/AGENTS.md stub already enforce the split at the file level per [ADR 0062](<redacted>.md) §7). A new host value that code must read goes in branding, not a new project-context resolver. Revisit only when code (not agents) must read project context as validated, structured data.](<redacted>.md) | core / host-split |
222
- | 0144-a | [Federated single-logout: hub-initiated back-channel session revocation (task [#1002238](https://cloudbongos.com/builders#/task/1002238), goal 41; follow-up to [ADR 0141](<redacted>.md)). ADR 0141 gave single sign-ON but not sign-OFF — a federated instance mints its own local session after the SSO handshake, so hub logout left every project still signed in. Owner chose **back-channel** (server-to-server) over front-channel (browser-driven, breaks under third-party-cookie blocking, incomplete if the tab closes) and soft-sessions (per-request hub re-check — too heavy, hard hub dependency). Core `POST /auth/logout` emits a `builder.logout` seam EVENT (no-op unless a module listens); the hub `platform-identity` module reacts by minting a **hub-signed EdDSA logout token** (`mintHubLogoutToken` — same envelope/key/~120s-TTL/aud-binding as the login assertion) per entered project and POSTing it to each `{origin}/api/gds/auth/backchannel-logout`; that core endpoint (dormant unless `idpConfigured()`, else 404) verifies via `verifyHubLogoutToken` against the hub pubkey and calls `revokeAllSessionsForBuilder`. The token carries only `{sub, purpose:"backchannel-logout"}` — **never** identity/rank (ADR 0016 boundary holds); the **`purpose` marker makes login assertions and logout tokens mutually non-interchangeable** (a login assertion lacks purpose → rejected as logout; a logout token lacks github_login → rejected as login). Entered-instance list = existing `<redacted>` (over-notify is safe — revoke is idempotent); fan-out is best-effort, per-instance 4s timeout, `Promise.allSettled`-isolated, never blocks/fails the logout (hub session revoked synchronously). No new schema, no doorway change; core `1.18.5→1.18.7`; re-vendors BOTH hub + federated instances. V1 = hub-initiated only (instance→hub propagation + a precise per-session ledger + unreachable-instance retry are follow-ups).](<redacted>.md) | auth / Cloud Bongos identity |
223
- | 0144-b | [Re-home the dev-box fleet onto the cloudbongos.com plane (native runners, local DB, isolated fleet tag)](<redacted>.md) | infra / dev boxes |
224
- | 0145-a | [A free "own-repo" hosted project tier can NOT be a bare `cloudbongos.com` subdomain running the owner's code — a pre-build security review found it unsafe two ways (owner design review, [task 1002249](https://cloudbongos.com/builders#/task/1002249); follow-up to [ADR 0141](<redacted>.md)/[0143](<redacted>.md)). (1) The hub `gds_session` cookie is `Domain=.cloudbongos.com`-scoped (so `builders.`/`status.` share one sign-in), so an owner-controlled `<slug>.cloudbongos.com` instance would receive every signed-in visitor's live hub token → account takeover; `httpOnly`/`sameSite:lax` don't help (same registrable domain = same site). The co-tenant instances that safely federate on cloudbongos.com subdomains (e.g. `demo.`) do so only because they run PLATFORM code, not owner code — so the precedent doesn't extend. (2) A control-plane-local `standalone` runs owner code on the hub box as `lars` with passwordless sudo in its `npm ci` deploy → root-on-hub (pre-existing, independent of federation). The federation protocol itself is sound (origin-bound, asymmetric Ed25519); the weakness is PLACEMENT + EXECUTION. **Decision:** a free own-repo tier is delivered ONLY behind (a) domain separation — free instances under a different registrable domain than the hub, so they never get the cookie — AND (b) per-project isolation (own droplet / container, distinct UID, `--ignore-scripts`, no passwordless sudo); only then is federation safe to extend. Owner chose this direction over the co-tenant free tier (safe/$0 but no own repo) and the honest-copy status quo. Build blocked on owner decisions: which separate domain, droplet-vs-container isolation, and the cost model (paid tier vs capped subsidy — [§4](../../CLAUDE.md)). Points 1&2 of the review (repo-step polish) were unrelated + already shipped ([task 1002247](https://cloudbongos.com/builders#/task/1002247)).](<redacted>.md) | auth / provisioning / Cloud Bongos identity |
225
- | 0145-b | [The Dev Box app becomes a branding-driven module (one source, per-instance builds)](<redacted>.md) | dev-box app / modules |
226
- | 0147 | [Carry ADR 0110's deploy-regen of generated docs to STANDALONE instances ([task 1002314](https://cloudbongos.com/builders#/task/1002314); the owner's "docs didn't transfer from primea"). The monorepo keeps its generated nav docs live because ADR 0110's regen is wired into `deploy-prod.sh`; standalone instances ([ADR 0108](<redacted>.md)) never got it — their bespoke pull-deploy regenerated nothing and `init`/`upgrade` only did the API docs, so cloudbongos.com's `docs/` was frozen with just the 3 API files. Two defects: (1) `gen-session-index.js` hardcoded the CORE root (`path.resolve(__dirname,'..','..')`) so on a standalone box it indexed `node_modules/@bongos/core`, not the instance — now `resolveInstanceRoot()`; (2) no standalone regen path. Fix: `upgrade.regenerateNavDocs()` + a `regen-instance-docs.js` CLI, wired at every refresh point (init seed, `bongos upgrade`, `provision.js` standalone deploy, and a bespoke `deploy.sh` one-liner); `INSTANCE_GITIGNORE` ignores the whole-file docs. Scope is `session-index` ONLY — the one gitignored, whole-file, instance-content nav doc; `repo-map` is core-scoped (empty for a code-less standalone; brownfield map = task 1001655), `file-map` + API docs are tracked/committed (file-map seeding at standup = follow-up), diagrams are the headless-browser exception.](<redacted>.md) | docs pipeline / Cloud Bongos |
227
- | 0146 | [Configurable reward mode — an instance chooses whether a shipped task pays BOTH reward streams or ONLY the cost-plus one ([task 1002270](https://cloudbongos.com/builders#/task/1002270), goal 1000037; follow-up to [ADR 0120](<redacted>.md)). A shipped task pays two streams onto one balance (the [ADR 0054](<redacted>.md) "two conventions, one ledger" tension): the per-task author ESTIMATE (`credits_reward` × kind multiplier — unitless, [ADR 0023](<redacted>.md)) AND the cost-plus SESSION reward (`round(true_cost_usd × 1.20)`, 1 credit = $1 — ADR 0054). New `branding.reward.mode`: `"cost-plus-and-estimate"` (DEFAULT — pay both, exactly today's behavior; every existing instance unchanged on upgrade) or `"cost-plus-only"` (the cost-plus reward is the SOLE equity reward; the per-task estimate PAYOUT is withheld and its idea-promotion slice cascades to 0). Resolved by a pure `rewardPolicy()`; gated in the single per-task credit writer (`insertMultipliedConfirmCredit`) — entirely inside the economy module, ZERO lifecycle changes. `credits_reward` still rides the task row for display + the no-zero-reward claim gate; only the payout is suppressed. Unknown mode → default (a typo can't silently zero rewards). Cloud Bongos selects `cost-plus-only`; achievements + the net-negative +1 bonus are out of scope. Backward-compatible + reversible — no clawback, no migration.](<redacted><redacted>.md) | GDS / economy |
228
- | 0148 | [Task-scoped box source access (rank-scope → claim-scope)](<redacted>.md) — paused by ADR 0193 | dev boxes / source scope |
229
- | 0149 | [`bongos dev` — a one-command LOCAL launcher for a standalone instance repo (owner ask, 2026-07-19: "we need a better option to stand up the local host"). A freshly-created project's `config/branding.json` points at `http://localhost:<port>` — configured to run on the owner's machine — but the platform shipped no command to START it there; `bongos onboard` provisions a CLOUD server and Docker serves a vanilla brand on `:3000`. Standing it up by hand was a six-part assembly (Postgres → createdb → migrate → PORT/PGDATABASE/PGHOST/OAuth env → server → login) with three silent mismatches: the server reads PORT from env not branding (platform-server.js:44); the pool DB name collapses to the OS user while migrate.sh defaults to `example` (pool.js:47 vs migrate.sh:52) unless PGDATABASE is set for BOTH; and node-pg defaults to the Linux peer socket `/var/run/postgresql` (pool.js:46), absent under Homebrew on a Mac. `bongos dev` does the whole thing idempotently from one source of truth (branding): ensures Postgres, createdb, migrate (`MEDUSA_GDS_ONLY=1`), starts the server on the branding port, and signs in. The background server is MANAGED (pid/log per instance) with `bongos dev stop`/`status` — the fix for the orphaned-server accident (a stale `:3005` answering a new project's login). Pure resolution + process mgmt in `scripts/gds/dev-lib.js` (unit-tested `tests/dev_lib.mjs`); orchestration in `scripts/gds/dev.js`. Alternatives rejected: cloud dev box (needs hosting + cost — kept for remote work), Docker (vanilla brand/wrong port — kept for Windows), recipe-only (leaves the six steps). Not bundled into the standalone on-ramp binary (needs a real checkout).](<redacted>.md) | Cloud Bongos / dev experience |
230
- | 0150 | [Box first-boot bring-up on vendored instances — resolve `infra/` from the instance root + fail loud on empty user-data (spike BV1.R97, [task 1002385](https://cloudbongos.com/builders#/task/1002385); C2 of goals 1000051/1000052, "make cloudbongos boxes stop coming up bare"). Root cause: `box.js` sets `REPO_ROOT` two dirs up from itself, which on a VENDORED instance is the `@bongos/core` package — but the R89 packaging split kept `infra/` (the DO cloud-init template + inlined `box-source-fetch.sh`/cloudflared/ttyd/heartbeat scripts) as INSTANCE content, not in the core publish-manifest. So `readCloudInit` reads a missing template, its `try/catch` returns `null`, and the caller's only guard (`0 > 64KiB`) passes → the box is created with EMPTY user-data and boots bare Ubuntu (no clone, no toolchain, no terminal); `fileGzB64` is likewise fail-soft (warns, returns `''`) so a partial `infra/` half-wires. Live re-confirmed 2026-07-19: instance-root `infra/` present, vendored-core `infra/` gone (the [ADR 0144](<redacted>.md) node_modules workaround wiped by a reinstall), fleet idle so the bug is latent-but-live (next provision boots bare). Fix (R98, [task 1002386](https://cloudbongos.com/builders#/task/1002386)): a `resolveBoxInfraDir()` locating `infra/` via [ADR 0108](<redacted>.md)'s `resolveInstanceRootExplicit()` (cloudbongos `bongos.config.json` = `{"instanceRoot":"."}`) + an env override + a walk-up-out-of-`node_modules` fallback, AND fail-loud backstops (throw on missing template; abort provision on empty user-data). R99 ([task 1002387](https://cloudbongos.com/builders#/task/1002387)) then ensures the toolchain builds on the box. Reinstall-proof + portable; core-only, no migration/doorway change.](<redacted>.md) | GDS / dev boxes |
231
- | 0151 | [Governance: **permissions are the atom, ranks become seeded roles** — the foundation of the BONGOS-V1 Governance goal (1000030), spike BV1.R90 ([task 1001838](https://cloudbongos.com/builders#/task/1001838)); settles the model in an ADR before R93–R114 build. Authority stops being an all-or-nothing rank threshold ([ADR 0016](<redacted>.md)/0018/0034) and becomes granular **permissions** (code constants that map 1:1 to real routes/surfaces — a permission no route consumes doesn't exist), bundled into assignable **roles**; the four live ranks become four seeded roles whose grants reproduce today's thresholds EXACTLY (the compat seed — cumulative nested supersets, so the resolver's UNION preserves the at-or-above threshold), and `builders.rank` stays as the identity/graduation column. A per-request **uncached** `resolveBuilderPermissions()` (kernel port, preserving ADR 0016's instant-demotion) + a `requirePermission()` gate that coexists with `requireRank` via the seed → gates migrate route-by-route, no flag day. The **complete authority model** (C3) represents four axes as first-class types, not rank special-cases: rank-seeded roles, machine **principals** (`system_role`=bfg/discord_bot, exact-identity gate, never UI-grantable), **session-scopes** (box tokens, deny-by-default allow-list intersected with the builder's roles), and **ownership-scoped** perms (`X.manage.own` ≠ `.any`; goal owner/manager per [ADR 0106](<redacted>.md)). The safety keystone (C6): every permission carries a **`system` flag** — `system:true` (deploy, protected-path, rank-set, version-close, grade-bypass, cross-tier edges) is **non-grantable** and LOCKED in the tab, so no configurable role can hand out the crown jewels; and the today-hardcoded `PROTECTED_GLOBS`/`EXPECTED_RANKS` map becomes a **governance-owned protected-surface registry** that the OUT-OF-BAND substrate (pre-push hook, grader pre-pass, `main-audit.js`, gate-review — [ADR 0043](<redacted>.md)) READS but never moves in-process: **policy owns / substrate enforces** — the tab can re-map the floor but a forced `system` grant still can't deploy (abuse-matrix + live-demo proofs, R112/R114). Capability in **core** (`modules/governance/`, default:true); role templates are per-instance **config** (`config/governance.json` over `config/governance.neutral.json` over env, mirroring `src/branding.js`), live assignments in the DB. Collapses the ~6 duplicated numeric rank-tier maps (the Builder-25-incident drift class) behind the resolver. Zero day-one behavior change (parity test R103); one deliberate fail-closed divergence (unwired divine ranks map to no role instead of latently inheriting Archon). Includes the full enumerated permission catalog (every key + `system` flag) grounded in the 2026-07-24 authority sweep. No new recurring cost. **Vocabulary amended by ADR 0174 (2026-08-16): governance→government, seeded roles→ranks; the decisions are unchanged.**](<redacted>.md) | governance / permissions / Cloud Bongos |
232
- | 0152-a | [The landing gate is served by the **core app**, not by the front proxy — the cutover step of the landing migration ([task 1002411](https://example.com/builders#/task/1002411), L05 of goal 1000035). The reference instance's orb-gate front page was `file_server`'d by Caddy straight out of a checkout of an UNRELATED product repo (`root * <checkout>/brand/cloud-bongos`), so the landing had two sources of truth, no deploy path of its own, and a cross-product coupling the core could not express. [L01](https://example.com/builders#/task/1002407) had already ported the bundle into the core as `public-landing/` and [L02](https://example.com/builders#/task/1002408) had already taught `platform-server.js` to serve it (`/` -> 302 `/gate/`, `/gate/` serves the index raw, `express.static` covers the assets) — so by the time L05 came up, BOTH halves of its original plan ("give the landing (a) its own Caddy config and (b) its own deploy script") had dissolved. **Decision:** the app owns the landing end to end; the proxy drops `root` + `file_server` and keeps exactly three jobs on that surface — TLS, an optional pre-launch `basic_auth` lock, and the `@hidden` guard that keeps `gate/README.md` unreachable. No separate Caddy config to write (the resulting block IS the generic pure-proxy shape `provision.js`'s `caddyBlock()` already emits per instance, [ADR 0111](<redacted>.md) §4; host-specific hostnames/certs stay on the host per [ADR 0062](<redacted>.md) §7) and no separate deploy script to write (the landing ships INSIDE the pinned core package, so it rides the instance's existing pull-deploy — one artifact, one version, one path). Keeps `redir / /gate/ 302` because Caddy orders `redir` before `basic_auth`, so "/" behaves identically before and after, and the app issues the same 302 once the lock is lifted. Cost accepted: the landing now sits in the app's request path (a dead app = a dead front page — but the gate's whole purpose is to reach the app), and a landing tweak now needs a core release. Verified on the live instance: identical status codes across the apex and both vanity doors before/after, relative redirects (no origin leak), and a **severance test** — with the foreign repo's landing folder moved aside entirely the site still served the byte-identical gate, proving the dependency is cut rather than merely unused. Retiring the foreign copy is L06 ([task 1002412](https://example.com/builders#/task/1002412)).](<redacted>.md) | platform serving / instance infra |
233
- | 0152-b | [**Metic gains task abandonment, paired with an un-abandon at the same rank** (owner directive 2026-07-27: "Metics should also be able to abandon tasks"; [task 1002476](https://cloudbongos.com/builders#/task/1002476)). [ADR 0090](<redacted>.md) moved task authoring (create/edit/promote/deps/criteria) down to the trusted working rank but kept `POST /tasks/:id/abandon` Archon-only as "terminal scope removal" — so the rank that runs idea-triage, seeds the tasks, and shapes them still had to hand every duplicate/moot/superseded chore back to the owner to retire. That hand-back is janitorial, not adjudication. The real hazard was never the grant: abandon was **one-way through the API** (`/promote` took only backlog/blocked, `PATCH` refuses `status`), so a mis-retire needed hand-SQL on prod, and an abandoned task is invisible in every claimable list and every version rollup (migration 017 excludes it from `version_progress`) — nobody notices until the work silently doesn't happen. So the gate moves to `requireRank('metic','archon')` **and** `/promote` gains a third source state, `abandoned → backlog` (never straight to ready — a retired task re-enters un-workable and takes a second deliberate promote), backed by `db.restoreTask`, the mirror of `abandonTask`, which clears the `'Abandoned: <reason>'` stamp and only that. Unchanged: the shipped-task and active-claim refusals, idempotency, and the `audit_log` record of who retired what and who brought it back. The `EXPECTED_RANKS` lifecycle pin moves in lockstep (a *further* downgrade stays a hard CI fail); under [ADR 0151](<redacted>.md) the `task.abandon` floor moves archon→metic, landing as `governance_003` because the compat seed is run-once — purely additive, and the drift guard now compares the **union** of grant migrations to the catalog and fails loudly on a future revocation.](<redacted>.md) | GDS / permissions |
234
- | 0153 | [**Sequential chain execution — one kickoff walks a dependency chain** ([task 1002484](https://cloudbongos.com/builders#/task/1002484)). [ADR 0015](<redacted>.md) made `dependencies[]` load-bearing and auto-promotes a task's dependents `backlog`→`ready` on ship — but nothing *picks the unblocked task up*, so on the live instance **146 of 303 open tasks sit behind a predecessor** and a 24-task planning seed needed 24 separate human kickoffs. The only "sequential" affordance was a clipboard prompt on the Tasks board (`<redacted>`), which is text rather than execution and cannot even select the gated successors, because they are still `backlog`. Requested independently by two builders (ideas 1000461, 1000398). **Decision:** a chain runner with **no schema change** (the polymorphic `dependencies` table + `task_is_fully_unblocked` + the promote-on-ship trigger already encode the order; a `chain_id`/queue column would be a second, drift-prone source of truth) and **no new claim guard** (one-active-claim-per-session is keyed on the *working tree*, so ship-before-next-claim is already the enforced shape — and batch-claiming a chain is impossible anyway, since successors fail `DEPS_NOT_SHIPPED` until their predecessor ships; the ship IS what unblocks the next step). Deterministic plumbing (`scripts/gds/sequence.js` — scope, topological order, halt matrix, all read off the task row) is split from judgement (`.claude/skills/builder-sequence/`, which defers to `/builder-claim` + `/dev` + `/builder-ship`). **Halt, do not skip** (owner decision): on `needs_migration`, `security_sensitive`, `protected_path`, `rank_floor`, `thin_spec`, `judgement_kind`, `not_claimable`, `deps_unmet`, `in_flight`, or `cycle`, the runner stops and reports with the task still claimed — because in a dependency graph a skipped task is a *predecessor*, so skipping strands everything behind it while the run keeps posting apparent progress. Not built: chains as a priced claimable unit on the board (idea 1000398), which is additive.](<redacted>.md) | GDS / methodology |
235
- | 0154 | [A goal's done-when criteria are authored by that goal's owner or managers](<redacted>.md) | GDS / goals |
236
- | 0155 | [**Adopting a private repo: widen the onboarding OAuth scope to `repo`** ([task 1002561](https://example.com/builders#/task/1002561)) — amends [ADR 0143](<redacted>.md) §1 by taking the exit 0143 itself named (“revisit to `repo` only if private support is needed”). Standing up a project on an EXISTING PRIVATE repo died at the provisioner’s scaffold push with GitHub `403 Write access to repository not granted`: `scopeForWebFlow` requested `public_repo`, which grants write to **public** repos only. Confirmed as scope and not credentials three ways — the live `github_oauth_tokens` row was `public_repo,read:user` and still VALID during the failure (a dead token 401s; this 403 text is GitHub’s canonical scope-insufficient reply); every greenfield project worked only because `createUserRepo` hardcodes `private:false`, so the platform had only ever pushed to repos it had just made public; and the picker already rendered a **“Private” badge** for a case `public_repo` made unreachable. Owner chose (B) widen-and-keep-pushing over (A) never-touch-the-user’s-repo (user runs `bongos init --adopt` + pushes; needs a deploy key for the instance to *run*) and (C) public-only, accepting the cost this reverses: the persisted token can now write to **every repo the user owns**. Containment from 0143 is unchanged and is what makes it tolerable — encrypted (secret-box), **1h TTL**, lazily swept, requested ONLY on the wizard’s `?repos=1` path (hall + CLI stay `read:user`), never stored when secret-box is off. Three guards ride along: (1) widening the *request* does not upgrade *stored* tokens, so new `grantedScopeCoversRepos()` gates `resolveRepoToken` and a surviving `public_repo` row takes the existing `repo_scope_required` (403) reconnect path instead of 200-ing while **silently omitting the user’s private repos** — it PARSES GitHub’s comma-separated grant rather than substring-matching, since `public_repo` *contains* “repo” and a naive match accepts exactly the stale token being guarded against; (2) `createUserRepo` keeps `private:false` — now a **product** choice (instances are open-source/capture-resistant by design), deliberately not a parameter, test-pinned so the widen can’t change what we create; (3) consent copy states the breadth in our own words first, since GitHub words `repo` as full control of all repositories. A central **GitHub App** (fine-grained `Contents:write` on only the selected repo) remains the principled fix and this makes its case stronger — a future ADR adopting it supersedes this. **This alone does not make adopt work:** provisioning is independently greenfield-only (`git init` a fresh tree + push to `main`, which an existing repo rejects as unrelated history) — [task 1002562](https://example.com/builders#/task/1002562) fixes that and, because the wider scope makes a mistaken `--force` a real data-loss route on a user’s repo, carries a test asserting the adopt push never forces; [1002563](https://example.com/builders#/task/1002563) verifies live.](<redacted>.md) | onboarding / auth / provisioning |
237
- | 0156 | [**A builder's own display name wins, always** ([task 1002568](https://cloudbongos.com/builders#/task/1002568)). A builder renamed themselves in hall Settings, the save worked, and the name reverted to their GitHub account name at the next sign-in. `upsertBuilder` (`src/bongos/db-kernel.js`) is named for account creation but runs on **every authentication**, and its `ON CONFLICT (github_id) DO UPDATE` re-set `display_name = EXCLUDED.display_name` unconditionally — where `EXCLUDED` carries whatever the caller just read off GitHub. All four login paths feed it (`pollDeviceFlow`, `completeWebFlow`, `completeIdpFlow`, `completeDeviceExchange`), so CLI re-auth alone was enough to undo the save the same day. **Decision:** the row records *who last set the name*. A new core column `builders.display_name_source` (`'github' | 'user'`, migration `core_197`) is stamped `'user'` by `setBuilderDisplayName` — the only self-rename path — and the upsert's conflict branch guards the assignment with `CASE WHEN builders.display_name_source = 'user' THEN builders.display_name ELSE EXCLUDED.display_name END`. GitHub keeps seeding and re-syncing the name for everyone who never renamed; it never overwrites one the builder chose. `github_login`/`avatar_url` re-sync unchanged. **Rejected:** the one-line fix of deleting the assignment entirely — it freezes `display_name` at signup for *everyone*, so a GitHub rename (or a bare login seeded because the account had no GitHub display name) would never propagate again; that trades a visible bug affecting the few who renamed for a silent one affecting the many who never opened Settings. Not surfaced in `GET /me/display-name` or the OpenAPI spec (internal persistence state, no UI consumer). The federated hub's `platform_identity_accounts` needs no matching guard — the `CASE` runs regardless of what `EXCLUDED` carries, and that table is identity-only with no self-service editor. `tests/display_name.mjs` previously covered only the pure validator, which is why this shipped; it now reads the real SQL and asserts both directions.](<redacted>.md) | GDS / builder experience |
238
- | 0157 | [**Archon is rank and identity only; the operational block delegates to Metic** ([task 1002584](https://cloudbongos.com/builders#/task/1002584), owner decision 2026-08-05). Archon had accumulated **50 route gates**, almost none about *authority* — reading the roster, listing dev boxes, viewing the audit log, adjudicating a vuln report, tearing down a droplet. Each arrived Archon-gated by [ADR 0016](<redacted>.md)'s fail-closed default for new privileged routes; nothing came back to lower them, so “fail closed” became “the owner is the only operator”. Generalises the one-power delegations of [ADR 0090](<redacted>.md) and [ADR 0152](<redacted>.md) instead of relitigating route by route. **20 permissions move archon→metic** — the governance pages (`/watch`, `/harbor`, `/gate`), session search, the dev-box + provisioning fleets, security adjudication, the audit log, roster reads, cross-builder memory reads, project curation, budgets, idea ratification, override decisions, release-on-behalf, criterion unsatisfy, goal reorder, version create, newcomer restock, peer-vote tally, Discord channel ops, autonomy precheck. **Only possible as a 1-file change** because BV1.R104/R105 had just migrated the gates to `requirePermission`, so the floor is declared once in `catalog.js` — the same decision was a 41-call-site diff a day earlier, and that is the concrete [ADR 0151](<redacted>.md) payoff. **13 stay Archon:** rank (`builder.rank.set`), identity (status/admission/session-revoke), and — refusing the literal reading of the directive — **four escalation keys** (`task.confirm.grade_bypass`, `gate_approval.decide`, `goal.scope.manage`, `governance.manage`) that form a chain to self-promotion: scope a goal into the permission core → claim a task there (protected paths already floor at *metic* per [ADR 0043](<redacted>.md)) → confirm past the failed grade → approve your own red CI gate → it lands on `main`. Moving all 20 would have made the delegation **self-reversible**, so keeping rank Archon-only depends on keeping those four Archon-only; any future delegation must re-run that check. Also held back: `version.close` (paired with rank as the powers that never delegate) and `dependency.cross_tier.manage` (`system: true` — clearing that flag to move it would widen what any CUSTOM role can hold, a second-order change nobody asked for). Mints **`gate_approval.read`** so the Gate page opens at Metic while approving stays Archon, and finishes the three reads R104/R105 skipped — otherwise the read would be **stricter than the write** (a Metic able to confirm a vuln report but not list one). Route pins move from ranks to `perm:` atoms so a floor change needs no pin edit. **Two flagged consequences:** `/security/docs` (attacker-usable exploit detail) is now Metic-readable, since it shares `security.report.adjudicate` — the one delegation widening who may *read* rather than *act*; and [ADR 0154](<redacted>.md)'s “the backward ratchet stays Archon” carve-out is superseded (satisfy/unsatisfy stay separate atoms, so confirming still never confers un-confirming). ADR 0016 is untouched end to end — floors move *inside* the boundary, never the enforcement point.](<redacted>.md) | governance / permissions |
239
- | 0158 | [**The choke-point verdict taxonomy, and why a verdict never executes itself** ([task 1002598](https://cloudbongos.com/builders#/task/1002598)). [ADR 0157](<redacted>.md) delegated 20 permissions on one observation — each had been Archon-gated by [ADR 0016](<redacted>.md)'s fail-closed default, *never by a decision* — but applied that reasoning once, to a known backlog, without recording it as a **method**. Two things now make the question tractable: a **second Archon exists** (until 2026-08-06 "needs an Archon" and "needs Lars" were the same sentence, and a large share of past escalations reclassify on that distinction alone), and post-[ADR 0151](<redacted>.md) a floor is declared once, so classifying exhaustively is cheap. **Decision:** six ordered verdicts, first match wins — GAP · UNINTENDED-MISSING-SURFACE · DRIFT · STRUCTURAL · INTENDED · UNINTENDED-OVERTIGHT. Order is load-bearing: GAP leads because arguing a wall's rank while nothing enforces it is the costlier mistake, and UNINTENDED-OVERTIGHT is last because its remedy is the only one that *weakens* a boundary. DRIFT is never terminal — a DRIFT row must name what it collapses into. **The load-bearing rule: a verdict is advisory and never self-executing.** Recording UNINTENDED-OVERTIGHT is not authorization to lower a floor; that still needs an owner decision + `catalog.js` + a migration (the `governance_002` seed is run-once, so an edited `floor:` never reaches a migrated DB) + an ADR. A document that could move a floor by asserting a verdict would be privilege escalation through markdown — the audit describes, the database enforces. Every row carries a rigor marker (`obs` / `read` / **`absent`** / **`unver`**); the last two earn their own names because the two highest-severity findings in the first map are *absences* (a pre-push hook never once in git history; an "authoritative" audit script with no caller) and the sharpest structural one is an `unver` — the layer [ADR 0043](<redacted>.md) designates authoritative lives entirely outside the repository that documents it, so the trust model cannot be audited from the artifact that states it. Two methodological constraints, both learned the expensive way: **an Archon cannot audit the ownership walls** (`gateTaskOwnership` short-circuits on `rank === 'archon'` before the ownership lookup, so an Archon probe passes all 7 routes and observes nothing — which is why the single-Archon era *hid* a class of wall rather than merely queueing it), and **send two bodies per write** (16 of 29 in-handler-gated routes return 400/422 on `{}` before the authorization check, so a `{}`-only sweep concludes there are no in-handler walls). Produces [`docs/governance/<redacted>.md`](../governance/<redacted>.md). ADR 0016 and 0043 untouched — this adds vocabulary for *describing* the boundary; it moves no floor and changes no enforcement. **Rejected:** generating the map from the route census (a census answers "where is the gate", never "should it be there", and generating it would imply otherwise); folding verdicts into `canonical-permissions.md` (that file mirrors the enforced model — mixing judgments *about* it into the description *of* it is how a doc starts reading as authority); auto-filing a task on UNINTENDED-OVERTIGHT (a queued privilege change nobody decided); and collapsing to three verdicts (it merges GAP into UNINTENDED-OVERTIGHT, inverting the remedy — an unenforced wall needs a gate *added*, an over-tight one needs a floor *lowered*).](<redacted>.md) | governance / methodology |
240
-
241
- | 0159 | [**Merge-gate reconciliation: required checks match reality, CODEOWNERS becomes real, protection stays the owner's switch** ([task 1002611](https://cloudbongos.com/builders#/task/1002611)). Findings D-12 + G-5 from the choke-point map: `gating.js` hardcoded three required status checks (`unit`, `trufflehog`, `gate-review`) of which two have never had a workflow — running `gating.js on` would have required contexts nothing can report, blocking every merge indefinitely instead of gating them — and CODEOWNERS was described as a live control by ADR 0042, `protected-surfaces.json`, `publish-manifest.js` and gating.js itself while not existing anywhere in the tree. Now that the `unit` lane exists (task 1002610), `DEFAULT_PROTECTION` requires ONLY `[unit]`; the future contexts are fixed as `secret-scan` + `gate-review` (the `CORE_EXPECTED_CHECKS` names — a context is named for what it asserts, not the tool asserting it) and return when their ADR 0058 workflows ship; CODEOWNERS becomes a real root file scoped to `gate-review.js`'s `HARD_FLOOR_GLOBS` + gating.js (inert until protection is on); branch protection itself stays OFF — flipping it remains the owner's explicit act, this only makes the switch safe. Rider: the unit lane is hardened so `unit` is actually requirable — `fetch-depth: 0` (depth-1 broke `tests/main_audit.mjs` on the lane's first run), two clean-env-red tests parked in `KNOWN_FAILING` with diagnoses + exit tracked in [task 1002617](https://cloudbongos.com/builders#/task/1002617), and `publish_manifest` (the public-mirror boundary test) fixed in-claim rather than parked — its ≥7-host-tests non-vacuity floor went stale when ADR 0107 moved host tests into `modules/<key>/tests/`, replaced by a regex specimen self-test. Rejected: deleting `GATE_WORKFLOWS`' forward-looking names (the switch handles absence gracefully; deleting them un-covers the workflows the moment they ship) and watering down the four CODEOWNERS descriptions instead of creating the one file.](<redacted>.md) | governance / build pipeline |
242
-
243
- | 0160 | [**The authoritative main audit gets a repo-verifiable caller** ([task 1002612](https://cloudbongos.com/builders#/task/1002612)). Findings G-3 + G-8, honest limit S-1: `main-audit.js` — the post-push layer [ADR 0043](<redacted>.md) designates authoritative — had two declared invokers that never existed in this git history, and `audit-authorship.sh` (the no-hardcoded-Archon-identity enforcer) had no caller either and turned out red with 340 pre-existing hits on untouched main (idea 1000653). Now a `main-audit` workflow audits every push to `main` over exactly the pushed `before..after` range (clamped-window fallback); the roster comes from the public leaderboard when no session exists (server-authoritative, login-only keying, an absent builder resolves as unresolved — never falsely trusted); new `--require-roster` makes an empty roster a loud DID-NOT-RUN exit 2 (an audit that cannot fire a blocker must not exit green); the lane runs non-strict per the audit's own zero-false-positive calibration; and `audit-authorship.sh` rides as an advisory job until the 1000653 triage. Detective lane, not a merge gate — deliberately outside the required PR contexts. Rider: the known-failing ratchet caught task 1002611's publish_manifest fix exactly as designed — entry removed, ceiling ratcheted 3 → 2, remaining entries re-owned to [task 1002617](https://cloudbongos.com/builders#/task/1002617), idea 1000542 superseded. Rejected: a GDS token secret (silent-fail dependency the devcontainer lane warned about), `--strict` in CI (permanent red rots into wallpaper), enforcing authorship before triage.](<redacted>.md) | governance / build pipeline |
244
- | 0161 | [**Publish on merge: every merge to core `main` auto-publishes `@bongos/core`** (task 1002620). Supersedes ADR 0137's manual owner-gated publish after its own "when to revisit" clause fired twice in three weeks: 171 commits/10 days, then 104 commits/5 days, sat on `main` reachable by no pin while every layer reported success. A `publish` workflow rides behind the required `unit` check (`workflow_run`, success-gated): when HEAD's `CORE_VERSION` is already on npm, CI bumps a PATCH via the shape-preserving `bump-version.js` (+ changelog line; pinned by `tests/bump_version.mjs`) and pushes the carrier commit; `package-core --self-verify --publish --live` builds through the fail-closed no-leak gate; the release is tagged `core-v<v>`. A MINOR/MAJOR stays a human in-task edit — it decides what patch-channel subscribers receive unattended (ADR 0136), a call CI must not make. Token custody knowingly reverses (owner-accepted): `NPM_TOKEN` write + `RELEASE_PUSH_TOKEN` PAT as Actions secrets, mitigated by the lane shipping INERT behind the `PUBLISH_ON_MERGE=1` repo variable — an owner kill-switch symmetrical to `gating.js`. Idempotent by version-compare; npm duplicate rejection is the backstop; queued no-cancel concurrency collapses merge races.](<redacted>.md) | build pipeline / distribution |
245
- | 0162 | [**Review gates retired: rank-consistent CI checks only on `main`** ([task 1002635](https://cloudbongos.com/builders#/task/1002635); owner directive, idea 1000657). Amends ADR 0159. Within a day of task 1002619 arming the merge gates, the code-owner review requirement had stranded two graded-PASS wave-4 ships (PR 28 sat BLOCKED behind a review only the repo-owner account could give) and then blocked the fixes for the strands it caused — structurally, not incidentally: GitHub can only express the gate as *accounts* (no rank concept, no self-approval), so on a solo-archon instance it degenerates into "one named login must click every gate-surface PR" — identity-pinned authority, against ADR 0016's rank principle and the remediation's own goal. Decision: branch protection carries required status checks ONLY; `gating.js` `DEFAULT_PROTECTION` sets `required_pull_request_reviews: null` and `cmdOn` normalizes BOTH restore paths so a pre-retirement snapshot cannot resurrect the gate (`tests/gating_review_retired.mjs` pins it with a counter-example); the CODEOWNERS *file* is deleted while its *glob* stays in the hard-floor lists so reintroducing it is itself gate-flagged. Accepted trade, chosen explicitly by the owner: no pre-merge human approval on safety-machinery changes — the unit check, the fail-closed gate-review sandbox, the grader panel, and the every-push main-audit are the net, catching a compromised session's merge after the fact rather than before.](<redacted>.md) | governance / build pipeline |
246
-
247
- | 0163 | [**Pre-197 display names: a transitional 'legacy' source, resolved at first sign-in** ([task 1002643](https://cloudbongos.com/builders#/task/1002643); idea 1000659). Amends [ADR 0156](<redacted>.md), whose migration defaulted every pre-existing row to `'github'` — false for builders who renamed after their last sign-in but before 197 landed, so their names kept reverting. No audit trail exists to backfill precisely, so `core_214` marks every pre-197 `'github'` row `'legacy'` (scoped by 197's `applied_at` on the instance — portable) and the upsert resolves each at its first sign-in: stored = incoming GitHub name → provably GitHub-derived → `'github'`; differs → treated as builder-set → kept, stamped `'user'`. Accepted misclassification: a GitHub-side rename in the window freezes a stale label (visible, self-fixable in Settings) — preferred over silently destroying deliberate renames. Rejected: `display_name <> github_login` heuristic (freezes nearly everyone), comparing forever on all `'github'` rows (turns every future GitHub rename into a frozen name).](<redacted>.md) | GDS / builder experience |
248
- | 0164 | [**The implausible-pass guard reads substance + evidence; the Metic+ honor-system residual is explicit** ([task 1002658](https://cloudbongos.com/builders#/task/1002658); grader-hardening goal 1000056). The guard fired only on `issues.length===0` — one fabricated `{}` defeated it, its magnitude inputs are client-supplied, and every in-sample firing (eval 2-for-2, tasks 1002643 ×2, 1002663) was a false positive on an honest unanimous zero-issue panel PASS. Redesign: count only SUBSTANTIVE issues (known severity + non-empty summary — padding trips the guard regardless), and waive the zero-issue pass when `signals.workers` carries the full &ge;3-worker panel shape, stamped `implausible_pass_waived` for /grade-audit. The residual is named, not hidden: every grade-time input is client-authored, so plausibility at grade time is honest-mistake hygiene; the binding authorities on the real diff are land-time (CI unit, gate-review, main audit) — Metic+ grade-time honesty is accepted honor-system per this ADR. The ADR-0042 CI grade-gate (land it vs record permanent deferral) goes to the owner as a blocker, not decided here. No approval step added or removed (ADR 0162).](<redacted>.md) | GDS / grading |
249
- | 0165 | [**Instances are hookless by default: no scaffolded `.husky/`; local hooks stay an instance-owned opt-in** ([task 1002641](https://cloudbongos.com/builders#/task/1002641); idea 1000655). Task 1002608 delivered the ADR 0043 Layer 1 pre-push gate for the CORE repo, but nothing materializes `.husky/` on a scaffolded standalone instance (`publish-manifest.js` excludes it by design). Decision: that is the accepted posture, not a gap. Sub-Metic instance builders hold no push credential at all (server-mediated publish, ADR 0053/0055) — a client pre-push hook guards a push that cannot happen; a client hook is not a security boundary (ADR 0016); the core cannot maintain hooks it deliberately does not ship (scaffolded hooks would be born orphans — the audit-authorship rot lesson, task 1002640); and ADR 0162 puts gates in CI, rank-consistent and hands-free. Secrets hooks follow the same rule (the PR-time CI secret scan enforces). The opt-in stays real and instance-owned: an instance commits its own `.husky/` and the core's `install-git-hooks.js` (already run by `/builder-setup`) activates it. Rejected: scaffolding wrappers at init, an `--with-hooks` flag (speculative), treating hooklessness as a regression. Revisit if an instance model has builders pushing directly with their own credentials.](<redacted>.md) | provisioning / trust boundary |
250
- | 0166 | [**Module maintenance & deprecation policy: upkeep is a declared `maintenance` block, and `bongos upgrade` surfaces rot in DISABLED modules too** ([task 1001773](https://cloudbongos.com/builders#/task/1001773)). Implements [ADR 0107](<redacted>.md) §7, which named the catalog-rot problem and deferred the mechanics. `module.json` gains an optional-in-schema, required-to-upstream `maintenance` block — `status` one of `core-maintained` / `maintained` / `deprecated` / `orphaned`, plus `since` / `removeAfter` / `successor` / `note`. Each status owes something the validator enforces: `maintained` requires a named top-level `maintainer` (an unnamed owner IS the orphaning failure), a sunset status requires a deprecation path, and `removeAfter` is rejected on a non-sunset status. `orphaned` is deliberately first-class — a policy that only permits happy statuses gets satisfied by lying. The load-bearing fix: accepted modules ship `default:false` (§6), so every rotted catalog entry is DISABLED by construction — and `preflightModules()` returned only `blocking`, discarding `warnings`, so a real `bongos upgrade --to X` reported `"all enabled modules satisfy the target"` while saying nothing about them. It now returns + prints `warnings`/`deprecations`/`expired` with the maintainer NAMED, before the blocking check returns (so a refused bump reports them too). Enabled mismatches still refuse; disabled ones still never block — report, don't block, because blocking an instance's bump on a catalog entry it never opted into makes the shared catalog a liability. `bongos module check`/`submit` refuse an undeclared posture or an already-sunset module; `GET /modules` + the hall Modules tab show an `upkeep:` chip (incl. `undeclared`); all 17 bundled modules stamped `core-maintained` with a unit test holding the line. Rejected: blocking on disabled rot, a required manifest field (breaks every existing manifest), DB-tracked upkeep (must travel with the module, ADR 0107 §2), auto-deprecating on incompatibility (a fact is not a decision), a separate MAINTAINERS file.](<redacted>.md) | modules / upstreaming |
251
- | 0167-a | [**The module catalog: `source` (which tree ships it) and `provenance` (who built it) are two axes** ([task 1001772](https://cloudbongos.com/builders#/task/1001772) — the last task of goal 1000029). Closes [ADR 0107](<redacted>.md) §2/§3: a browsable catalog of enable-able core modules, each with author + origin credit, in the hall AND via `bongos module list --catalog`. The bug it fixes: the Modules tab derived its own/core-loaded split from the manifest's `origin` alone, but attribution is OPTIONAL and core's own modules declare none (nothing upstreamed them) — so on a split instance every core module read as *built here* and the catalog you could opt into read as empty. One field was answering two questions. Fix: a shared model (`src/module-loader/catalog.js`) that both the hall route and the CLI build from, carrying `source` — read STRUCTURALLY from the loader's discovery root ([ADR 0108](<redacted>.md) §1: core package vs instance tree), falling back on coincident roots to whether `package.json` names the core — beside the unchanged `provenance` credit axis. Plus `credit: {author, origin, inferred}`, which fills a core module's blank attribution with the platform name and MARKS it inferred (`*` in the CLI, a hoverable `<abbr>` in the hall) so it never reads as a claim the author typed. The tab now sections by `source` ("Module catalog" / "Built by this project"), gains a search box, and draws "Submit for review" from instance-source modules only. Rejected: stamping author/origin into 15 core manifests (repetitive, and module 16 forgets), overloading `origin` (destroys the credit axis), inferring from `default:true` (§6 makes accepted catalog modules default-OFF — exactly backwards), a DB catalog table (premature; the manifests already are the catalog).](<redacted>.md) | modules / upstreaming |
252
- | 0168 | [**The scaffold vendors the hub's RUNNING core (`npm pack` of the installed package); registry-default deferred to the go-public step** ([task 1002706](https://cloudbongos.com/builders#/task/1002706); gap 13 of the project-creation audit). Two collided facts: children were born pinned to a frozen `file:vendor/bongos-core-1.19.2.tgz` (installedCoreTarball reuses the hub's own file: pin — task 1002275 — and the hub's pin sat at 1.19.2), and the task-1002621 registry cutover (hub pin now `"@bongos/core": "1.19.56"`, verified on the box) made installedCoreTarball return null — dropping the scaffold into the package-core.js source rebuild that ABORTS on a consumer instance (empty core set → empty child repo, the 1002275 failure resurrected). Fix: a three-rung ladder — file:-pin reuse → NEW `installedCorePackDir()` + `npm pack` of the hub's installed core (child pins the SAME, CURRENT core, stays file:-pinned, owner needs no token) → source rebuild. The brief's "make registry the default now that the package is public" was verified FALSE (both npm scopes 404 anonymously; ADR 0134 keeps them private until go-public) — a registry-default child would be born UNINSTALLABLE for a tokenless owner. Registry-default is deferred to the go-public step (idea 1000700); the gap-13 "flip the box env" ops leg is explicitly a no-op until then; child freshness rides this ladder + the hub-pin lane (task 1002708). Rejected: registry-default now, dropping NPM_TOKEN, vendoring from the npm cacache.](<redacted>.md) | provisioning / distribution |
253
- | 0167-b | [**The /create gate trust link defaults to the AGPL-3.0 license text, not the npm package page** ([task 1002705](https://cloudbongos.com/builders#/task/1002705); gap 1 of the project-creation audit). The gate's "open source" link pointed at a nonexistent repo (404); the task brief's suggested default — the npm package page — is ALSO dead for the gate's actual audience (signed-out visitors): `@cloudbongos/core` and `@bongos/core` both 404 anonymously (private scopes, ADR 0134), and the ADR 0099 public mirror stays dormant until ~2026-11. The license text is the one live, instance-neutral artifact behind the claim today. The browsable-code upgrade is the owner's filed choice (idea 1000700): public repo now (forfeits the ADR 0065 §4 redaction window) / public npm package / wait for the mirror. Rejected: the npm link (verified dead), flipping the repo public (not a builder's call), plain unlinked text (less information, same cost).](<redacted>.md) | public landing / governance |
254
- | 0169 | [**`bongos serve`: the `bongos dev` boot path, on a real host** ([task 1002398](https://cloudbongos.com/builders#/task/1002398), goal 1000052). [ADR 0149](<redacted>.md) gave an owner one command to stand an instance up on their laptop; "now put it on a server" had no equally good answer — Docker serves the *vanilla* brand on `:3000`, `bongos onboard` provisions a whole droplet through the control plane, and [`instance-deploy.md`](../recipes/instance-deploy.md) left the systemd unit as a hand-written prose step. The six steps (deps → Postgres → createdb → migrate → env → server → health) were already right for a host; only four *decisions inside them* were laptop-shaped. So: a `--server` **mode** on the same script (`bongos serve`), never a second boot path, so the two can't drift. (1) **Supervision** — a systemd unit (`Restart=always`, `WantedBy=multi-user.target`) instead of a pid file that dies on reboot, falling back to the pid-managed process where systemd is absent rather than refusing. (2) **Bind address, derived from the origin** — `https://…` means a proxy terminates TLS, so bind `127.0.0.1`; plain `http://` on a public host means nothing is in front, so bind `0.0.0.0`. The common misconfiguration (app publicly reachable on its raw port behind a proxy) becomes impossible by default. (3) **Peer-auth bootstrap** — a fresh `apt-get install postgresql` cluster has only the `postgres` role, so a deploy user's `createdb` fails with `role "deploy" does not exist`; that reads like a broken install and is really an unbootstrapped cluster, so we create the login role as the superuser. (4) **Health means the PUBLIC origin** — the loopback probe proves the app; DNS/proxy/TLS are where a host actually fails, so it probes the advertised origin too and names the missing layer (advisory — DNS may legitimately not be wired). Secrets go to a root-owned `0600` `EnvironmentFile`, never `Environment=` (`systemctl show` leaks those, and the laptop path's `buildEnv()` spreads the caller's whole shell). Every decision is a pure injectable in `dev-lib.js`, proved in `tests/dev_lib.mjs` with no host. Rejected: a separate `deploy.sh` (drift — the point is the *same* six steps), Docker-on-the-host (makes brand+port a compose override), always binding `0.0.0.0` (exposes the raw port), refusing without systemd (breaks containers/BSD).](<redacted>.md) | onboarding / deploy |
255
- | 0173 | [**The legacy multi-ship reward shortfall is NOT backfilled — it closes as a one-time transition cost** ([task 1002520](https://cloudbongos.com/builders#/task/1002520), goal 1000037). Before `core_196` the cost-plus reward booked ONE row per session, so a multi-ship session (the normal shape — ship, new worktree, claim again) was paid at its first ship's level and never topped up; `core_196`'s watermark accounting `(session_id, reward_basis)` fixed that forward and deferred the retroactive half to an "owner-signed-off backfill, tracked separately". This is that decision, and it is **decline**. Measured live 2026-08-16, both of the task's open questions answered: (1) the task's own premise is wrong — `reward_basis = 0` is the column's DEFAULT, so it means "booked before top-ups existed", not "underpaid", and **over-selects by ~60%** (478 legacy sessions, but 287 are single-ship and were paid in full; only 191 are candidates); (2) re-pricing each candidate's stored `session_records.model_usage` at `round(true_cost_usd × 1.20)` gives **7,697 credits owed** across 119 sessions — example-owner 5,296 (69%), jaxri 2,319, scootermack 82 — with **72 candidates unreconstructable** (empty `model_usage`) and 16 already OVER-paid by 1,131 that nobody proposed clawing back. Declined because it mirrors the ADR 0146 clawback's own logic (the accounting changed; neither side of the boundary reopens), because paying only the 119 that happened to leave a server-side trace is arbitrary against the 38% that did not, and because correcting shortfalls while ignoring overpayment is a policy choice the owner declined to adopt. Consequence recorded plainly: three builders stay permanently short. **Does NOT close task 1002448** — that is the Windows `encodeProjectPath` population, never *recorded* rather than paid at a stale watermark, still recoverable and still owed.](<redacted>.md) | economy / rewards |
256
- | 0172-a | [**The sub-Archon rank roles are owner-editable; the Archon role is not; the substrate keys are non-grantable to everything** ([task 1002961](https://cloudbongos.com/builders#/task/1002961), goal 1000030). Amends ADR 0151 §2's "the four seeded rank-roles are immutable through this API" wall — the GRANT half only. Their grants were always ordinary `governance_role_permissions` rows (the `governance_002` seed writes them); the read-only rendering was route policy, not structure. Three walls now, where there was one: the **Archon** rank-role stays frozen as the LOCKOUT GUARD (it is the only role holding `governance.manage` by seed, so freezing it means no click sequence can revoke the permission needed to reach this tab); the three **sub-Archon** rank-roles become grant-editable, the seven **in-app** Archon powers included (owner's explicit choice, escalation consequence stated); and `substrate` becomes a declared catalog axis — `deploy.push_main`/`path.protected.modify`/`infra.ssh` are non-grantable to EVERY role forever, because ADR 0043 enforces them out of band, so a grant row confers nothing and the toggle would be the UI lying. Grantability goes per-role (`isGrantableTo(key, role)`); `isGrantable(key)` survives as the narrow custom-role question config.js strips against. **Custom roles are deliberately NOT widened** — a rank-role moves only by a rank write, a custom role is assignable by anyone holding `governance.manage`. Assignment stays rank-derived (R95b) untouched. Adds `POST /governance/roles/:roleKey/reset` (one transaction, filtered through `isGrantableTo`, refused on a custom role) because a drifted ~60-row rank has no way back by clicking. Rejected: substrate keys behind a warning (a toggle that succeeds and delegates nothing), an editable Archon role behind a confirm (a confirm is not a guard; real recovery is hand-written SQL on the droplet). **Vocabulary amended by ADR 0174 (2026-08-16): governance→government, seeded roles→ranks; the decisions are unchanged.**](<redacted>.md) | governance / permissions |
257
- | 0171 | [**Profile ↔ cross-project rollup: the join runs in-process on the hub; consent is fail-closed account state; the hide flag lives on the account, enforced in the read path** ([task 1002277](https://cloudbongos.com/builders#/task/1002277), R01 of goal 1000045). The C1 cross-project profile reads the platform-identity rollup through the module's **declared-but-never-registered `platform-identity.hub` port**: the module registers a real provider and core `GET /builders/:id/profile` `resolveOptional`s it beside `reward` — zero network, `null` (clean degrade) on every non-hub instance. Federated instances **link out** instead of inlining (amends profile-page spec Decision 1: an instance-side copy is either stale-on-hide or a per-view hub call). Consent: `profile_state` on `platform_identity_accounts`, **fail-closed** — `active` iff touched by a hub-session `/sso/authorize` (device-poll/check-in create `provisional`; backfill = all provisional, lazy upgrade; no core-`builders` join — type-unsafe vs `system:*` ids and a module-migration boundary breach), so the P03 owner decision can only ever widen, never claw back. The C2 hide flag is `hide_stats` on the account (NOT per-instance `behavior_prefs`), enforced **inside** the module's read functions — `listScoutingRollup` itself gains the `active AND NOT hide_stats` predicate so the live Metic-floor `/people`+`/scouting` surfaces honor it by construction. Hub profile responses go `no-store` (hide must beat the 60s cache). Publishing is gated on hardening the self-reported rollup (membership-row requirement, rank-vocab validation, disciplines caps, check-in→rollup sequencing). Found + filed: the `LATERAL unnest` fan-out inflating rollup sums (task 1002911), the unreserved `u` slug (task 1002912), the missing platform T&C artifact (task 1002913), criterion amendments (task 1002914). Rejected: instance→hub per-view pull (always-on dependency), hub→instance mirroring (stale-on-hide), storing the flag in builder-settings, backfilling `active` from hub `builders` rows.](<redacted>.md) | platform identity / profiles / privacy |
258
- | 0170 | [**The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon** ([task 1002778](https://cloudbongos.com/builders#/task/1002778), goal 1000061). The hub card's metadata pass added `created_at` to the public `/projects/featured` projection, labelled **Listed** — it is the moment the hub catalogued the row, never when the project began, and the C6 card test bans the word "Created". Two recorded deviations: (1) the task brief said `updated_at` was ready to add, but the column is bumped by every client re-registration AND the Archon's feature toggle, so as a public "last updated" it would report curation noise as project activity — it may sort, never project, and the test pins the SELECT list itself (`SELECT *` banned: the route is CORS-open rank:public and a star would leak `owner_github_id`/`status`). (2) The owner fact reads the project's OWN leaderboard archon row (login + avatar, type-checked and capped) from the payload project-stats already fetches — zero extra requests, the 2-fetch pin intact, the advisory `owner_github_id` column still unread per the design contract. Multi-archon: top-credits archon is named; a fallen-off-the-page archon renders as no owner shown, never a wrong one. Rejected: projecting `updated_at` as asked (it lies), reading `owner_github_id` (advisory, contract-banned), a third per-origin fetch for activity recency (moves the test pin for a fact nobody asked for yet).](<redacted>.md) | platform identity / public surface |
259
- | 0172-b | [Per-craft compensation: the ideator credit lane, separate from cost-plus](<redacted>.md) | economy / compensation |
260
- | 0174 | [**Governance is renamed government, its positions are ranks, and "role" is reserved for the three crafts** ([task 1003002](https://cloudbongos.com/builders#/task/1003002), R01 of goal 1000068). Owner directive, 2026-08-16. The word **role** named two unrelated things: the permission system's ladder tiers (`governance_roles`, the "rank-roles") and the three crafts (`tasks.discipline` = engineer/artist/ideator) — and goal 1000066 was actively claiming it for the crafts (`three-roles-only`, `users-hold-roles`), putting the two on a collision course. Resolved at full depth rather than as a label pass, because a labels-only fix leaves the UI and the code disagreeing, which is the condition that produced the collision. `modules/governance/` → `modules/government/`; the kernel module key, the config files, `/api/bongos/governance/*` → `/government/*`, and the permission keys `governance.manage` → `government.manage` (~50 sites) + `page.view.governance` → `page.view.government`. **The entity IS the rank — no second noun**: `governance_roles` → `government_ranks`, `role_key` → `rank_key`, `rank-roles.js` → `ranks.js`; "position" stays prose-only, since introducing it as a code term would recreate the qualifier problem "rank-role" already demonstrates. The cross-module kernel port `governance.roleSync` → `government.rankSync` must land atomically across its provider and both consumers (`modules/lifecycle/db.js`, `src/bongos/db.js`) because it is a transaction participant on the rank-write path. Hand-composed bundles stay in the one table as **custom ranks** — the owner's call over splitting them into a separate "appointments" concept, with the objection recorded (a custom bundle is not a ladder position, and ADR 0172 §4 already treats the two kinds as different). Enforced by a `fitness.js` rule, allowlist shipped with reasons. **Discord's server roles keep the word** — that noun belongs to Discord's API. Amends ADR 0151 + ADR 0172 (the editable-rank-roles one) in **vocabulary only**; ladder slugs, `builders.rank`, `requireRank`, and every floor/grant/assignment semantic are untouched. Rejected: renaming the ladder slugs (turns a bounded rename into a trust-boundary migration), a permanent compat shim (both vocabularies stay correct forever).](<redacted>.md) | government / permissions |
261
- | 0175 | [**The Board Room: Full Idea ratification is the first upward-pointing gate** ([task 1003014](https://cloudbongos.com/builders#/task/1003014), R01 of goal 1000069). Every existing gate points DOWN the rank ladder; the owner asked for one that could slow THEM down — *"otherwise I can just ship and build as I want, finding loopholes."* The board's subjects are exactly **two**, both rare by construction: a **Full Idea** and a **change to the constitution itself** (`subject_type` from day one). Because the vote gates no build path, the **ADR 0162 tension dissolves rather than being compromised** — nothing self-executes because there is nothing downstream to execute; ratification confers legitimacy and does NOT auto-create the goal. A vote is a **timed sitting, not a quorum**: absent members are *nulled* (dropped from the denominator, never counted as a no), so the clock is the deadlock exit and no shotgun/fork-with-treasury clause is needed at this scale — default 90 minutes, closing early on full turnout, and yes, a short window means whoever is awake governs (the owner's explicit intent). **Consent, not majority**: silence is consent, one reasoned objection returns the idea, and a zero-vote close PASSES. A rejection must carry free text **plus one of the five Full Idea sections** — which is what **forces the hall to be the voting surface**, since a Discord reaction could only ever be feedback-free; Discord is therefore an outbound notification mirror that casts nothing, and its own mobile app is the mobile push (no mobile surface is built). The completeness grade is the **floor** (a window opens automatically on pass), the vote is the **merit**; concurrent windows are normal. Who governs is **configuration** — day one is monarchy expressed as config on the real machinery, never a separate code path — and **amendments are decided under whatever rule is currently in force**, so flipping out of monarchy is a legal monarchy act and no bootstrap exception is needed. Open ballot; an author may never vote on their own item. Franchise derives from **rank, not role**, and that is forced: `PATCH /me/disciplines` is deliberately ungated, so a craft-scoped chamber would be one-request privilege escalation. Unblocks goal 1000067 / IC5, which consumes the pass event and *"invents no parallel vote"*. Rejected: quadratic vote weighting and the attribution-economy knobs (γ, τ, the 30% stack cap — v1's franchise is not credit-weighted at all), eternity clauses, and any legal-entity integration.](<redacted>.md) | government / ideas / governance |
262
- | 0176 | [**Private-repo standup: a per-repo read-only deploy key, minted with the owner's token** ([task 1002735](https://cloudbongos.com/builders#/task/1002735), goal 1000032; walk finding N4, seen live on `mercury` 2026-08-17). A standalone instance's repo is cloned/pushed once with the owner's `repo`-scoped token (ADR 0155), but the durable `origin` rides the shared SSH deploy alias — which GitHub answers "Repository not found" for a private repo it isn't registered on, so every private standup failed at the recurring `git pull --ff-only origin main`, **token or not**. Fix: when the target repo answers *private* to the owner's stored token, provisioning mints the instance its OWN ed25519 deploy key beside the checkout (`.git/cloudbongos-deploy-key` — untracked, survives `git reset --hard`, dies with the checkout), registers it on the repo **read-only** as `cloudbongos-deploy-<slug>` (idempotent by title; a stale same-title key is replaced, never duplicated), then wires the CHECKOUT: `origin` re-pointed at plain `git@github.com` and `core.sshCommand` pinned to the key with `-F /dev/null` so no `~/.ssh/config` identity can shadow it. Public repos take the unchanged shared-alias path; pushes keep preferring the owner-token HTTPS URL (task 1002317), which is why read-only suffices; teardown best-effort revokes by title. Rejected: **GitHub App installation tokens** (hourly expiry means every unattended pull needs a minting sidecar or a hub round-trip) and **registering the shared platform key on the repo** (one shared secret granting the whole platform durable access to private code; per-repo keys keep the blast radius to one repo and are visible to the owner in their own repo settings). Follow-ups: the manage page's flip-to-private stays refused until already-provisioned instances get the same wiring, and builder dev boxes of a private-repo project need their own leg.](<redacted>.md) | provisioning / security |
263
- | 0177 | [**A goal membership is a `membership_kind`, and the rename ships expand-then-contract** ([task 1003013](https://cloudbongos.com/builders#/task/1003013), goal 1000068). ADR 0174 reserved `role` for the three crafts and enforced it on the GOVERNMENT surface only — by design, since `builders.system_role` and Discord's own noun are legitimate. That left a **fourth** use uncatalogued: `goal_members.role` (`'lead' | 'member'`), the MANAGER predicate behind goal authorization. Renamed to **`membership_kind`** — values unchanged, because the noun was the problem; `rank` is wrong (not a ladder position) and `position` is worse (ADR 0174 uses it as the prose word for a rank, recreating the "rank-role" qualifier collision). A boolean `is_lead` was seriously considered and rejected: the text rename keeps the copied CHECK and stays a reversible change on a live authz column, where `ADD boolean; UPDATE; DROP text` is data-destructive. The migration is **expand-then-contract, never `RENAME COLUMN`** — auto-rollback reverts the code pin and NOT the DB, so `core_220` ADDs the column and keeps the pair in lockstep with a BEFORE trigger (direction decided by which side changed), and a later task DROPs the old column with the `-- forward-unsafe` pragma, a backup, and its own window; no `SET NOT NULL` in step 1, because a static NOT NULL on a column the previous release never heard of is what the forward-safety gate rightly flags. The API field renames with it and carries **no `role` alias** (ADR 0174's Lineage lesson: a time-boxed shim outlives its release), pinned by the test of its absence. The reservation gains a **second narrow wall** rather than a widened first one — line-scoped inside the 5k-line `db.js` to the lines that name `goal_members`, so no allowlist is needed. A **fifth** use (`project_memberships.role`) is catalogued and filed, with this as its pattern.](<redacted>.md) | naming / lifecycle |
264
- | 0178 | [**The copy desk: a flag carries a judgement, and no column here could ever hold copy** ([task 1003113](https://cloudbongos.com/builders#/task/1003113), R02 of goal 1000074). [ADR 0081](<redacted>.md) made the repo the source of truth for design TOKENS; user-facing TEXT had no equivalent. R01 built the inventory (`copy-inventory.js` → `docs/copy-registry.json`), which answers *what does the product say* but cannot answer *what is wrong with it* — a judgement a person must record. This is that record plus the artist's queue. A new default-on module keyed **`copy-desk`, not `copy`**, for a concrete reason: the branding contract already owns a `copy` block, so the bare word reds the `core carries no module code` fitness check on the English word in `src/branding.js` — and the editorial "copy desk" is the more accurate name anyway. **The reason is required in the route AND in a CHECK constraint**: the route gives a person a usable error, the constraint means no future write path can create the reasonless flag the queue exists to not contain (a pointer with no judgement is *worse* than no flag — it still costs the artist a look). Closing requires a note by the same symmetry, since an unexplained close is indistinguishable from clearing a queue to move a number. The load-bearing decision is **NO LIVE CMS enforced by the schema's SHAPE**: `copy_desk_flags` has no column that could hold replacement text and must never gain one, because that non-goal does not erode via an "add a CMS" commit but via one plausible column, then one plausible read of it — `copy_no_cms.mjs` pins the write surface at exactly two verbs so R03's proposal path changes it in a reviewable diff. Rank is asymmetric: **filing is open to every rank** (the engineer who wrote the toast and the newcomer who could not read the button are the likeliest noticers, and neither is the artist — the reason floor is the quality gate, rank is not), closing is Metic+ **or the author** (withdrawing your own flag is a correction, not triage); nothing gates on craft, since an artist is a craft and not a rank. Duplicates are the only automatic signal (the one of stale/duplicated/off-voice needing no judgement layer) and the count **subtracts settled work**, or it never moves and teaches that the queue is decorative. A vanished target is **reported, never auto-closed** — the id is a hash over the text, so an edit routinely orphans a flag, and closing a human judgement because a hash moved is the family of error ADR 0162 rules out. Deliberately reports **no `generated_at`**: R01's contract is determinism, so a clock is the one field it cannot carry.](<redacted>.md) | design / artist loop |
265
- | 0179 | [**`rank:metic+` means metic and above; a bare `rank:metic` still means exactly metic** ([task 1003094](https://cloudbongos.com/builders#/task/1003094), goal 1000069). [ADR 0175](<redacted>.md) gave the constitution ONE membership form, `rank:<key>`, resolved by an EXACT SQL match — so `rank:metic` seated metics ONLY (excluding Archons), `rank:xenos` seated NEWCOMERS ONLY rather than "everyone", and "Metic and above" was inexpressible. Everywhere else a rank is a LADDER and authority is cumulative (ADR 0034 inheritance, requireRank('metic','archon'), the floor-derived seed); board membership was the one place it meant "exactly", and nothing said so. Rehearsing R25 on a real Postgres, an Archon ratified `rank:metic` under monarchy — the apply worked perfectly — and the next vote came back 403 not_board_member: **the owner flipped the switch and fell off their own board**, with the board deadlocked at one member because an Archon can still PROPOSE but no longer VOTE. Owner decision: EXPLICIT GRAMMAR. `rank:metic+` (that rank and above), `rank:metic,archon` (union), and **a bare key still means EXACTLY that key** — rejecting the smaller diff (silently redefining the bare form) precisely because it would have changed the meaning of every already-ratified constitution, the monarchy default included, on upgrade: nothing in a government should change meaning without a vote. `+` is defined only on the STANDARD ladder — "and above" about a custom rank with no ladder position is a parse REFUSAL, not a guess, and a refusal seats nobody. The grammar had been written TWICE (a shape regex in the config sanitizer, a parse regex in the resolver, pinned by a property test) — sound for one literal form, unsound the moment a rule needs a LOOKUP, which is the same class of drift that produced the bug; both now call one pure `membership-predicate.js`. Adds the DISENFRANCHISEMENT GUARD: a proposal that would unseat its own proposer is refused before any write, naming the one-character fix, overridable with an explicit `acknowledge_self_removal` (a separate parameter, never a proposal field) because stepping back is a legitimate act. The two DB reads take a set via `= ANY($1)` + DISTINCT (a double-counted member silently raises the consent bar), the structural filters are re-proven against real Postgres (a deactivated builder and a machine principal still never govern), and the hall panel now describes every form in plain language — rendering `rank:metic+` as "the metic+ rank" would be the same bug one layer up. Unblocks task 1003095, which can now write each charter's TRUE predicate rather than wording around a limitation.](<redacted>.md) | government / the board |
266
- | 0180 | [**Dependency intake & drift policy: baseline-ratchet audit gate, install scripts off, cooldown via patrol, OIDC publish** ([task 1003127](https://cloudbongos.com/builders#/task/1003127), goal 1000076). The red-team baseline audit found 18 advisories (2 high) sitting unwatched in 9 production deps, the `undici ^6.27.0` override pinned INSIDE a vulnerable range while also breaking Node 26 CLIs, and nothing gated what an agent may install — against a 2025 npm threat wave (chalk/debug, Shai-Hulud, Nx) that ran on postinstall scripts and long-lived publish tokens. Four decisions. (1) `scripts/gds/audit-gate.js` runs in the required `unit` check: a high/critical advisory NOT in the committed `config/audit-baseline.json` fails the merge gate; the baseline is **shrink-only pre-gate debt** (10 findings at adoption) — adding an entry to silence a NEW advisory is out of policy. Fail-OPEN on registry/infra errors, fail-CLOSED on findings: the gate catches new advisories, it does not prove registry uptime. Notably a blanket `npm audit fix` cannot even run here (`workspace:` protocol, EUNSUPPORTEDPROTOCOL) — fixes are explicit tasks. (2) `.npmrc ignore-scripts=true`: the tree needs no install-lifecycle scripts — sharp installs prebuilds via optionalDependencies, proven by a script-less `npm ci` + full 430-test suite; a future dep needing one is an ADR-level exception. (3) Update cooldown is PATROL-enforced (task 1003128 flags versions younger than 7 days + stale overrides + action-pin drift) rather than `.npmrc`-enforced, because CI's npm 10 warns on the npm-11 `minimum-release-age` key for every builder; move it into `.npmrc` when the toolchain floor reaches npm ≥ 11.10. (4) Publish auth moves to npm **trusted publishing (OIDC)** — needs a one-time owner registration on npmjs.com (blocker filed); until then the granular 90-day `NPM_TOKEN` is a documented exception.](<redacted>.md) | devsecops / supply chain |
220
+ | 0141 | [Cross-site identity — cloudbongos.com becomes a federated identity provider (the "hub") so one platform account reaches any hosted project's builders hall, browser AND CLI, with no second GitHub sign-in (Goal 41 design of record; task [#1002082](https://cloudbongos.com/builders#/task/1002082); implemented across [#1002106](https://cloudbongos.com/builders#/task/1002106)–[#1002115](https://cloudbongos.com/builders#/task/1002115)). Core principle: **identity is federated, authority is not** — the hub carries identity ONLY (github_id, login, display name, avatar), never rank/credits/admission, so [ADR 0016](0016-trust-boundary-server-enforced-permissions.md)'s self-owned-authority boundary is preserved by construction. Federation is opt-in + reversible via a config switch (`auth.idp`): a hosted instance provisioned with `auth.idp` + `<PREFIX>_HUB_*` routes web sign-in through the hub's `/sso/authorize`→`/sso/token` (OAuth authorization-code, single-use 60s codes, server-to-server client-secret redemption) and CLI sign-in through the hub's device flow + an audience-scoped Ed25519-signed assertion verified offline at a new `/auth/device/exchange`; a self-hosted instance leaves `auth.idp` unset and keeps owning its own GitHub app (the direct-GitHub path, unchanged fallback). Adds a hub `platform-identity` module (accounts / sso_clients / sso_authorization_codes / projects / project_memberships / builder_project_activity), a project→hub membership check-in inside upsertBuilder (coarse role, never rank), a My Projects view with project-executed join/leave, a curated public Explore showcase, per-builder cross-project activity rollups, and an Archon-only cross-project scouting view (upgrading the coarse [ADR 0127](0127-cross-box-fleet-observability.md) fleet gauges to a github_id-keyed store). Two credential types by design: per-client hashed secret (instance→hub server-to-server auth) + a hub Ed25519 keypair (CLI assertions, asymmetric so one instance can't forge for another). Was scoped as "ADR 0135"; that number was reassigned, so it lands here as 0141.](<redacted>.md) | auth / Cloud Bongos identity |
221
+ | 0142 | [Project context is prose-only; the structured host-identity contract is branding (audit H11 / finding F7; task [#1002224](https://example.com/builders#/task/1002224), goal 1000042). Branding flows as a structured neutral⊕instance→validated-resolver contract because CODE reads those strings; project context (vision/world/visual-style in `docs/project-context.md`, scaffolded from the shipped `docs/project-context.template.md`) is deliberately narrative — read by AI agents, never parsed by code — so it stays prose-only with no parallel resolver/schema/env layer (YAGNI: no consumer; branding already covers code-read strings; the publish boundary + the CLAUDE.md/AGENTS.md stub already enforce the split at the file level per [ADR 0062](0062-medusa-instance-model-decoupling.md) §7). A new host value that code must read goes in branding, not a new project-context resolver. Revisit only when code (not agents) must read project context as validated, structured data.](0142-project-context-is-prose-only.md) | core / host-split |
222
+ | 0144-a | [Federated single-logout: hub-initiated back-channel session revocation (task [#1002238](https://cloudbongos.com/builders#/task/1002238), goal 41; follow-up to [ADR 0141](<redacted>.md)). ADR 0141 gave single sign-ON but not sign-OFF — a federated instance mints its own local session after the SSO handshake, so hub logout left every project still signed in. Owner chose **back-channel** (server-to-server) over front-channel (browser-driven, breaks under third-party-cookie blocking, incomplete if the tab closes) and soft-sessions (per-request hub re-check — too heavy, hard hub dependency). Core `POST /auth/logout` emits a `builder.logout` seam EVENT (no-op unless a module listens); the hub `platform-identity` module reacts by minting a **hub-signed EdDSA logout token** (`mintHubLogoutToken` — same envelope/key/~120s-TTL/aud-binding as the login assertion) per entered project and POSTing it to each `{origin}/api/gds/auth/backchannel-logout`; that core endpoint (dormant unless `idpConfigured()`, else 404) verifies via `verifyHubLogoutToken` against the hub pubkey and calls `revokeAllSessionsForBuilder`. The token carries only `{sub, purpose:"backchannel-logout"}` — **never** identity/rank (ADR 0016 boundary holds); the **`purpose` marker makes login assertions and logout tokens mutually non-interchangeable** (a login assertion lacks purpose → rejected as logout; a logout token lacks github_login → rejected as login). Entered-instance list = existing `<redacted>` (over-notify is safe — revoke is idempotent); fan-out is best-effort, per-instance 4s timeout, `Promise.allSettled`-isolated, never blocks/fails the logout (hub session revoked synchronously). No new schema, no doorway change; core `1.18.5→1.18.7`; re-vendors BOTH hub + federated instances. V1 = hub-initiated only (instance→hub propagation + a precise per-session ledger + unreachable-instance retry are follow-ups).](0144-federated-single-logout-backchannel.md) | auth / Cloud Bongos identity |
223
+ | 0144-b | [Re-home the dev-box fleet onto the cloudbongos.com plane (native runners, local DB, isolated fleet tag)](0144-devbox-rehome-onto-cloudbongos-plane.md) | infra / dev boxes |
224
+ | 0145-a | [A free "own-repo" hosted project tier can NOT be a bare `cloudbongos.com` subdomain running the owner's code — a pre-build security review found it unsafe two ways (owner design review, [task 1002249](https://cloudbongos.com/builders#/task/1002249); follow-up to [ADR 0141](<redacted>.md)/[0143](0143-onboarding-repo-picker-oauth-scope.md)). (1) The hub `gds_session` cookie is `Domain=.cloudbongos.com`-scoped (so `builders.`/`status.` share one sign-in), so an owner-controlled `<slug>.cloudbongos.com` instance would receive every signed-in visitor's live hub token → account takeover; `httpOnly`/`sameSite:lax` don't help (same registrable domain = same site). The co-tenant instances that safely federate on cloudbongos.com subdomains (e.g. `demo.`) do so only because they run PLATFORM code, not owner code — so the precedent doesn't extend. (2) A control-plane-local `standalone` runs owner code on the hub box as `lars` with passwordless sudo in its `npm ci` deploy → root-on-hub (pre-existing, independent of federation). The federation protocol itself is sound (origin-bound, asymmetric Ed25519); the weakness is PLACEMENT + EXECUTION. **Decision:** a free own-repo tier is delivered ONLY behind (a) domain separation — free instances under a different registrable domain than the hub, so they never get the cookie — AND (b) per-project isolation (own droplet / container, distinct UID, `--ignore-scripts`, no passwordless sudo); only then is federation safe to extend. Owner chose this direction over the co-tenant free tier (safe/$0 but no own repo) and the honest-copy status quo. Build blocked on owner decisions: which separate domain, droplet-vs-container isolation, and the cost model (paid tier vs capped subsidy — [§4](../../CLAUDE.md)). Points 1&2 of the review (repo-step polish) were unrelated + already shipped ([task 1002247](https://cloudbongos.com/builders#/task/1002247)).](0145-free-hosted-project-tier-isolation-and-domain-separation.md) | auth / provisioning / Cloud Bongos identity |
225
+ | 0145-b | [The Dev Box app becomes a branding-driven module (one source, per-instance builds)](0145-devbox-app-branding-driven-module.md) | dev-box app / modules |
226
+ | 0147 | [Carry ADR 0110's deploy-regen of generated docs to STANDALONE instances ([task 1002314](https://cloudbongos.com/builders#/task/1002314); the owner's "docs didn't transfer from primea"). The monorepo keeps its generated nav docs live because ADR 0110's regen is wired into `deploy-prod.sh`; standalone instances ([ADR 0108](0108-instance-composition-configurable-root.md)) never got it — their bespoke pull-deploy regenerated nothing and `init`/`upgrade` only did the API docs, so cloudbongos.com's `docs/` was frozen with just the 3 API files. Two defects: (1) `gen-session-index.js` hardcoded the CORE root (`path.resolve(__dirname,'..','..')`) so on a standalone box it indexed `node_modules/@bongos/core`, not the instance — now `resolveInstanceRoot()`; (2) no standalone regen path. Fix: `upgrade.regenerateNavDocs()` + a `regen-instance-docs.js` CLI, wired at every refresh point (init seed, `bongos upgrade`, `provision.js` standalone deploy, and a bespoke `deploy.sh` one-liner); `INSTANCE_GITIGNORE` ignores the whole-file docs. Scope is `session-index` ONLY — the one gitignored, whole-file, instance-content nav doc; `repo-map` is core-scoped (empty for a code-less standalone; brownfield map = task 1001655), `file-map` + API docs are tracked/committed (file-map seeding at standup = follow-up), diagrams are the headless-browser exception.](0147-standalone-live-docs-regen.md) | docs pipeline / Cloud Bongos |
227
+ | 0146 | [Configurable reward mode — an instance chooses whether a shipped task pays BOTH reward streams or ONLY the cost-plus one ([task 1002270](https://cloudbongos.com/builders#/task/1002270), goal 1000037; follow-up to [ADR 0120](0120-pay-on-land-and-builder-owned-rebase-gate.md)). A shipped task pays two streams onto one balance (the [ADR 0054](0054-session-token-reward-cost-plus.md) "two conventions, one ledger" tension): the per-task author ESTIMATE (`credits_reward` × kind multiplier — unitless, [ADR 0023](0023-kind-multipliers-and-idea-bonus.md)) AND the cost-plus SESSION reward (`round(true_cost_usd × 1.20)`, 1 credit = $1 — ADR 0054). New `branding.reward.mode`: `"cost-plus-and-estimate"` (DEFAULT — pay both, exactly today's behavior; every existing instance unchanged on upgrade) or `"cost-plus-only"` (the cost-plus reward is the SOLE equity reward; the per-task estimate PAYOUT is withheld and its idea-promotion slice cascades to 0). Resolved by a pure `rewardPolicy()`; gated in the single per-task credit writer (`insertMultipliedConfirmCredit`) — entirely inside the economy module, ZERO lifecycle changes. `credits_reward` still rides the task row for display + the no-zero-reward claim gate; only the payout is suppressed. Unknown mode → default (a typo can't silently zero rewards). Cloud Bongos selects `cost-plus-only`; achievements + the net-negative +1 bonus are out of scope. Backward-compatible + reversible — no clawback, no migration.](<redacted><redacted>.md) | GDS / economy |
228
+ | 0148 | [Task-scoped box source access (rank-scope → claim-scope)](0148-task-scoped-box-source-access.md) — paused by ADR 0193 | dev boxes / source scope |
229
+ | 0149 | [`bongos dev` — a one-command LOCAL launcher for a standalone instance repo (owner ask, 2026-07-19: "we need a better option to stand up the local host"). A freshly-created project's `config/branding.json` points at `http://localhost:<port>` — configured to run on the owner's machine — but the platform shipped no command to START it there; `bongos onboard` provisions a CLOUD server and Docker serves a vanilla brand on `:3000`. Standing it up by hand was a six-part assembly (Postgres → createdb → migrate → PORT/PGDATABASE/PGHOST/OAuth env → server → login) with three silent mismatches: the server reads PORT from env not branding (platform-server.js:44); the pool DB name collapses to the OS user while migrate.sh defaults to `example` (pool.js:47 vs migrate.sh:52) unless PGDATABASE is set for BOTH; and node-pg defaults to the Linux peer socket `/var/run/postgresql` (pool.js:46), absent under Homebrew on a Mac. `bongos dev` does the whole thing idempotently from one source of truth (branding): ensures Postgres, createdb, migrate (`MEDUSA_GDS_ONLY=1`), starts the server on the branding port, and signs in. The background server is MANAGED (pid/log per instance) with `bongos dev stop`/`status` — the fix for the orphaned-server accident (a stale `:3005` answering a new project's login). Pure resolution + process mgmt in `scripts/gds/dev-lib.js` (unit-tested `tests/dev_lib.mjs`); orchestration in `scripts/gds/dev.js`. Alternatives rejected: cloud dev box (needs hosting + cost — kept for remote work), Docker (vanilla brand/wrong port — kept for Windows), recipe-only (leaves the six steps). Not bundled into the standalone on-ramp binary (needs a real checkout).](0149-bongos-dev-local-launcher.md) | Cloud Bongos / dev experience |
230
+ | 0150 | [Box first-boot bring-up on vendored instances — resolve `infra/` from the instance root + fail loud on empty user-data (spike BV1.R97, [task 1002385](https://cloudbongos.com/builders#/task/1002385); C2 of goals 1000051/1000052, "make cloudbongos boxes stop coming up bare"). Root cause: `box.js` sets `REPO_ROOT` two dirs up from itself, which on a VENDORED instance is the `@bongos/core` package — but the R89 packaging split kept `infra/` (the DO cloud-init template + inlined `box-source-fetch.sh`/cloudflared/ttyd/heartbeat scripts) as INSTANCE content, not in the core publish-manifest. So `readCloudInit` reads a missing template, its `try/catch` returns `null`, and the caller's only guard (`0 > 64KiB`) passes → the box is created with EMPTY user-data and boots bare Ubuntu (no clone, no toolchain, no terminal); `fileGzB64` is likewise fail-soft (warns, returns `''`) so a partial `infra/` half-wires. Live re-confirmed 2026-07-19: instance-root `infra/` present, vendored-core `infra/` gone (the [ADR 0144](0144-devbox-rehome-onto-cloudbongos-plane.md) node_modules workaround wiped by a reinstall), fleet idle so the bug is latent-but-live (next provision boots bare). Fix (R98, [task 1002386](https://cloudbongos.com/builders#/task/1002386)): a `resolveBoxInfraDir()` locating `infra/` via [ADR 0108](0108-instance-composition-configurable-root.md)'s `resolveInstanceRootExplicit()` (cloudbongos `bongos.config.json` = `{"instanceRoot":"."}`) + an env override + a walk-up-out-of-`node_modules` fallback, AND fail-loud backstops (throw on missing template; abort provision on empty user-data). R99 ([task 1002387](https://cloudbongos.com/builders#/task/1002387)) then ensures the toolchain builds on the box. Reinstall-proof + portable; core-only, no migration/doorway change.](0150-box-first-boot-bringup-vendored-instances.md) | GDS / dev boxes |
231
+ | 0151 | [Governance: **permissions are the atom, ranks become seeded roles** — the foundation of the BONGOS-V1 Governance goal (1000030), spike BV1.R90 ([task 1001838](https://cloudbongos.com/builders#/task/1001838)); settles the model in an ADR before R93–R114 build. Authority stops being an all-or-nothing rank threshold ([ADR 0016](0016-trust-boundary-server-enforced-permissions.md)/0018/0034) and becomes granular **permissions** (code constants that map 1:1 to real routes/surfaces — a permission no route consumes doesn't exist), bundled into assignable **roles**; the four live ranks become four seeded roles whose grants reproduce today's thresholds EXACTLY (the compat seed — cumulative nested supersets, so the resolver's UNION preserves the at-or-above threshold), and `builders.rank` stays as the identity/graduation column. A per-request **uncached** `resolveBuilderPermissions()` (kernel port, preserving ADR 0016's instant-demotion) + a `requirePermission()` gate that coexists with `requireRank` via the seed → gates migrate route-by-route, no flag day. The **complete authority model** (C3) represents four axes as first-class types, not rank special-cases: rank-seeded roles, machine **principals** (`system_role`=bfg/discord_bot, exact-identity gate, never UI-grantable), **session-scopes** (box tokens, deny-by-default allow-list intersected with the builder's roles), and **ownership-scoped** perms (`X.manage.own` ≠ `.any`; goal owner/manager per [ADR 0106](0106-goal-owner-manager-authority.md)). The safety keystone (C6): every permission carries a **`system` flag** — `system:true` (deploy, protected-path, rank-set, version-close, grade-bypass, cross-tier edges) is **non-grantable** and LOCKED in the tab, so no configurable role can hand out the crown jewels; and the today-hardcoded `PROTECTED_GLOBS`/`EXPECTED_RANKS` map becomes a **governance-owned protected-surface registry** that the OUT-OF-BAND substrate (pre-push hook, grader pre-pass, `main-audit.js`, gate-review — [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md)) READS but never moves in-process: **policy owns / substrate enforces** — the tab can re-map the floor but a forced `system` grant still can't deploy (abuse-matrix + live-demo proofs, R112/R114). Capability in **core** (`modules/governance/`, default:true); role templates are per-instance **config** (`config/governance.json` over `config/governance.neutral.json` over env, mirroring `src/branding.js`), live assignments in the DB. Collapses the ~6 duplicated numeric rank-tier maps (the Builder-25-incident drift class) behind the resolver. Zero day-one behavior change (parity test R103); one deliberate fail-closed divergence (unwired divine ranks map to no role instead of latently inheriting Archon). Includes the full enumerated permission catalog (every key + `system` flag) grounded in the 2026-07-24 authority sweep. No new recurring cost. **Vocabulary amended by ADR 0174 (2026-08-16): governance→government, seeded roles→ranks; the decisions are unchanged.**](0151-governance-permissions-as-atom-ranks-as-roles.md) | governance / permissions / Cloud Bongos |
232
+ | 0152-a | [The landing gate is served by the **core app**, not by the front proxy — the cutover step of the landing migration ([task 1002411](https://example.com/builders#/task/1002411), L05 of goal 1000035). The reference instance's orb-gate front page was `file_server`'d by Caddy straight out of a checkout of an UNRELATED product repo (`root * <checkout>/brand/cloud-bongos`), so the landing had two sources of truth, no deploy path of its own, and a cross-product coupling the core could not express. [L01](https://example.com/builders#/task/1002407) had already ported the bundle into the core as `public-landing/` and [L02](https://example.com/builders#/task/1002408) had already taught `platform-server.js` to serve it (`/` -> 302 `/gate/`, `/gate/` serves the index raw, `express.static` covers the assets) — so by the time L05 came up, BOTH halves of its original plan ("give the landing (a) its own Caddy config and (b) its own deploy script") had dissolved. **Decision:** the app owns the landing end to end; the proxy drops `root` + `file_server` and keeps exactly three jobs on that surface — TLS, an optional pre-launch `basic_auth` lock, and the `@hidden` guard that keeps `gate/README.md` unreachable. No separate Caddy config to write (the resulting block IS the generic pure-proxy shape `provision.js`'s `caddyBlock()` already emits per instance, [ADR 0111](0111-instance-hosting-provisioning-module.md) §4; host-specific hostnames/certs stay on the host per [ADR 0062](0062-medusa-instance-model-decoupling.md) §7) and no separate deploy script to write (the landing ships INSIDE the pinned core package, so it rides the instance's existing pull-deploy — one artifact, one version, one path). Keeps `redir / /gate/ 302` because Caddy orders `redir` before `basic_auth`, so "/" behaves identically before and after, and the app issues the same 302 once the lock is lifted. Cost accepted: the landing now sits in the app's request path (a dead app = a dead front page — but the gate's whole purpose is to reach the app), and a landing tweak now needs a core release. Verified on the live instance: identical status codes across the apex and both vanity doors before/after, relative redirects (no origin leak), and a **severance test** — with the foreign repo's landing folder moved aside entirely the site still served the byte-identical gate, proving the dependency is cut rather than merely unused. Retiring the foreign copy is L06 ([task 1002412](https://example.com/builders#/task/1002412)).](0152-landing-gate-served-by-the-core-app.md) | platform serving / instance infra |
233
+ | 0152-b | [**Metic gains task abandonment, paired with an un-abandon at the same rank** (owner directive 2026-07-27: "Metics should also be able to abandon tasks"; [task 1002476](https://cloudbongos.com/builders#/task/1002476)). [ADR 0090](0090-metic-task-authoring.md) moved task authoring (create/edit/promote/deps/criteria) down to the trusted working rank but kept `POST /tasks/:id/abandon` Archon-only as "terminal scope removal" — so the rank that runs idea-triage, seeds the tasks, and shapes them still had to hand every duplicate/moot/superseded chore back to the owner to retire. That hand-back is janitorial, not adjudication. The real hazard was never the grant: abandon was **one-way through the API** (`/promote` took only backlog/blocked, `PATCH` refuses `status`), so a mis-retire needed hand-SQL on prod, and an abandoned task is invisible in every claimable list and every version rollup (migration 017 excludes it from `version_progress`) — nobody notices until the work silently doesn't happen. So the gate moves to `requireRank('metic','archon')` **and** `/promote` gains a third source state, `abandoned → backlog` (never straight to ready — a retired task re-enters un-workable and takes a second deliberate promote), backed by `db.restoreTask`, the mirror of `abandonTask`, which clears the `'Abandoned: <reason>'` stamp and only that. Unchanged: the shipped-task and active-claim refusals, idempotency, and the `audit_log` record of who retired what and who brought it back. The `EXPECTED_RANKS` lifecycle pin moves in lockstep (a *further* downgrade stays a hard CI fail); under [ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) the `task.abandon` floor moves archon→metic, landing as `governance_003` because the compat seed is run-once — purely additive, and the drift guard now compares the **union** of grant migrations to the catalog and fails loudly on a future revocation.](0152-metic-task-abandonment.md) | GDS / permissions |
234
+ | 0153 | [**Sequential chain execution — one kickoff walks a dependency chain** ([task 1002484](https://cloudbongos.com/builders#/task/1002484)). [ADR 0015](0015-task-dependencies-and-auto-promotion.md) made `dependencies[]` load-bearing and auto-promotes a task's dependents `backlog`→`ready` on ship — but nothing *picks the unblocked task up*, so on the live instance **146 of 303 open tasks sit behind a predecessor** and a 24-task planning seed needed 24 separate human kickoffs. The only "sequential" affordance was a clipboard prompt on the Tasks board (`<redacted>`), which is text rather than execution and cannot even select the gated successors, because they are still `backlog`. Requested independently by two builders (ideas 1000461, 1000398). **Decision:** a chain runner with **no schema change** (the polymorphic `dependencies` table + `task_is_fully_unblocked` + the promote-on-ship trigger already encode the order; a `chain_id`/queue column would be a second, drift-prone source of truth) and **no new claim guard** (one-active-claim-per-session is keyed on the *working tree*, so ship-before-next-claim is already the enforced shape — and batch-claiming a chain is impossible anyway, since successors fail `DEPS_NOT_SHIPPED` until their predecessor ships; the ship IS what unblocks the next step). Deterministic plumbing (`scripts/gds/sequence.js` — scope, topological order, halt matrix, all read off the task row) is split from judgement (`.claude/skills/builder-sequence/`, which defers to `/builder-claim` + `/dev` + `/builder-ship`). **Halt, do not skip** (owner decision): on `needs_migration`, `security_sensitive`, `protected_path`, `rank_floor`, `thin_spec`, `judgement_kind`, `not_claimable`, `deps_unmet`, `in_flight`, or `cycle`, the runner stops and reports with the task still claimed — because in a dependency graph a skipped task is a *predecessor*, so skipping strands everything behind it while the run keeps posting apparent progress. Not built: chains as a priced claimable unit on the board (idea 1000398), which is additive.](0153-sequential-chain-runner.md) | GDS / methodology |
235
+ | 0154 | [A goal's done-when criteria are authored by that goal's owner or managers](0154-goal-owner-criterion-authoring.md) | GDS / goals |
236
+ | 0155 | [**Adopting a private repo: widen the onboarding OAuth scope to `repo`** ([task 1002561](https://example.com/builders#/task/1002561)) — amends [ADR 0143](0143-onboarding-repo-picker-oauth-scope.md) §1 by taking the exit 0143 itself named (“revisit to `repo` only if private support is needed”). Standing up a project on an EXISTING PRIVATE repo died at the provisioner’s scaffold push with GitHub `403 Write access to repository not granted`: `scopeForWebFlow` requested `public_repo`, which grants write to **public** repos only. Confirmed as scope and not credentials three ways — the live `github_oauth_tokens` row was `public_repo,read:user` and still VALID during the failure (a dead token 401s; this 403 text is GitHub’s canonical scope-insufficient reply); every greenfield project worked only because `createUserRepo` hardcodes `private:false`, so the platform had only ever pushed to repos it had just made public; and the picker already rendered a **“Private” badge** for a case `public_repo` made unreachable. Owner chose (B) widen-and-keep-pushing over (A) never-touch-the-user’s-repo (user runs `bongos init --adopt` + pushes; needs a deploy key for the instance to *run*) and (C) public-only, accepting the cost this reverses: the persisted token can now write to **every repo the user owns**. Containment from 0143 is unchanged and is what makes it tolerable — encrypted (secret-box), **1h TTL**, lazily swept, requested ONLY on the wizard’s `?repos=1` path (hall + CLI stay `read:user`), never stored when secret-box is off. Three guards ride along: (1) widening the *request* does not upgrade *stored* tokens, so new `grantedScopeCoversRepos()` gates `resolveRepoToken` and a surviving `public_repo` row takes the existing `repo_scope_required` (403) reconnect path instead of 200-ing while **silently omitting the user’s private repos** — it PARSES GitHub’s comma-separated grant rather than substring-matching, since `public_repo` *contains* “repo” and a naive match accepts exactly the stale token being guarded against; (2) `createUserRepo` keeps `private:false` — now a **product** choice (instances are open-source/capture-resistant by design), deliberately not a parameter, test-pinned so the widen can’t change what we create; (3) consent copy states the breadth in our own words first, since GitHub words `repo` as full control of all repositories. A central **GitHub App** (fine-grained `Contents:write` on only the selected repo) remains the principled fix and this makes its case stronger — a future ADR adopting it supersedes this. **This alone does not make adopt work:** provisioning is independently greenfield-only (`git init` a fresh tree + push to `main`, which an existing repo rejects as unrelated history) — [task 1002562](https://example.com/builders#/task/1002562) fixes that and, because the wider scope makes a mistaken `--force` a real data-loss route on a user’s repo, carries a test asserting the adopt push never forces; [1002563](https://example.com/builders#/task/1002563) verifies live.](0155-adopt-private-repo-widen-oauth-scope.md) | onboarding / auth / provisioning |
237
+ | 0156 | [**A builder's own display name wins, always** ([task 1002568](https://cloudbongos.com/builders#/task/1002568)). A builder renamed themselves in hall Settings, the save worked, and the name reverted to their GitHub account name at the next sign-in. `upsertBuilder` (`src/bongos/db-kernel.js`) is named for account creation but runs on **every authentication**, and its `ON CONFLICT (github_id) DO UPDATE` re-set `display_name = EXCLUDED.display_name` unconditionally — where `EXCLUDED` carries whatever the caller just read off GitHub. All four login paths feed it (`pollDeviceFlow`, `completeWebFlow`, `completeIdpFlow`, `completeDeviceExchange`), so CLI re-auth alone was enough to undo the save the same day. **Decision:** the row records *who last set the name*. A new core column `builders.display_name_source` (`'github' | 'user'`, migration `core_197`) is stamped `'user'` by `setBuilderDisplayName` — the only self-rename path — and the upsert's conflict branch guards the assignment with `CASE WHEN builders.display_name_source = 'user' THEN builders.display_name ELSE EXCLUDED.display_name END`. GitHub keeps seeding and re-syncing the name for everyone who never renamed; it never overwrites one the builder chose. `github_login`/`avatar_url` re-sync unchanged. **Rejected:** the one-line fix of deleting the assignment entirely — it freezes `display_name` at signup for *everyone*, so a GitHub rename (or a bare login seeded because the account had no GitHub display name) would never propagate again; that trades a visible bug affecting the few who renamed for a silent one affecting the many who never opened Settings. Not surfaced in `GET /me/display-name` or the OpenAPI spec (internal persistence state, no UI consumer). The federated hub's `platform_identity_accounts` needs no matching guard — the `CASE` runs regardless of what `EXCLUDED` carries, and that table is identity-only with no self-service editor. `tests/display_name.mjs` previously covered only the pure validator, which is why this shipped; it now reads the real SQL and asserts both directions.](0156-user-set-display-name-wins.md) | GDS / builder experience |
238
+ | 0157 | [**Archon is rank and identity only; the operational block delegates to Metic** ([task 1002584](https://cloudbongos.com/builders#/task/1002584), owner decision 2026-08-05). Archon had accumulated **50 route gates**, almost none about *authority* — reading the roster, listing dev boxes, viewing the audit log, adjudicating a vuln report, tearing down a droplet. Each arrived Archon-gated by [ADR 0016](0016-trust-boundary-server-enforced-permissions.md)'s fail-closed default for new privileged routes; nothing came back to lower them, so “fail closed” became “the owner is the only operator”. Generalises the one-power delegations of [ADR 0090](0090-metic-task-authoring.md) and [ADR 0152](0152-metic-task-abandonment.md) instead of relitigating route by route. **20 permissions move archon→metic** — the governance pages (`/watch`, `/harbor`, `/gate`), session search, the dev-box + provisioning fleets, security adjudication, the audit log, roster reads, cross-builder memory reads, project curation, budgets, idea ratification, override decisions, release-on-behalf, criterion unsatisfy, goal reorder, version create, newcomer restock, peer-vote tally, Discord channel ops, autonomy precheck. **Only possible as a 1-file change** because BV1.R104/R105 had just migrated the gates to `requirePermission`, so the floor is declared once in `catalog.js` — the same decision was a 41-call-site diff a day earlier, and that is the concrete [ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) payoff. **13 stay Archon:** rank (`builder.rank.set`), identity (status/admission/session-revoke), and — refusing the literal reading of the directive — **four escalation keys** (`task.confirm.grade_bypass`, `gate_approval.decide`, `goal.scope.manage`, `governance.manage`) that form a chain to self-promotion: scope a goal into the permission core → claim a task there (protected paths already floor at *metic* per [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md)) → confirm past the failed grade → approve your own red CI gate → it lands on `main`. Moving all 20 would have made the delegation **self-reversible**, so keeping rank Archon-only depends on keeping those four Archon-only; any future delegation must re-run that check. Also held back: `version.close` (paired with rank as the powers that never delegate) and `dependency.cross_tier.manage` (`system: true` — clearing that flag to move it would widen what any CUSTOM role can hold, a second-order change nobody asked for). Mints **`gate_approval.read`** so the Gate page opens at Metic while approving stays Archon, and finishes the three reads R104/R105 skipped — otherwise the read would be **stricter than the write** (a Metic able to confirm a vuln report but not list one). Route pins move from ranks to `perm:` atoms so a floor change needs no pin edit. **Two flagged consequences:** `/security/docs` (attacker-usable exploit detail) is now Metic-readable, since it shares `security.report.adjudicate` — the one delegation widening who may *read* rather than *act*; and [ADR 0154](0154-goal-owner-criterion-authoring.md)'s “the backward ratchet stays Archon” carve-out is superseded (satisfy/unsatisfy stay separate atoms, so confirming still never confers un-confirming). ADR 0016 is untouched end to end — floors move *inside* the boundary, never the enforcement point.](0157-archon-is-rank-and-identity-only.md) | governance / permissions |
239
+ | 0158 | [**The choke-point verdict taxonomy, and why a verdict never executes itself** ([task 1002598](https://cloudbongos.com/builders#/task/1002598)). [ADR 0157](0157-archon-is-rank-and-identity-only.md) delegated 20 permissions on one observation — each had been Archon-gated by [ADR 0016](0016-trust-boundary-server-enforced-permissions.md)'s fail-closed default, *never by a decision* — but applied that reasoning once, to a known backlog, without recording it as a **method**. Two things now make the question tractable: a **second Archon exists** (until 2026-08-06 "needs an Archon" and "needs Lars" were the same sentence, and a large share of past escalations reclassify on that distinction alone), and post-[ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) a floor is declared once, so classifying exhaustively is cheap. **Decision:** six ordered verdicts, first match wins — GAP · UNINTENDED-MISSING-SURFACE · DRIFT · STRUCTURAL · INTENDED · UNINTENDED-OVERTIGHT. Order is load-bearing: GAP leads because arguing a wall's rank while nothing enforces it is the costlier mistake, and UNINTENDED-OVERTIGHT is last because its remedy is the only one that *weakens* a boundary. DRIFT is never terminal — a DRIFT row must name what it collapses into. **The load-bearing rule: a verdict is advisory and never self-executing.** Recording UNINTENDED-OVERTIGHT is not authorization to lower a floor; that still needs an owner decision + `catalog.js` + a migration (the `governance_002` seed is run-once, so an edited `floor:` never reaches a migrated DB) + an ADR. A document that could move a floor by asserting a verdict would be privilege escalation through markdown — the audit describes, the database enforces. Every row carries a rigor marker (`obs` / `read` / **`absent`** / **`unver`**); the last two earn their own names because the two highest-severity findings in the first map are *absences* (a pre-push hook never once in git history; an "authoritative" audit script with no caller) and the sharpest structural one is an `unver` — the layer [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) designates authoritative lives entirely outside the repository that documents it, so the trust model cannot be audited from the artifact that states it. Two methodological constraints, both learned the expensive way: **an Archon cannot audit the ownership walls** (`gateTaskOwnership` short-circuits on `rank === 'archon'` before the ownership lookup, so an Archon probe passes all 7 routes and observes nothing — which is why the single-Archon era *hid* a class of wall rather than merely queueing it), and **send two bodies per write** (16 of 29 in-handler-gated routes return 400/422 on `{}` before the authorization check, so a `{}`-only sweep concludes there are no in-handler walls). Produces [`docs/governance/<redacted>.md`](../governance/<redacted>.md). ADR 0016 and 0043 untouched — this adds vocabulary for *describing* the boundary; it moves no floor and changes no enforcement. **Rejected:** generating the map from the route census (a census answers "where is the gate", never "should it be there", and generating it would imply otherwise); folding verdicts into `canonical-permissions.md` (that file mirrors the enforced model — mixing judgments *about* it into the description *of* it is how a doc starts reading as authority); auto-filing a task on UNINTENDED-OVERTIGHT (a queued privilege change nobody decided); and collapsing to three verdicts (it merges GAP into UNINTENDED-OVERTIGHT, inverting the remedy — an unenforced wall needs a gate *added*, an over-tight one needs a floor *lowered*).](0158-choke-point-verdict-taxonomy.md) | governance / methodology |
240
+
241
+ | 0159 | [**Merge-gate reconciliation: required checks match reality, CODEOWNERS becomes real, protection stays the owner's switch** ([task 1002611](https://cloudbongos.com/builders#/task/1002611)). Findings D-12 + G-5 from the choke-point map: `gating.js` hardcoded three required status checks (`unit`, `trufflehog`, `gate-review`) of which two have never had a workflow — running `gating.js on` would have required contexts nothing can report, blocking every merge indefinitely instead of gating them — and CODEOWNERS was described as a live control by ADR 0042, `protected-surfaces.json`, `publish-manifest.js` and gating.js itself while not existing anywhere in the tree. Now that the `unit` lane exists (task 1002610), `DEFAULT_PROTECTION` requires ONLY `[unit]`; the future contexts are fixed as `secret-scan` + `gate-review` (the `CORE_EXPECTED_CHECKS` names — a context is named for what it asserts, not the tool asserting it) and return when their ADR 0058 workflows ship; CODEOWNERS becomes a real root file scoped to `gate-review.js`'s `HARD_FLOOR_GLOBS` + gating.js (inert until protection is on); branch protection itself stays OFF — flipping it remains the owner's explicit act, this only makes the switch safe. Rider: the unit lane is hardened so `unit` is actually requirable — `fetch-depth: 0` (depth-1 broke `tests/main_audit.mjs` on the lane's first run), two clean-env-red tests parked in `KNOWN_FAILING` with diagnoses + exit tracked in [task 1002617](https://cloudbongos.com/builders#/task/1002617), and `publish_manifest` (the public-mirror boundary test) fixed in-claim rather than parked — its ≥7-host-tests non-vacuity floor went stale when ADR 0107 moved host tests into `modules/<key>/tests/`, replaced by a regex specimen self-test. Rejected: deleting `GATE_WORKFLOWS`' forward-looking names (the switch handles absence gracefully; deleting them un-covers the workflows the moment they ship) and watering down the four CODEOWNERS descriptions instead of creating the one file.](0159-merge-gate-reconciliation.md) | governance / build pipeline |
242
+
243
+ | 0160 | [**The authoritative main audit gets a repo-verifiable caller** ([task 1002612](https://cloudbongos.com/builders#/task/1002612)). Findings G-3 + G-8, honest limit S-1: `main-audit.js` — the post-push layer [ADR 0043](0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md) designates authoritative — had two declared invokers that never existed in this git history, and `audit-authorship.sh` (the no-hardcoded-Archon-identity enforcer) had no caller either and turned out red with 340 pre-existing hits on untouched main (idea 1000653). Now a `main-audit` workflow audits every push to `main` over exactly the pushed `before..after` range (clamped-window fallback); the roster comes from the public leaderboard when no session exists (server-authoritative, login-only keying, an absent builder resolves as unresolved — never falsely trusted); new `--require-roster` makes an empty roster a loud DID-NOT-RUN exit 2 (an audit that cannot fire a blocker must not exit green); the lane runs non-strict per the audit's own zero-false-positive calibration; and `audit-authorship.sh` rides as an advisory job until the 1000653 triage. Detective lane, not a merge gate — deliberately outside the required PR contexts. Rider: the known-failing ratchet caught task 1002611's publish_manifest fix exactly as designed — entry removed, ceiling ratcheted 3 → 2, remaining entries re-owned to [task 1002617](https://cloudbongos.com/builders#/task/1002617), idea 1000542 superseded. Rejected: a GDS token secret (silent-fail dependency the devcontainer lane warned about), `--strict` in CI (permanent red rots into wallpaper), enforcing authorship before triage.](0160-main-audit-repo-verifiable-caller.md) | governance / build pipeline |
244
+ | 0161 | [**Publish on merge: every merge to core `main` auto-publishes `@bongos/core`** (task 1002620). Supersedes ADR 0137's manual owner-gated publish after its own "when to revisit" clause fired twice in three weeks: 171 commits/10 days, then 104 commits/5 days, sat on `main` reachable by no pin while every layer reported success. A `publish` workflow rides behind the required `unit` check (`workflow_run`, success-gated): when HEAD's `CORE_VERSION` is already on npm, CI bumps a PATCH via the shape-preserving `bump-version.js` (+ changelog line; pinned by `tests/bump_version.mjs`) and pushes the carrier commit; `package-core --self-verify --publish --live` builds through the fail-closed no-leak gate; the release is tagged `core-v<v>`. A MINOR/MAJOR stays a human in-task edit — it decides what patch-channel subscribers receive unattended (ADR 0136), a call CI must not make. Token custody knowingly reverses (owner-accepted): `NPM_TOKEN` write + `RELEASE_PUSH_TOKEN` PAT as Actions secrets, mitigated by the lane shipping INERT behind the `PUBLISH_ON_MERGE=1` repo variable — an owner kill-switch symmetrical to `gating.js`. Idempotent by version-compare; npm duplicate rejection is the backstop; queued no-cancel concurrency collapses merge races.](0161-publish-on-merge.md) | build pipeline / distribution |
245
+ | 0162 | [**Review gates retired: rank-consistent CI checks only on `main`** ([task 1002635](https://cloudbongos.com/builders#/task/1002635); owner directive, idea 1000657). Amends ADR 0159. Within a day of task 1002619 arming the merge gates, the code-owner review requirement had stranded two graded-PASS wave-4 ships (PR 28 sat BLOCKED behind a review only the repo-owner account could give) and then blocked the fixes for the strands it caused — structurally, not incidentally: GitHub can only express the gate as *accounts* (no rank concept, no self-approval), so on a solo-archon instance it degenerates into "one named login must click every gate-surface PR" — identity-pinned authority, against ADR 0016's rank principle and the remediation's own goal. Decision: branch protection carries required status checks ONLY; `gating.js` `DEFAULT_PROTECTION` sets `required_pull_request_reviews: null` and `cmdOn` normalizes BOTH restore paths so a pre-retirement snapshot cannot resurrect the gate (`tests/gating_review_retired.mjs` pins it with a counter-example); the CODEOWNERS *file* is deleted while its *glob* stays in the hard-floor lists so reintroducing it is itself gate-flagged. Accepted trade, chosen explicitly by the owner: no pre-merge human approval on safety-machinery changes — the unit check, the fail-closed gate-review sandbox, the grader panel, and the every-push main-audit are the net, catching a compromised session's merge after the fact rather than before.](0162-review-gates-retired-rank-consistent-ci-only.md) | governance / build pipeline |
246
+
247
+ | 0163 | [**Pre-197 display names: a transitional 'legacy' source, resolved at first sign-in** ([task 1002643](https://cloudbongos.com/builders#/task/1002643); idea 1000659). Amends [ADR 0156](0156-user-set-display-name-wins.md), whose migration defaulted every pre-existing row to `'github'` — false for builders who renamed after their last sign-in but before 197 landed, so their names kept reverting. No audit trail exists to backfill precisely, so `core_214` marks every pre-197 `'github'` row `'legacy'` (scoped by 197's `applied_at` on the instance — portable) and the upsert resolves each at its first sign-in: stored = incoming GitHub name → provably GitHub-derived → `'github'`; differs → treated as builder-set → kept, stamped `'user'`. Accepted misclassification: a GitHub-side rename in the window freezes a stale label (visible, self-fixable in Settings) — preferred over silently destroying deliberate renames. Rejected: `display_name <> github_login` heuristic (freezes nearly everyone), comparing forever on all `'github'` rows (turns every future GitHub rename into a frozen name).](0163-legacy-display-name-resolution.md) | GDS / builder experience |
248
+ | 0164 | [**The implausible-pass guard reads substance + evidence; the Metic+ honor-system residual is explicit** ([task 1002658](https://cloudbongos.com/builders#/task/1002658); grader-hardening goal 1000056). The guard fired only on `issues.length===0` — one fabricated `{}` defeated it, its magnitude inputs are client-supplied, and every in-sample firing (eval 2-for-2, tasks 1002643 ×2, 1002663) was a false positive on an honest unanimous zero-issue panel PASS. Redesign: count only SUBSTANTIVE issues (known severity + non-empty summary — padding trips the guard regardless), and waive the zero-issue pass when `signals.workers` carries the full &ge;3-worker panel shape, stamped `implausible_pass_waived` for /grade-audit. The residual is named, not hidden: every grade-time input is client-authored, so plausibility at grade time is honest-mistake hygiene; the binding authorities on the real diff are land-time (CI unit, gate-review, main audit) — Metic+ grade-time honesty is accepted honor-system per this ADR. The ADR-0042 CI grade-gate (land it vs record permanent deferral) goes to the owner as a blocker, not decided here. No approval step added or removed (ADR 0162).](0164-implausible-pass-guard-substance-and-evidence.md) | GDS / grading |
249
+ | 0165 | [**Instances are hookless by default: no scaffolded `.husky/`; local hooks stay an instance-owned opt-in** ([task 1002641](https://cloudbongos.com/builders#/task/1002641); idea 1000655). Task 1002608 delivered the ADR 0043 Layer 1 pre-push gate for the CORE repo, but nothing materializes `.husky/` on a scaffolded standalone instance (`publish-manifest.js` excludes it by design). Decision: that is the accepted posture, not a gap. Sub-Metic instance builders hold no push credential at all (server-mediated publish, ADR 0053/0055) — a client pre-push hook guards a push that cannot happen; a client hook is not a security boundary (ADR 0016); the core cannot maintain hooks it deliberately does not ship (scaffolded hooks would be born orphans — the audit-authorship rot lesson, task 1002640); and ADR 0162 puts gates in CI, rank-consistent and hands-free. Secrets hooks follow the same rule (the PR-time CI secret scan enforces). The opt-in stays real and instance-owned: an instance commits its own `.husky/` and the core's `install-git-hooks.js` (already run by `/builder-setup`) activates it. Rejected: scaffolding wrappers at init, an `--with-hooks` flag (speculative), treating hooklessness as a regression. Revisit if an instance model has builders pushing directly with their own credentials.](0165-instances-hookless-by-default.md) | provisioning / trust boundary |
250
+ | 0166 | [**Module maintenance & deprecation policy: upkeep is a declared `maintenance` block, and `bongos upgrade` surfaces rot in DISABLED modules too** ([task 1001773](https://cloudbongos.com/builders#/task/1001773)). Implements [ADR 0107](0107-module-upstreaming-contribution-to-core-catalog.md) §7, which named the catalog-rot problem and deferred the mechanics. `module.json` gains an optional-in-schema, required-to-upstream `maintenance` block — `status` one of `core-maintained` / `maintained` / `deprecated` / `orphaned`, plus `since` / `removeAfter` / `successor` / `note`. Each status owes something the validator enforces: `maintained` requires a named top-level `maintainer` (an unnamed owner IS the orphaning failure), a sunset status requires a deprecation path, and `removeAfter` is rejected on a non-sunset status. `orphaned` is deliberately first-class — a policy that only permits happy statuses gets satisfied by lying. The load-bearing fix: accepted modules ship `default:false` (§6), so every rotted catalog entry is DISABLED by construction — and `preflightModules()` returned only `blocking`, discarding `warnings`, so a real `bongos upgrade --to X` reported `"all enabled modules satisfy the target"` while saying nothing about them. It now returns + prints `warnings`/`deprecations`/`expired` with the maintainer NAMED, before the blocking check returns (so a refused bump reports them too). Enabled mismatches still refuse; disabled ones still never block — report, don't block, because blocking an instance's bump on a catalog entry it never opted into makes the shared catalog a liability. `bongos module check`/`submit` refuse an undeclared posture or an already-sunset module; `GET /modules` + the hall Modules tab show an `upkeep:` chip (incl. `undeclared`); all 17 bundled modules stamped `core-maintained` with a unit test holding the line. Rejected: blocking on disabled rot, a required manifest field (breaks every existing manifest), DB-tracked upkeep (must travel with the module, ADR 0107 §2), auto-deprecating on incompatibility (a fact is not a decision), a separate MAINTAINERS file.](0166-module-maintenance-and-deprecation-policy.md) | modules / upstreaming |
251
+ | 0167-a | [**The module catalog: `source` (which tree ships it) and `provenance` (who built it) are two axes** ([task 1001772](https://cloudbongos.com/builders#/task/1001772) — the last task of goal 1000029). Closes [ADR 0107](0107-module-upstreaming-contribution-to-core-catalog.md) §2/§3: a browsable catalog of enable-able core modules, each with author + origin credit, in the hall AND via `bongos module list --catalog`. The bug it fixes: the Modules tab derived its own/core-loaded split from the manifest's `origin` alone, but attribution is OPTIONAL and core's own modules declare none (nothing upstreamed them) — so on a split instance every core module read as *built here* and the catalog you could opt into read as empty. One field was answering two questions. Fix: a shared model (`src/module-loader/catalog.js`) that both the hall route and the CLI build from, carrying `source` — read STRUCTURALLY from the loader's discovery root ([ADR 0108](0108-instance-composition-configurable-root.md) §1: core package vs instance tree), falling back on coincident roots to whether `package.json` names the core — beside the unchanged `provenance` credit axis. Plus `credit: {author, origin, inferred}`, which fills a core module's blank attribution with the platform name and MARKS it inferred (`*` in the CLI, a hoverable `<abbr>` in the hall) so it never reads as a claim the author typed. The tab now sections by `source` ("Module catalog" / "Built by this project"), gains a search box, and draws "Submit for review" from instance-source modules only. Rejected: stamping author/origin into 15 core manifests (repetitive, and module 16 forgets), overloading `origin` (destroys the credit axis), inferring from `default:true` (§6 makes accepted catalog modules default-OFF — exactly backwards), a DB catalog table (premature; the manifests already are the catalog).](0167-module-catalog-source-vs-provenance.md) | modules / upstreaming |
252
+ | 0168 | [**The scaffold vendors the hub's RUNNING core (`npm pack` of the installed package); registry-default deferred to the go-public step** ([task 1002706](https://cloudbongos.com/builders#/task/1002706); gap 13 of the project-creation audit). Two collided facts: children were born pinned to a frozen `file:vendor/bongos-core-1.19.2.tgz` (installedCoreTarball reuses the hub's own file: pin — task 1002275 — and the hub's pin sat at 1.19.2), and the task-1002621 registry cutover (hub pin now `"@bongos/core": "1.19.56"`, verified on the box) made installedCoreTarball return null — dropping the scaffold into the package-core.js source rebuild that ABORTS on a consumer instance (empty core set → empty child repo, the 1002275 failure resurrected). Fix: a three-rung ladder — file:-pin reuse → NEW `installedCorePackDir()` + `npm pack` of the hub's installed core (child pins the SAME, CURRENT core, stays file:-pinned, owner needs no token) → source rebuild. The brief's "make registry the default now that the package is public" was verified FALSE (both npm scopes 404 anonymously; ADR 0134 keeps them private until go-public) — a registry-default child would be born UNINSTALLABLE for a tokenless owner. Registry-default is deferred to the go-public step (idea 1000700); the gap-13 "flip the box env" ops leg is explicitly a no-op until then; child freshness rides this ladder + the hub-pin lane (task 1002708). Rejected: registry-default now, dropping NPM_TOKEN, vendoring from the npm cacache.](0168-scaffold-packs-installed-core.md) | provisioning / distribution |
253
+ | 0167-b | [**The /create gate trust link defaults to the AGPL-3.0 license text, not the npm package page** ([task 1002705](https://cloudbongos.com/builders#/task/1002705); gap 1 of the project-creation audit). The gate's "open source" link pointed at a nonexistent repo (404); the task brief's suggested default — the npm package page — is ALSO dead for the gate's actual audience (signed-out visitors): `@cloudbongos/core` and `@bongos/core` both 404 anonymously (private scopes, ADR 0134), and the ADR 0099 public mirror stays dormant until ~2026-11. The license text is the one live, instance-neutral artifact behind the claim today. The browsable-code upgrade is the owner's filed choice (idea 1000700): public repo now (forfeits the ADR 0065 §4 redaction window) / public npm package / wait for the mirror. Rejected: the npm link (verified dead), flipping the repo public (not a builder's call), plain unlinked text (less information, same cost).](0167-gate-trust-link-agpl-default.md) | public landing / governance |
254
+ | 0169 | [**`bongos serve`: the `bongos dev` boot path, on a real host** ([task 1002398](https://cloudbongos.com/builders#/task/1002398), goal 1000052). [ADR 0149](0149-bongos-dev-local-launcher.md) gave an owner one command to stand an instance up on their laptop; "now put it on a server" had no equally good answer — Docker serves the *vanilla* brand on `:3000`, `bongos onboard` provisions a whole droplet through the control plane, and [`instance-deploy.md`](../recipes/instance-deploy.md) left the systemd unit as a hand-written prose step. The six steps (deps → Postgres → createdb → migrate → env → server → health) were already right for a host; only four *decisions inside them* were laptop-shaped. So: a `--server` **mode** on the same script (`bongos serve`), never a second boot path, so the two can't drift. (1) **Supervision** — a systemd unit (`Restart=always`, `WantedBy=multi-user.target`) instead of a pid file that dies on reboot, falling back to the pid-managed process where systemd is absent rather than refusing. (2) **Bind address, derived from the origin** — `https://…` means a proxy terminates TLS, so bind `127.0.0.1`; plain `http://` on a public host means nothing is in front, so bind `0.0.0.0`. The common misconfiguration (app publicly reachable on its raw port behind a proxy) becomes impossible by default. (3) **Peer-auth bootstrap** — a fresh `apt-get install postgresql` cluster has only the `postgres` role, so a deploy user's `createdb` fails with `role "deploy" does not exist`; that reads like a broken install and is really an unbootstrapped cluster, so we create the login role as the superuser. (4) **Health means the PUBLIC origin** — the loopback probe proves the app; DNS/proxy/TLS are where a host actually fails, so it probes the advertised origin too and names the missing layer (advisory — DNS may legitimately not be wired). Secrets go to a root-owned `0600` `EnvironmentFile`, never `Environment=` (`systemctl show` leaks those, and the laptop path's `buildEnv()` spreads the caller's whole shell). Every decision is a pure injectable in `dev-lib.js`, proved in `tests/dev_lib.mjs` with no host. Rejected: a separate `deploy.sh` (drift — the point is the *same* six steps), Docker-on-the-host (makes brand+port a compose override), always binding `0.0.0.0` (exposes the raw port), refusing without systemd (breaks containers/BSD).](0169-bongos-serve-server-bring-up.md) | onboarding / deploy |
255
+ | 0173 | [**The legacy multi-ship reward shortfall is NOT backfilled — it closes as a one-time transition cost** ([task 1002520](https://cloudbongos.com/builders#/task/1002520), goal 1000037). Before `core_196` the cost-plus reward booked ONE row per session, so a multi-ship session (the normal shape — ship, new worktree, claim again) was paid at its first ship's level and never topped up; `core_196`'s watermark accounting `(session_id, reward_basis)` fixed that forward and deferred the retroactive half to an "owner-signed-off backfill, tracked separately". This is that decision, and it is **decline**. Measured live 2026-08-16, both of the task's open questions answered: (1) the task's own premise is wrong — `reward_basis = 0` is the column's DEFAULT, so it means "booked before top-ups existed", not "underpaid", and **over-selects by ~60%** (478 legacy sessions, but 287 are single-ship and were paid in full; only 191 are candidates); (2) re-pricing each candidate's stored `session_records.model_usage` at `round(true_cost_usd × 1.20)` gives **7,697 credits owed** across 119 sessions — example-owner 5,296 (69%), jaxri 2,319, scootermack 82 — with **72 candidates unreconstructable** (empty `model_usage`) and 16 already OVER-paid by 1,131 that nobody proposed clawing back. Declined because it mirrors the ADR 0146 clawback's own logic (the accounting changed; neither side of the boundary reopens), because paying only the 119 that happened to leave a server-side trace is arbitrary against the 38% that did not, and because correcting shortfalls while ignoring overpayment is a policy choice the owner declined to adopt. Consequence recorded plainly: three builders stay permanently short. **Does NOT close task 1002448** — that is the Windows `encodeProjectPath` population, never *recorded* rather than paid at a stale watermark, still recoverable and still owed.](0173-legacy-multi-ship-shortfall-not-backfilled.md) | economy / rewards |
256
+ | 0172-a | [**The sub-Archon rank roles are owner-editable; the Archon role is not; the substrate keys are non-grantable to everything** ([task 1002961](https://cloudbongos.com/builders#/task/1002961), goal 1000030). Amends ADR 0151 §2's "the four seeded rank-roles are immutable through this API" wall — the GRANT half only. Their grants were always ordinary `governance_role_permissions` rows (the `governance_002` seed writes them); the read-only rendering was route policy, not structure. Three walls now, where there was one: the **Archon** rank-role stays frozen as the LOCKOUT GUARD (it is the only role holding `governance.manage` by seed, so freezing it means no click sequence can revoke the permission needed to reach this tab); the three **sub-Archon** rank-roles become grant-editable, the seven **in-app** Archon powers included (owner's explicit choice, escalation consequence stated); and `substrate` becomes a declared catalog axis — `deploy.push_main`/`path.protected.modify`/`infra.ssh` are non-grantable to EVERY role forever, because ADR 0043 enforces them out of band, so a grant row confers nothing and the toggle would be the UI lying. Grantability goes per-role (`isGrantableTo(key, role)`); `isGrantable(key)` survives as the narrow custom-role question config.js strips against. **Custom roles are deliberately NOT widened** — a rank-role moves only by a rank write, a custom role is assignable by anyone holding `governance.manage`. Assignment stays rank-derived (R95b) untouched. Adds `POST /governance/roles/:roleKey/reset` (one transaction, filtered through `isGrantableTo`, refused on a custom role) because a drifted ~60-row rank has no way back by clicking. Rejected: substrate keys behind a warning (a toggle that succeeds and delegates nothing), an editable Archon role behind a confirm (a confirm is not a guard; real recovery is hand-written SQL on the droplet). **Vocabulary amended by ADR 0174 (2026-08-16): governance→government, seeded roles→ranks; the decisions are unchanged.**](0172-editable-rank-roles-substrate-axis.md) | governance / permissions |
257
+ | 0171 | [**Profile ↔ cross-project rollup: the join runs in-process on the hub; consent is fail-closed account state; the hide flag lives on the account, enforced in the read path** ([task 1002277](https://cloudbongos.com/builders#/task/1002277), R01 of goal 1000045). The C1 cross-project profile reads the platform-identity rollup through the module's **declared-but-never-registered `platform-identity.hub` port**: the module registers a real provider and core `GET /builders/:id/profile` `resolveOptional`s it beside `reward` — zero network, `null` (clean degrade) on every non-hub instance. Federated instances **link out** instead of inlining (amends profile-page spec Decision 1: an instance-side copy is either stale-on-hide or a per-view hub call). Consent: `profile_state` on `platform_identity_accounts`, **fail-closed** — `active` iff touched by a hub-session `/sso/authorize` (device-poll/check-in create `provisional`; backfill = all provisional, lazy upgrade; no core-`builders` join — type-unsafe vs `system:*` ids and a module-migration boundary breach), so the P03 owner decision can only ever widen, never claw back. The C2 hide flag is `hide_stats` on the account (NOT per-instance `behavior_prefs`), enforced **inside** the module's read functions — `listScoutingRollup` itself gains the `active AND NOT hide_stats` predicate so the live Metic-floor `/people`+`/scouting` surfaces honor it by construction. Hub profile responses go `no-store` (hide must beat the 60s cache). Publishing is gated on hardening the self-reported rollup (membership-row requirement, rank-vocab validation, disciplines caps, check-in→rollup sequencing). Found + filed: the `LATERAL unnest` fan-out inflating rollup sums (task 1002911), the unreserved `u` slug (task 1002912), the missing platform T&C artifact (task 1002913), criterion amendments (task 1002914). Rejected: instance→hub per-view pull (always-on dependency), hub→instance mirroring (stale-on-hide), storing the flag in builder-settings, backfilling `active` from hub `builders` rows.](0171-profile-rollup-join-and-consent.md) | platform identity / profiles / privacy |
258
+ | 0170 | [**The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon** ([task 1002778](https://cloudbongos.com/builders#/task/1002778), goal 1000061). The hub card's metadata pass added `created_at` to the public `/projects/featured` projection, labelled **Listed** — it is the moment the hub catalogued the row, never when the project began, and the C6 card test bans the word "Created". Two recorded deviations: (1) the task brief said `updated_at` was ready to add, but the column is bumped by every client re-registration AND the Archon's feature toggle, so as a public "last updated" it would report curation noise as project activity — it may sort, never project, and the test pins the SELECT list itself (`SELECT *` banned: the route is CORS-open rank:public and a star would leak `owner_github_id`/`status`). (2) The owner fact reads the project's OWN leaderboard archon row (login + avatar, type-checked and capped) from the payload project-stats already fetches — zero extra requests, the 2-fetch pin intact, the advisory `owner_github_id` column still unread per the design contract. Multi-archon: top-credits archon is named; a fallen-off-the-page archon renders as no owner shown, never a wrong one. Rejected: projecting `updated_at` as asked (it lies), reading `owner_github_id` (advisory, contract-banned), a third per-origin fetch for activity recency (moves the test pin for a fact nobody asked for yet).](0170-featured-projection-honest-facts.md) | platform identity / public surface |
259
+ | 0172-b | [Per-craft compensation: the ideator credit lane, separate from cost-plus](0172-per-craft-compensation-ideator-credit-lane.md) | economy / compensation |
260
+ | 0174 | [**Governance is renamed government, its positions are ranks, and "role" is reserved for the three crafts** ([task 1003002](https://cloudbongos.com/builders#/task/1003002), R01 of goal 1000068). Owner directive, 2026-08-16. The word **role** named two unrelated things: the permission system's ladder tiers (`governance_roles`, the "rank-roles") and the three crafts (`tasks.discipline` = engineer/artist/ideator) — and goal 1000066 was actively claiming it for the crafts (`three-roles-only`, `users-hold-roles`), putting the two on a collision course. Resolved at full depth rather than as a label pass, because a labels-only fix leaves the UI and the code disagreeing, which is the condition that produced the collision. `modules/governance/` → `modules/government/`; the kernel module key, the config files, `/api/bongos/governance/*` → `/government/*`, and the permission keys `governance.manage` → `government.manage` (~50 sites) + `page.view.governance` → `page.view.government`. **The entity IS the rank — no second noun**: `governance_roles` → `government_ranks`, `role_key` → `rank_key`, `rank-roles.js` → `ranks.js`; "position" stays prose-only, since introducing it as a code term would recreate the qualifier problem "rank-role" already demonstrates. The cross-module kernel port `governance.roleSync` → `government.rankSync` must land atomically across its provider and both consumers (`modules/lifecycle/db.js`, `src/bongos/db.js`) because it is a transaction participant on the rank-write path. Hand-composed bundles stay in the one table as **custom ranks** — the owner's call over splitting them into a separate "appointments" concept, with the objection recorded (a custom bundle is not a ladder position, and ADR 0172 §4 already treats the two kinds as different). Enforced by a `fitness.js` rule, allowlist shipped with reasons. **Discord's server roles keep the word** — that noun belongs to Discord's API. Amends ADR 0151 + ADR 0172 (the editable-rank-roles one) in **vocabulary only**; ladder slugs, `builders.rank`, `requireRank`, and every floor/grant/assignment semantic are untouched. Rejected: renaming the ladder slugs (turns a bounded rename into a trust-boundary migration), a permanent compat shim (both vocabularies stay correct forever).](0174-government-terminology-ranks-not-roles.md) | government / permissions |
261
+ | 0175 | [**The Board Room: Full Idea ratification is the first upward-pointing gate** ([task 1003014](https://cloudbongos.com/builders#/task/1003014), R01 of goal 1000069). Every existing gate points DOWN the rank ladder; the owner asked for one that could slow THEM down — *"otherwise I can just ship and build as I want, finding loopholes."* The board's subjects are exactly **two**, both rare by construction: a **Full Idea** and a **change to the constitution itself** (`subject_type` from day one). Because the vote gates no build path, the **ADR 0162 tension dissolves rather than being compromised** — nothing self-executes because there is nothing downstream to execute; ratification confers legitimacy and does NOT auto-create the goal. A vote is a **timed sitting, not a quorum**: absent members are *nulled* (dropped from the denominator, never counted as a no), so the clock is the deadlock exit and no shotgun/fork-with-treasury clause is needed at this scale — default 90 minutes, closing early on full turnout, and yes, a short window means whoever is awake governs (the owner's explicit intent). **Consent, not majority**: silence is consent, one reasoned objection returns the idea, and a zero-vote close PASSES. A rejection must carry free text **plus one of the five Full Idea sections** — which is what **forces the hall to be the voting surface**, since a Discord reaction could only ever be feedback-free; Discord is therefore an outbound notification mirror that casts nothing, and its own mobile app is the mobile push (no mobile surface is built). The completeness grade is the **floor** (a window opens automatically on pass), the vote is the **merit**; concurrent windows are normal. Who governs is **configuration** — day one is monarchy expressed as config on the real machinery, never a separate code path — and **amendments are decided under whatever rule is currently in force**, so flipping out of monarchy is a legal monarchy act and no bootstrap exception is needed. Open ballot; an author may never vote on their own item. Franchise derives from **rank, not role**, and that is forced: `PATCH /me/disciplines` is deliberately ungated, so a craft-scoped chamber would be one-request privilege escalation. Unblocks goal 1000067 / IC5, which consumes the pass event and *"invents no parallel vote"*. Rejected: quadratic vote weighting and the attribution-economy knobs (γ, τ, the 30% stack cap — v1's franchise is not credit-weighted at all), eternity clauses, and any legal-entity integration.](0175-the-board-room-full-idea-ratification.md) | government / ideas / governance |
262
+ | 0176 | [**Private-repo standup: a per-repo read-only deploy key, minted with the owner's token** ([task 1002735](https://cloudbongos.com/builders#/task/1002735), goal 1000032; walk finding N4, seen live on `mercury` 2026-08-17). A standalone instance's repo is cloned/pushed once with the owner's `repo`-scoped token (ADR 0155), but the durable `origin` rides the shared SSH deploy alias — which GitHub answers "Repository not found" for a private repo it isn't registered on, so every private standup failed at the recurring `git pull --ff-only origin main`, **token or not**. Fix: when the target repo answers *private* to the owner's stored token, provisioning mints the instance its OWN ed25519 deploy key beside the checkout (`.git/cloudbongos-deploy-key` — untracked, survives `git reset --hard`, dies with the checkout), registers it on the repo **read-only** as `cloudbongos-deploy-<slug>` (idempotent by title; a stale same-title key is replaced, never duplicated), then wires the CHECKOUT: `origin` re-pointed at plain `git@github.com` and `core.sshCommand` pinned to the key with `-F /dev/null` so no `~/.ssh/config` identity can shadow it. Public repos take the unchanged shared-alias path; pushes keep preferring the owner-token HTTPS URL (task 1002317), which is why read-only suffices; teardown best-effort revokes by title. Rejected: **GitHub App installation tokens** (hourly expiry means every unattended pull needs a minting sidecar or a hub round-trip) and **registering the shared platform key on the repo** (one shared secret granting the whole platform durable access to private code; per-repo keys keep the blast radius to one repo and are visible to the owner in their own repo settings). Follow-ups: the manage page's flip-to-private stays refused until already-provisioned instances get the same wiring, and builder dev boxes of a private-repo project need their own leg.](0176-private-repo-deploy-keys.md) | provisioning / security |
263
+ | 0177 | [**A goal membership is a `membership_kind`, and the rename ships expand-then-contract** ([task 1003013](https://cloudbongos.com/builders#/task/1003013), goal 1000068). ADR 0174 reserved `role` for the three crafts and enforced it on the GOVERNMENT surface only — by design, since `builders.system_role` and Discord's own noun are legitimate. That left a **fourth** use uncatalogued: `goal_members.role` (`'lead' | 'member'`), the MANAGER predicate behind goal authorization. Renamed to **`membership_kind`** — values unchanged, because the noun was the problem; `rank` is wrong (not a ladder position) and `position` is worse (ADR 0174 uses it as the prose word for a rank, recreating the "rank-role" qualifier collision). A boolean `is_lead` was seriously considered and rejected: the text rename keeps the copied CHECK and stays a reversible change on a live authz column, where `ADD boolean; UPDATE; DROP text` is data-destructive. The migration is **expand-then-contract, never `RENAME COLUMN`** — auto-rollback reverts the code pin and NOT the DB, so `core_220` ADDs the column and keeps the pair in lockstep with a BEFORE trigger (direction decided by which side changed), and a later task DROPs the old column with the `-- forward-unsafe` pragma, a backup, and its own window; no `SET NOT NULL` in step 1, because a static NOT NULL on a column the previous release never heard of is what the forward-safety gate rightly flags. The API field renames with it and carries **no `role` alias** (ADR 0174's Lineage lesson: a time-boxed shim outlives its release), pinned by the test of its absence. The reservation gains a **second narrow wall** rather than a widened first one — line-scoped inside the 5k-line `db.js` to the lines that name `goal_members`, so no allowlist is needed. A **fifth** use (`project_memberships.role`) is catalogued and filed, with this as its pattern.](0177-goal-membership-kind.md) | naming / lifecycle |
264
+ | 0178 | [**The copy desk: a flag carries a judgement, and no column here could ever hold copy** ([task 1003113](https://cloudbongos.com/builders#/task/1003113), R02 of goal 1000074). [ADR 0081](0081-tool-agnostic-design-layer.md) made the repo the source of truth for design TOKENS; user-facing TEXT had no equivalent. R01 built the inventory (`copy-inventory.js` → `docs/copy-registry.json`), which answers *what does the product say* but cannot answer *what is wrong with it* — a judgement a person must record. This is that record plus the artist's queue. A new default-on module keyed **`copy-desk`, not `copy`**, for a concrete reason: the branding contract already owns a `copy` block, so the bare word reds the `core carries no module code` fitness check on the English word in `src/branding.js` — and the editorial "copy desk" is the more accurate name anyway. **The reason is required in the route AND in a CHECK constraint**: the route gives a person a usable error, the constraint means no future write path can create the reasonless flag the queue exists to not contain (a pointer with no judgement is *worse* than no flag — it still costs the artist a look). Closing requires a note by the same symmetry, since an unexplained close is indistinguishable from clearing a queue to move a number. The load-bearing decision is **NO LIVE CMS enforced by the schema's SHAPE**: `copy_desk_flags` has no column that could hold replacement text and must never gain one, because that non-goal does not erode via an "add a CMS" commit but via one plausible column, then one plausible read of it — `copy_no_cms.mjs` pins the write surface at exactly two verbs so R03's proposal path changes it in a reviewable diff. Rank is asymmetric: **filing is open to every rank** (the engineer who wrote the toast and the newcomer who could not read the button are the likeliest noticers, and neither is the artist — the reason floor is the quality gate, rank is not), closing is Metic+ **or the author** (withdrawing your own flag is a correction, not triage); nothing gates on craft, since an artist is a craft and not a rank. Duplicates are the only automatic signal (the one of stale/duplicated/off-voice needing no judgement layer) and the count **subtracts settled work**, or it never moves and teaches that the queue is decorative. A vanished target is **reported, never auto-closed** — the id is a hash over the text, so an edit routinely orphans a flag, and closing a human judgement because a hash moved is the family of error ADR 0162 rules out. Deliberately reports **no `generated_at`**: R01's contract is determinism, so a clock is the one field it cannot carry.](0178-the-copy-desk-flag-and-queue.md) | design / artist loop |
265
+ | 0179 | [**`rank:metic+` means metic and above; a bare `rank:metic` still means exactly metic** ([task 1003094](https://cloudbongos.com/builders#/task/1003094), goal 1000069). [ADR 0175](0175-the-board-room-full-idea-ratification.md) gave the constitution ONE membership form, `rank:<key>`, resolved by an EXACT SQL match — so `rank:metic` seated metics ONLY (excluding Archons), `rank:xenos` seated NEWCOMERS ONLY rather than "everyone", and "Metic and above" was inexpressible. Everywhere else a rank is a LADDER and authority is cumulative (ADR 0034 inheritance, requireRank('metic','archon'), the floor-derived seed); board membership was the one place it meant "exactly", and nothing said so. Rehearsing R25 on a real Postgres, an Archon ratified `rank:metic` under monarchy — the apply worked perfectly — and the next vote came back 403 not_board_member: **the owner flipped the switch and fell off their own board**, with the board deadlocked at one member because an Archon can still PROPOSE but no longer VOTE. Owner decision: EXPLICIT GRAMMAR. `rank:metic+` (that rank and above), `rank:metic,archon` (union), and **a bare key still means EXACTLY that key** — rejecting the smaller diff (silently redefining the bare form) precisely because it would have changed the meaning of every already-ratified constitution, the monarchy default included, on upgrade: nothing in a government should change meaning without a vote. `+` is defined only on the STANDARD ladder — "and above" about a custom rank with no ladder position is a parse REFUSAL, not a guess, and a refusal seats nobody. The grammar had been written TWICE (a shape regex in the config sanitizer, a parse regex in the resolver, pinned by a property test) — sound for one literal form, unsound the moment a rule needs a LOOKUP, which is the same class of drift that produced the bug; both now call one pure `membership-predicate.js`. Adds the DISENFRANCHISEMENT GUARD: a proposal that would unseat its own proposer is refused before any write, naming the one-character fix, overridable with an explicit `acknowledge_self_removal` (a separate parameter, never a proposal field) because stepping back is a legitimate act. The two DB reads take a set via `= ANY($1)` + DISTINCT (a double-counted member silently raises the consent bar), the structural filters are re-proven against real Postgres (a deactivated builder and a machine principal still never govern), and the hall panel now describes every form in plain language — rendering `rank:metic+` as "the metic+ rank" would be the same bug one layer up. Unblocks task 1003095, which can now write each charter's TRUE predicate rather than wording around a limitation.](0179-board-membership-predicate-grammar.md) | government / the board |
266
+ | 0180 | [**Dependency intake & drift policy: baseline-ratchet audit gate, install scripts off, cooldown via patrol, OIDC publish** ([task 1003127](https://cloudbongos.com/builders#/task/1003127), goal 1000076). The red-team baseline audit found 18 advisories (2 high) sitting unwatched in 9 production deps, the `undici ^6.27.0` override pinned INSIDE a vulnerable range while also breaking Node 26 CLIs, and nothing gated what an agent may install — against a 2025 npm threat wave (chalk/debug, Shai-Hulud, Nx) that ran on postinstall scripts and long-lived publish tokens. Four decisions. (1) `scripts/gds/audit-gate.js` runs in the required `unit` check: a high/critical advisory NOT in the committed `config/audit-baseline.json` fails the merge gate; the baseline is **shrink-only pre-gate debt** (10 findings at adoption) — adding an entry to silence a NEW advisory is out of policy. Fail-OPEN on registry/infra errors, fail-CLOSED on findings: the gate catches new advisories, it does not prove registry uptime. Notably a blanket `npm audit fix` cannot even run here (`workspace:` protocol, EUNSUPPORTEDPROTOCOL) — fixes are explicit tasks. (2) `.npmrc ignore-scripts=true`: the tree needs no install-lifecycle scripts — sharp installs prebuilds via optionalDependencies, proven by a script-less `npm ci` + full 430-test suite; a future dep needing one is an ADR-level exception. (3) Update cooldown is PATROL-enforced (task 1003128 flags versions younger than 7 days + stale overrides + action-pin drift) rather than `.npmrc`-enforced, because CI's npm 10 warns on the npm-11 `minimum-release-age` key for every builder; move it into `.npmrc` when the toolchain floor reaches npm ≥ 11.10. (4) Publish auth moves to npm **trusted publishing (OIDC)** — needs a one-time owner registration on npmjs.com (blocker filed); until then the granular 90-day `NPM_TOKEN` is a documented exception.](0180-dependency-intake-and-drift-policy.md) | devsecops / supply chain |
267
267
  | 0181 | [**Teardown accepts every state but `torn_down`, and deliberately keeps the address** ([task 1003042](https://cloudbongos.com/builders#/task/1003042), goal 1000058). Walk finding N5: an instance at `status='error'` could never be removed — the owner's teardown answered 409 `not_teardownable` and the archon `force-teardown`, which exists as the escape hatch, answered the SAME refusal, because both read one hand-kept whitelist `['provisioning','active','tearing_down']` that omitted `requested` and `error`. No role had a path out, so errored rows accumulated permanently (instance 15 `swag` from 2026-08-07). The whitelist justified itself as "nothing running to tear down", which is FALSE for `error`: a provision failing mid-run lands there AFTER it may have created the database, taken a port, and written the systemd unit, Caddy block and DNS record — a partial standup is the thing most in need of reclaiming. `TEARDOWN_FROM` is now DERIVED from `INSTANCE_STATES` minus `torn_down`, so a future state is teardownable by DEFAULT — the bug was an allow-list that silently omitted states, and for a failure mode whose signature is "a row with no exit", the safe default is that an exit exists. The runner needed no change (its steps are each `allowFail`); only the web guard refused. The one surviving refusal is now ONE shared helper: the copies had drifted, and force-teardown's positional `res.fail` form put the bare code `not_teardownable` on the wire as the human text. **The `port` and `domain` are deliberately NOT released** — releasing the port alone would be WORSE than the status quo, because the `<slug>.caddy` block survives teardown and proxies the hostname to a PORT NUMBER, so a later instance given that port would answer at the torn-down project's still-live address (verified live: two projects torn down 2026-08-13 still resolved on 2026-08-19). Holding the port is what keeps the address inert at a harmless 502; releasing both is [task 1002897](https://cloudbongos.com/builders#/task/1002897). Retention is also correct for revive, which re-uses the same hostname. Copy was corrected to match: it had promised "the address stops answering", which was never true.](0181-teardown-state-gate-and-address-retention.md) | provisioning / lifecycle |
268
- | 0182 | [**Project visibility is TWO axes: a derived publish gate and a stored join door** ([task 1002320](https://cloudbongos.com/builders#/task/1002320), goal 1000046). Goal 1000046 seeded 31 tasks each citing "per R01" — this ADR — so nothing in the goal could start until it landed: it gated **29 open tasks**, the largest single chokepoint in the repo, while itself parked at `backlog` with no dependencies. The spec its planning task cites was never written, so this is the design of record. The goal's criterion says **four** states (Public/Private/Stealth/Unlisted) and R04 says **three** stored plus a derived dark-matter; both are right and conflating them is the trap. There are two INDEPENDENT axes: **publishability** (DERIVED from the identity gate — name + description, per the owner interview) decides whether a project is on the map at all, and **the join door** (STORED `visibility ∈ public|private|stealth`, owner-set) decides how it renders and who gets in. Four user-facing states = 3 × publishable + one derived state that collapses all three. The column name `visibility` is a mild lie kept for consistency with `goals.visibility` (ADR 0112) — it does NOT decide visibility, publishability does — and the ADR is the mitigation. All three stored states are VISIBLE; only the derived state is invisible, which is what makes the 3+1 split honest rather than a workaround. `private` inherits ADR 0112's exact meaning (visible-but-closed); `stealth` exposes EXISTENCE but not IDENTITY, and its redaction is **server-side in the feed** — the naive client-side version leaks to anyone opening the network tab, and the orbs page is a static site OUTSIDE this repo (R09) so it could never be enforced there. Three mechanisms touch "does this appear" and are separated: `status` (operational), `visibility` (owner), `featured` (Archon editorial) — on the map iff `status='active' AND publishable`, with `featured` only ranking among those. That is a real behaviour change: today's predicate is `featured=true AND status='active'`, so **featuring is currently load-bearing for visibility at all** and the goal's owner-control premise silently does not work without it. Visibility is owned by the INSTANCE and the hub row is a CACHE (ADR 0141 §3 + the admin-console spike's D1) — so `owner_github_id` being "advisory, not an authority" is CORRECT rather than a gap, R05 moves to an instance surface, and R08's absent-report default is fail-closed (unreported ⇒ dark matter, off the map rather than exposed by omission). Per-state join rules compose with the separate `open|closed` join policy ([task 1002817](https://cloudbongos.com/builders#/task/1002817)) by one rule: **visibility sets the ceiling, join policy may only narrow** — otherwise the two settings are a coin-flip decided by evaluation order. Finally, `stealth` COLLIDES with the existing instance-wide pre-launch password gate (`STEALTH_PASSWORD`, `src/bongos/stealth-gate.js`), which means nearly the opposite (a whole instance made unreadable vs a project publicly on the map, anonymously); per ADR 0174 the product meaning keeps the word and the infra rename is filed separately rather than decided by an ADR about visibility. Rejected: a stored 4-valued enum (makes `unlisted` compete with `private`, and lets publishable+unlisted disagree), a separate `published` boolean (can drift from the identity it summarises; derived cannot), and keeping `featured` as the visibility gate (an owner who sets public and appears nowhere has not been given control).](<redacted>.md) | platform-identity / project visibility |
269
- | 0183 | [**A done-when criterion closes itself; the review queue becomes a place to look** ([task 1002069](https://cloudbongos.com/builders#/task/1002069), goal 1000072, owner request 2026-07-05). ADR 0086 §6 made `satisfied` reachable exactly one way — a Metic+ calling `POST /done-when/:id/satisfy` — even though the signal it gated on is unambiguous (**every** gating task shipped) and a human confirming it adds no information. When nobody runs `/goal-review` the result is not a careful pause but a **lying board**: this session measured goal 1000072 at `6/9 shipped, 0/5 criteria`. Now one helper, `doneWhen.autoSatisfyShippedCriteria(exec, {taskId})`, runs from **two** places for two different reasons — inside `shipTask`'s txn (scoped, **immediate**, on the txn client so a rolled-back ship cannot leave a goal achieved) and unscoped on every `publish-reconciler` tick (**backlog drain** for criteria whose tasks shipped before this existed, since nothing will ship against them again, plus **self-heal**). Idempotent by construction — it reads only `satisfied=false` rows — which is what makes an every-tick sweep safe. **The load-bearing subtlety is the delivery guard.** Auto-close needs BOTH "nothing still pending" AND **≥ 1 task actually `shipped`**; the second is not redundant, because since [task 1002651](https://cloudbongos.com/builders#/task/1002651) made `abandoned` terminal-not-pending, a criterion whose tasks were **all cut** satisfies the first clause *vacuously* and would auto-close on ZERO delivered work, cascading its goal to `achieved` (measured: `buildCriterionProgress` returns `pending_review:true` for exactly that shape). Yet `pending_review` **keeps** that wider meaning on purpose — it asks "should a human LOOK at this", and 1002651 chose that an all-abandoned criterion surfaces so someone decides whether it still means anything. So: everything that auto-closes was flagged; not everything flagged auto-closes. An earlier draft "aligned" the two predicates and broke that intent — `tests/criterion_progress.mjs` caught it; **do not re-align them**, the gap is the design. `/goal-review` is kept, demoted from gate to viewing surface; manual `POST /satisfy` stays for what automation cannot judge (a criterion with no linked tasks never auto-closes). Honest limits recorded: auto-satisfy cannot help a criterion nothing is LINKED to, and the in-txn close logs before commit (accepted — threading the summary out through `withTx` would cost ~5 lines in the over-threshold `db.js` under shrink-only ratchet policy). Rejected: sweep-only (up to 5 min to see your own goal close), hook-only (never drains the backlog), a stored `pending_review` column (second source of truth), deleting `/goal-review`.](<redacted>.md) | lifecycle / goal tier |
270
- | 0184-a | [**A task's visual is its value summary in another medium, and inherits that audience** ([task 1003109](https://cloudbongos.com/builders#/task/1003109), goal 1000072, from owner idea 1000400 filed in Discord `#ideas` 2026-06-24). A shipped task can now carry ONE optional image beside its `value_summary` — a screenshot of the surface that changed, a before/after, a diagram — so a reader who does not read code can **see** what shipped instead of reading about it. The rest of that idea had already landed (the note itself; the spend half via `/cost`); the picture was the whole gap, and the only path a builder had was a Discord post — outside the ledger, on a CDN whose links expire. The decision that keeps it from rotting is a REFUSAL to invent a second privacy axis: **a visual has no audience of its own.** One predicate says who may see it — `task-visuals.isPubliclyViewable` (public = `shipped|confirmed`, byte-for-byte the test `routes/public.js:projectPublicTask` already applies to a title/summary) — so any future surface rendering `value_summary` inherits a settled answer for the picture beside it. Three consequences are load-bearing. (1) The serve gate lives **inside** the handler, not in mounted middleware, because the audience is a property of the ROW; the stored filename therefore carries the task id (`task-<id>-<16hex>.<ext>`), which is simultaneously the path-traversal defense and how the route finds the task, and every step fails CLOSED (unparseable name → absent task → not-public → `requireBuilder`, never past it). (2) The public shipped feed **mirrors the predicate in SQL** — `db.recentShippedTasks` nulls the URL for a `security_sensitive` row, rather than selecting the flag and filtering in JS, so the unauthenticated dashboard never advertises an image the serve route would refuse AND never learns the flag; change one, change the other. (3) The single deliberate narrowing vs `value_summary`: a security-sensitive task's image stays builders-only even once shipped — a summary is a sentence its author chose, a screenshot is whatever was on the screen. Nothing is HIDDEN by that; non-public means un-published, not invisible. **Optionality is a contract, not a default** ("where relevant" is the owner's wording): `ship.js --visual` screens the file BEFORE the claim resolves (bad input costs a retyped command, never a claim) and uploads AFTER (a failed picture warns and prints the re-attach command; it never fails a ship that already landed), a task with no visual renders **nothing at all** — no placeholder, so it never reads as unfinished — and `DELETE /tasks/:id/visual` exists because an image is not prose (task 1003102's write-once rule guards a written CLAIM, not the picture beside it). Not an arbitrary-file host: extension allowlist + magic-byte sniff + 4 MB cap + server-minted name, **SVG deliberately excluded** (an executable document served from our own origin), `nosniff`/`inline`/`default-src 'none'; sandbox` on the way out, files under `var/task-visuals/` outside the git tree so a deploy's `git reset --hard` cannot wipe them. Rejected: a per-visual visibility flag (two axes over one fact drift, and the first disagreement leaves a reader guessing which is true); requiring `modules/discord/bug-attachments.js` (ADR 0083 forbids module→module, and the intake genuinely differs — that one DOWNLOADS from a CDN and needs an SSRF allowlist; duplication is re-stated under ADR 0091 §2 and cross-referenced in both headers); full-size images in every feed (the ledger, a profile and the dashboard are scannable lists — they get a thumbnail linking to the file; the task page is where you go to look).](<redacted>.md) | lifecycle / hall-ui / status-ui |
271
- | 0184-b | [**A ship must account for itself: `shipTask` requires a land proof or a declared no-artifact** ([task 1002483](https://cloudbongos.com/builders#/task/1002483), goal 1000024). `db.shipTask` is the confirmed→shipped flip and, since [ADR 0120](<redacted>.md), where the CREDIT is booked — yet its only precondition was `WHERE status = 'confirmed'`, nothing about whether code reached main. The two AUTOMATED paths each proved the land first; the MANUAL route passed nothing, so a task confirmed with no pushed code could be shipped by its claim holder **or by any Metic+ sweeping `/merge-mode`**, booking its `task.shipped` credit. It stayed untracked for two months because its tracking task was closed in June asserting task 1001547 had delivered the gate — it had not, blocker 1000047's own final update says so, and the code agreed with the blocker rather than the closure note: **a hole hidden by a record nobody checked**, which is the same failure the gate is about. Decision: the gate lives at the WRITE, not the route (a route gate leaves the invariant unenforced at the layer that moves the ledger, so the next caller reopens it silently), and `shipTask` is FAIL-CLOSED on a required `landAssurance`. TWO kinds, and the second is why this does not make things worse: `proof` (this task's commit is on main) and **`no_artifact`** — a deliberate DB-only ship, an explicitly supported shape (CLAUDE.md "Direct DB writes are work too") that [task 1002478](https://cloudbongos.com/builders#/task/1002478) documents as a dead end below Archon. A naive proof-only gate would have made 1002478 strictly worse; naming no-artifact as a KIND turns it from an absence the gate must be talked out of into a recorded answer. Both carry a reason, audited post-commit and best-effort (an audit miss must never roll back a landed ship). `ship.js` now declares its own `--db-only`/`--allow-empty` lands, without which this fix would have regressed the very flow CLAUDE.md calls supported. Rejected: (b) gate only the route — leaves the write unguarded; (c) record the residual as tolerable — defensible on blast radius and genuinely close, but it would have been the FOURTH record asserting this was handled with nothing checkable behind it. Does NOT gate confirm: a codeless confirm is still accepted, it simply cannot reach `shipped` or pay.](<redacted>.md) | lifecycle / integrity |
272
- | 0185 | [**The spark-handoff credit split: 70% developer, 30% spark author** ([task 1002923](https://cloudbongos.com/builders#/task/1002923), BV1.R147, goal 1000062 — completing the chain [task 1002920](https://cloudbongos.com/builders#/task/1002920) mark → [task 1002921](https://cloudbongos.com/builders#/task/1002921) queue → [task 1002922](https://cloudbongos.com/builders#/task/1002922) pickup). [ADR 0172](<redacted>.md) built the ideator streams and paid `captured_by`, the only person an idea then had. The spark handoff created a second: R146 develops a spark IN PLACE, preserving the author while recording a `developed_by`. **R146 therefore shipped paying NOTHING on purpose** — not an oversight but a refusal to guess, because `awardIdeaFullPassCredit`'s idempotency predicate is `(builder_id, reason)`, so paying the DEVELOPER would not have blocked a later payment to the AUTHOR and one idea would have paid twice; the cost of that caution was that a developed Full Idea received stream 2 **never** unless its author resubmitted. Owner decision (2026-08-22): **70/30 favouring the developer**, on BOTH existing streams, one ratio so there is one rule to explain. The reasoning is behavioural, not sentimental — developing someone else's spark is the scarce act the queue depends on, and at an even split it pays HALF what authoring your own Full Idea pays for the same writing work, so the rational move is always to write your own and the R145 queue stays a list nobody reads. The author keeps 30% plus their share of stream 1 on every task shipped under the idea, forever. `IDEA_DEVELOPER_SHARE = 0.70` is a **policy constant in `credits.js`** beside the pass bar and quick factor (ADR 0172's own consequence, the ADR 0023 precedent): a share of someone's earnings deserves a review and a commit, not a runtime edit. **The structural half is what retires R146's caution:** the economy module now derives the payees itself (`ideaCreditParties` reads `captured_by` + `developed_by`), so the caller can no longer choose a recipient — the develop route hands over an idea and a score, never a payee, and books the split like any other path (fail-open; the development has already committed). Three arithmetic properties, each a real bug if missed: the total is **conserved** (developer rounded, author gets the REMAINDER — rounding both pays 6 out of a 5-credit payout); a **self-developed** idea collapses to ONE payee, because two rows for one builder would silently UNDER-pay under the per-builder idempotency predicate, not double-pay (the comparison normalises via `String()` since `bigint` arrives as a string, so a type mismatch cannot become a phantom payee); and **zero-value rows are dropped**. Ids otherwise pass through unconverted — `Number()`-ing a `bigint` risks precision. `builder_id` in the idempotency predicate, which entered as an index optimisation (the R143/IC2 grader finding), is now **load-bearing for correctness**: two payees share one `reason`, so a reason-only predicate would refuse the second, and a partially-booked split completes on the next call. **Not decided here:** stream 3's split (ships with the Board Room, task 1002999), and whether `origin='develop'` in `ideas_task_lineage` belongs to whoever promotes a DEVELOPED idea into a task — ADR 0172 §5 says the row is "set at promotion and at develop (R146)", but R146 creates no task, so that origin stays deliberately unwired rather than invented; it changes who stream 1 pays.](<redacted>.md) | economy / ideator lane |
273
- | 0186 | [**Per-project idea templates: the seam is already built; the economics are the open problem** ([task 1002932](https://cloudbongos.com/builders#/task/1002932), BV1.R156, goal 1000062 — a SPIKE, output is this ADR plus a task list, no implementation). The literal question — how does a founder define their own idea shape without a schema change or forking the scorer — is **already answered by shipped work**: R141 gave every template function an optional `registry` defaulting to the built-in one, so validation and scoring both read the declaration as data; `ideas_grades.template_key` is per-row with a format CHECK whose own comment says a project key "stays addressable"; and `provisioning_instances.settings jsonb` is an existing per-project home. **No schema change, no fork.** Decision A: a **`project-settings` kernel port**, sibling of `builder-settings`, read via `resolveOptional` so a vanilla instance with no provisioning module degrades to the built-in registry; `base-v1` stays the default and the fallback for an unresolvable key. Rejected a new `ideas_templates` table (second home for data that has one) and a config file (per-instance, but one instance hosts many projects). **Decision B is the finding that matters.** Completeness is normalised 0–100 WITHIN its template (mean of per-field ratios), while karma reads it against an absolute bar (`IDEA_FULL_PASS_BAR = 60`, `IDEA_FULL_PASS_CREDIT = 5`) — so template choice becomes a credit-minting lever: `base-v1` needs ~200 characters for 5 credits, a 1-field template with `substanceMin` 10 needs ~10, a ~20× arbitrage handed to exactly the projects this ADR empowers. Shipping A without B would make the feature a way to print credits. The fix separates the two questions one number is answering: **completeness stays relative** (it is the right signal for triage ordering and the hall meter) and **karma stops reading it**, switching to an absolute substance total the scorer already computes per field and currently discards after averaging. An interim guard (≥3 required fields, ≥120 total `substanceMin`) bounds the arbitrage without fixing it. Decision C: an optional per-field `weight` (default 1) turning the unweighted mean into a weighted one — today's behaviour is the all-weights-1 case, so it is a data addition and one arithmetic line. Decision D: the five base fields are **not** mandatory (forcing them would make "define your own" cosmetic), replaced by two constraints — a template must declare ≥1 required field, and a stored template is validated at WRITE time so a malformed one is refused there rather than discovered inside the capture path, which must never throw. **Boundary note:** goal 1000062's wall is `[ideas, hall-ui]`, so the port task (provisioning + kernel) and the karma task (economy) cannot be claimed under it.](<redacted>.md) | ideas / templates |
274
- | 0187-a | [**Collab's four carried-forward questions, answered** ([task 1003170](https://cloudbongos.com/builders#/task/1003170), goal 1000075). The goal deliberately carried four open questions under the owner's instruction to "answer them in the goal, do not let them be decided by accident" — each being the kind an implementation settles silently by picking whichever branch was easier to code. **(1) Closeness is a WEIGHTED BLEND** of co-claims, co-build sessions, goals shipped together and (per 4a) comms/mentions — not one signal. Two parts of the decision, not follow-ups: the weights are DATA rather than a buried constant, and "why am I close to them" must be answerable on the surface that shows it, because a number that ranks colleagues and cannot say what it is made of becomes unfair with nothing to point at. Consent is unchanged — ADR 0171 fail-closed, hide enforced in the READ path, a hidden builder is not a node in anyone's graph. **(2) Passing RECOMMENDS, never transfers**: the recipient still claims it, claim/release stays the only path to holding work, declining is free, and the ledger records who recommended it and why. Transfer-on-pass was rejected because it would stop the claim ledger meaning "this person chose this work". **(3) Co-build is a GOAL-level fact, not a shared claim — and the reward question was WRONG-PREMISED.** Owner, verbatim: nobody will "co run the task on their claims"; collaboration happens at the goal. So each builder holds and is paid for their own task exactly as today and there is NO split to design — recorded because it is a deletion: a reward-splitting mechanism would otherwise have been designed, reviewed and shipped for a situation that does not occur. **(4) Mingle pairs CROSS-CRAFT** (engineer with artist or ideator), same-craft rejected as duplicating the craft-matched Collab path; the two-switch consent rule stands, since one switch is not consent for a real-world social commitment. **4a adds scope:** Discord mirrors INWARD so comms and mentions are visible in-hall — which puts attacker-authorable text on an in-hall surface, so the trust posture for mirrored content is this goal's to answer rather than assume (Discord is outbound-only and outside the boundary today, ADR 0032/0033). It is BLOCKED on task 1002946 (the Agora, goal 1000064, unstarted pending team buy-in), and this goal must not reimplement the Agora to get around it. No criterion was added for it deliberately — one blocked on another goal's unstarted work would make 1000075 unachievable for reasons unrelated to collaboration.](<redacted>.md) | collab / social hall |
275
- | 0187-b | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](<redacted>.md) | auth / security posture |
276
- | 0188 | [**The weekly patrol runs on CI, and a declared cadence must have something that fires it** ([task 1003209](https://cloudbongos.com/builders#/task/1003209), goal 1000079). Goal 1000076 shipped a weekly red-team patrol, a routine describing how to run it, a recipe, and a manifest row — and nothing ever ran it; the follow-up audit §7 named all three halves (no committed scheduler, no cadence for a scheduler to read, and a manifest green anyway because its wiring predicate was satisfied by the SKILL.md mentioning its own scanner). The symmetric fix — an in-process timer beside `peer-vote-tally` in `routine-timers.js` — is **wrong on correctness, not cost**: all three patrol checks read things only the REPO has (full git history, `.github/workflows/`, an auditable lockfile), and a deployed instance runs a VENDORED core out of `node_modules/@bongos/core` that has none of them, so the timer would have scanned nothing and reported clean — worse than unscheduled. The runner is therefore CI: a weekly Actions lane with `fetch-depth: 0` and a pinned, checksum-verified gitleaks (the missing binary the audit listed separately), invoking `run-routine.js redteam-weekly` so the SKILL.md's steps stay the single source of truth. `cadence:` becomes a real frontmatter field — declarative only, scheduler-agnostic. The load-bearing piece is `routine-schedule.js` (fitness Check 25), whose predicate is **relational, not existential**: it classifies the cron into the period it actually realizes and fails when that disagrees with the declared cadence — the one rot mode no substring or existence check can EVER see, since editing weekly→monthly leaves every "is it wired?" predicate green. The manifest row was re-pointed to match ("wired" now means the SCHEDULER names the routine). Finally the exit code says **a finding was LOST, not that a finding exists**: findings are advisory (ADR 0162), so filing three is a SUCCESS and red is reserved for findings that cannot be recorded — failing on findings would have gone red on run one and stayed there (the unrotated staging credential in `092e473` is a standing history hit), and an always-red weekly job is one nobody reads. Filing is credential-gated but OPTIONAL — with no session secret the patrol still runs and writes its findings to the job summary, which matters because this goal's C1 is about eliminating long-lived tokens, not adding one. Rejected: the in-process timer; teaching `routine-timers.js` generic cadence discovery (a scheduler built for nobody — its only candidate user cannot run there); failing whenever findings exist; requiring the secret; a deeper manifest predicate instead of a new check.](<redacted>.md) | security / enforcement durability |
277
- | 0189 | [**A frozen baseline must justify itself: declared floors and the freeze-forever detector** ([task 1003210](https://cloudbongos.com/builders#/task/1003210), goal 1000079). The shrink-only ratchet makes a quality metric unable to REGRESS and, by the same design, unable to sit still noticeably: the follow-up audit measured the whole file at "every metric equal to baseline, 0 reduced" and the ratchet reported a clean PASS, because one green covers both actively-paying-down and nobody-has-looked-in-a-year. The ratchet has no concept of time; this adds it. **Age comes from git, never a stored `since:` field** — a timestamp beside each metric would be faster and hermetic and would also be the field that silently stops being updated, leaving old debt permanently fresh-looking, which is worse than no check because it produces confident green; an unavailable history reports UNKNOWN, not fresh (the `--require-roster` doctrine). A metric's age is the date of the OLDEST commit still carrying its current value, so a baseline recorded once and never revisited dates to its introduction — exactly the case being hunted. **The half that matters is the declared floor.** The naive "flag anything unmoved for N weeks" fires forever on metrics that are FINISHED (`require_cycle_count`'s four deliberate lazy requires; the ~28 duplicate windows ADR 0091 §2 forces because a module may not import core), and a check that cries wolf weekly is one everyone learns to skip — the same alert-fatigue failure ADR 0188 §2.4 avoids. So a metric may declare `floors: {name: {value, why}}` and a metric AT its floor is done, not stale; the `why` is mandatory and length-checked, because a floor exempts a metric from the clock permanently and must therefore be a written claim someone can disagree with — **silence was the problem**, and a floor without a reason is just a mute button. A floor exempts only DOWN TO the floor, so `duplicate_window_count`'s 59-vs-28 keeps its ~31 reducible windows on the clock. **Two severities, split by whose fault it is:** declaration rot (a floor naming a dead metric, a floor with no reason, a floor the metric already dropped below) is hermetic and caused by the diff at hand, so it HARD-FAILS the merge gate; staleness is nobody's diff and would punish whoever pushed next, so it warns in fitness and is FILED weekly by the patrol — the "repeatable, scheduled version" the audit asked for. Also fixed a latent eater: `--update-baselines` wrote a fixed object literal, so one refresh destroyed the accumulated `_comment` and (once floors landed) every floor, deleting a finished metric's justification from the repo while turning it into permanent weekly noise. Nothing is flagged today — the file is six days old — so the detector is verified by shrinking the window, not by waiting. Rejected: a stored `since:`; no exemptions; a floor with no required reason; hard-failing on staleness; patrol-only placement.](<redacted>.md) | quality ratchets / debt governance |
278
- | 0190 | [**A changed project setting reaches a running instance by an in-place env patch and a restart — never a template re-write** ([task 1003140](https://cloudbongos.com/builders#/task/1003140), goal 1000058). Task 1003139 made a NEW instance read `platform_visibility` / `joinability` from the web.env the runner writes; a RUNNING instance loads its branding pack once, memoized and deep-frozen for the process lifetime, so a changed value could not reach it at all — the settings were write-only state on the platform. Decision: `PATCH /provisioning/instances/:id/settings` writes the platform row first, then for an ACTIVE instance on a shape the runner can reach enqueues a `settings-apply` intent (migration provisioning_010); the control-plane runner reads the instance's web.env as it stands, upserts ONLY the two settings lines (`upsertEnvVars`), and RESTARTS the service. The in-place patch is the load-bearing choice: the natural move — re-running `webEnvBody` as domain-attach does — regenerates the file from the row, and the row does not know the OAuth client secret or the App private key that `placeManifestCreds` placed later, so a template re-write silently blanks sign-in on the very instance the owner just touched. `SETTINGS_APPLY_FROM = {active}` because a pre-active instance gets the same values at standup and a torn-down one has nothing to restart (the provision intent cannot stand in — it excludes `active`, task 1002691); `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` because a dedicated droplet has no post-hoc exec path, and a push that cannot land is refused at the route AND the runner rather than reported. The response is a receipt, not a claim: it says the restart is coming and that the instance keeps its current setting until then, and the read-back lives on the instance's own public `GET /instance` manifest, whose `brand.project` now carries the resolved policy (`clientBranding` allow-list widened deliberately). Rejected: the template re-write; re-provisioning as the push; skipping the restart (impossible while the pack is memoized); claiming the new state on PATCH return.](<redacted>.md) | provisioning / settings channel |
279
-
280
- | 0191 | [**Majority, and an author who may vote but never decide alone** ([task 1003273](https://cloudbongos.com/builders#/task/1003273); diagnosis and companion in [task 1003271](https://cloudbongos.com/builders#/task/1003271)). Supersedes ADR 0175 §3's pass-rule set and §§7/11's flat anti-self rule. The owner reported voting as broken; nothing was broken. Nine items sat open under `first_ratifier` with four archons active — any one of three non-authors could have ratified any of them in a click — but every item was authored by the one person looking at them, and ADR 0175's anti-self rule was enforced by rendering NO BALLOT, so from the owner's seat the feature did not exist. The lesson: a rule enforced by removing the control is indistinguishable from a broken feature. Two decisions. (1) `majority` joins PASS_RULES: strictly more than half of the resolved MEMBERSHIP must vote yes — of the membership, not of the votes cast, so low turnout cannot quietly pass things; a tie is not a majority. The three rules are NOT a ladder of strictness, they differ on WHAT SILENCE MEANS — `first_ratifier` and `consent` pass an unvoted item, `majority` returns it — which is the axis that matters and now the comment in config.js. (2) An author MAY vote and may NEVER DECIDE ALONE: the rule moved from refusing the cast to the two places something is actually decided — `shouldCloseAfterVote` needs a non-author vote under every rule, and the majority tally needs a non-author YES. Their yes still counts toward the numerator; their `no` is excluded from the objection test (else silence would be an author's safest act); a self-cast earns no karma. The ADR 0175 §7 amendment carve-out survives as the author rule's SCOPE, not an exception: `authorRuleApplies` is full_idea-only, so a sole Archon still ratifies their own amendment. Also records why the CLOCK is a consequence of majority and not an independent fix — `tallyOutcome` passes on ZERO votes, so adding a deadline under `first_ratifier` would have closed all nine items as ratified-unread, paying karma and credits on ideas nobody read; `BOARD_DEFAULTS.window_minutes` therefore stays `null` in the neutral core. States the cost plainly: three affirmative votes are needed on a four-member board, and a solo instance still cannot ratify its own ideas. Discord stays outbound-only (ADR 0175 §8 re-affirmed).](<redacted>.md) | government / board room |
281
- | 0192-a | [**A gated project refuses strangers at the app's front door, and the hub reads what the project runs from the project itself** ([task 1003043](https://cloudbongos.com/builders#/task/1003043), goal 1000058). Task 1003040 gave every project a `platform_visibility` setting and 1003139/1003140 carried it into the instance's `branding().project.platformVisibility` — resolved, enforced by nothing. Decision: ONE app-level middleware (`src/bongos/platform-visibility-gate.js`), mounted beside the pre-launch gate ahead of every human and public-read surface — the landing, the hub pages, the status host, the hall shell, `/public/*` and every module's `rank: public` route — rather than per-surface wiring, which misses the next surface nobody told it about. "Member" is a live, non-deactivated session on this instance, the same `resolveSession` lookup `requireBuilder` uses, per request, no cache; a lookup that throws fails CLOSED. The exempt list is the door, the manifest and the probes and nothing wider: `/api/*/auth/*` (a door that cannot be opened locks the members out too), `/api/*/instance` (what a stranger reads to learn how to sign in AND what the runner reads after a settings restart to learn whether the value was consumed — including the push that turns this gate on), `/healthz`, `/version`, `/metrics`; the pre-launch gate's blanket `/api/` exemption is deliberately not copied. A refused page gets a self-contained 401 door (no stylesheet, no branding bundle, no script) with one control — Sign in, returning (relatively) to the page knocked on; a refused API call gets `res.fail('unauthenticated', 401)`, exactly requireBuilder's envelope, which is why it mounts after `attachFail`. A member's answer leaves marked `private, no-store` whatever the route said — the public read API sends `public, max-age`, and a CDN edge would otherwise serve a member's copy to the next stranger. The exempt list covers every API prefix (the versioned contract too), `/downloads/*` and Caddy's TLS ask; a request's session is resolved once (`resolveSession` memoizes per request). The hub's manage page shows the CHOICE from the platform's row and what the project RUNS from the project's own public manifest (`brand.project`), and never confuses the two: `push.message` is rendered verbatim, `saved` moves only the radio, the "runs" line is written only from the manifest and says "doesn't report this setting yet" for a core that predates the channel. Rejected: per-surface wiring; a 302 into OAuth for a refused page (sends assets and crawlers to GitHub); copying the `/api/` exemption; painting the radio from the manifest or the runs line from `saved`; reusing the pre-launch gate.](<redacted>.md) | instance / project visibility |
282
- | 0192-b | [**A work category orients a human and authorises nothing** ([task 1003275](https://cloudbongos.com/builders#/task/1003275), goal 1000072, criterion `legibility-categories`). A goal gains a human CATEGORY from an owner-named set of eight (Builders Hall, Context Management, Government (Decision Making), Modules, Security, Economics, Legal, User/ABI Experience) so a person scanning the board can tell at a glance which goals are about money, which about the walls, which about the hall — today each goal's own words say so, but only if you read all thirty-eight. The DECISION is the constraint, not the feature: the owner said twice that this organisation has no hierarchical purpose and no effect on the agentic management of the work, and the system ALREADY has a load-bearing hierarchy (ADR 0086 — the scope_modules wall, rank floors, goal membership). The goal's own prose predicts the failure: a second thing that looks like a hierarchy gets mistaken for one within a release, and the mistake arrives as ONE plausible line in a gate ('skip Legal goals in the feed'), after which work is refused because of a label nobody meant as a permission. So: no authority, claim-eligibility, feed or dependency surface may read a category — enforced by the `scripts/gds/category-advisory-guard.js` fitness check, which scans every decision surface with comments stripped (a gate may CITE the rule, never branch on it) AND asserts the feature still exists, so a renamed-away subject fails loudly instead of passing vacuously (the ADR 0022 assurance-of-assurance finding). Uncategorised is LEGAL — nullable, no default, required by no write — because the criterion is explicit that Claude failing to place a goal is a designed prompt, not an error. A task stores no category and INHERITS its goal's through `tasks.goal_id`; a stored copy would be a second answer to one question. The set is a table, not an enum (runtime-extensible without a migration per addition) and retiring is `active = false`, never a DELETE, so a goal keeps rendering a label someone tidied away. The three route-rank pins are a tripwire: if one ever needs a WIDER gate than its neighbours, the label has grown into a permission. Deliberately NOT built: a per-category page or filtered navigation view — a chip is a label, a drill-down destination is a tier. Making a category gate anything is an owner decision and needs its own ADR first.](<redacted>.md) | GDS / goals |
283
- | 0193 | [**Pause task-scoped box slices: a box serves its rank scope (the full repo for Metic+)** ([task 1003285](https://cloudbongos.com/builders#/task/1003285), owner decision from the 2026-08-25 dev-box efficiency review). Pauses, does not repeal, ADR 0148 / goal 1000051: `routes/box.js` skips the lifecycle-port `scopeKeys` resolution unless `BOX_TASK_SCOPE=on`, so `decideSourceAccess` takes its documented rank fallback — full repo for Metic+, starter surface below. WHY: the slice was defense-in-depth git cannot hard-enforce (ADR 0031 §6's honest limit) while its costs were real on the live fleet — sessions on a box that cannot see a module (or its nested CLAUDE.md) misdiagnose themselves as stuck, the scope machinery kept failing in the field (task 1002729's base-slice collapse), and every live box user is Metic+ anyway, so the slice narrowed trusted builders while newcomers were already gated by rank. UNCHANGED: live-rank allow/deny per request (ADR 0016), the provision floor and demotion clawback, the read-only per-request credential (ADR 0055 — push still 403s, re-verified on hardware in task 1003253), and sub-Metic starter boxes. `GET /box/scope` now reports `task_scoping_paused` so the hall/app can say why `task_scope` is false; the app task-picker's "scoped to `<modules>` and nothing else" copy overstates while paused (cosmetic). Revert = set the env flag; make-permanent = delete the resolution + a follow-up ADR.](<redacted>.md) | dev-box |
284
- | 0194 | [**A project's membership door is one decision, answered the same way everywhere a stranger meets it** ([task 1003044](https://cloudbongos.com/builders#/task/1003044), goal 1000058). The owner's 2026-08-16 joinability setting — open / apply / invite_only — was stored (1003040), pushed (1003139/1003140) and resolved into `branding().project.joinability`, enforced by nothing. Decision: ONE reader, `joinabilityMode()` in `auth-admission.js`, and `openEnrollmentEnabled()` becomes `mode === 'open'`, so the four sign-in call sites, `webAdmissionStatus` and the manifest's `admission.policy` follow untouched (SR-19: one gate, no drift). The legacy `OPEN_XENOS_ENROLLMENT` flag still opens an APPLY project — a self-hosted instance that set it keeps its door across the upgrade — but never reopens an explicit invite_only; an unreadable pack or a value this core cannot read fails CLOSED (invite_only), the ADR 0192 rule. The public `POST /access-requests` answers the door FIRST — before validation, before any lookup — `403 invite_only` / `409 open_enrollment` with plain sentences: no query, nothing revealed about who exists, and DB-free enough for the real booted app to prove. An Archon's invite is the row the gate already reads: `POST /access-requests/invite` writes `access_requests` at 'invited' (resolving a pending one in place), so sign-in consults nothing new — surfaced as "Invite by GitHub username" on the Watch page. The door is SAID wherever a stranger meets it, in the project's words: the hall landing renders one door per mode from the injected pack, the web 403 names the invite-only door and the way in, the CLI stops instead of filing, the hub's join relay names the door in the hub's own words (a project is trusted to build, not to write prose into the platform's UI) instead of painting a refusal as "requested" or "unreachable". `admission.policy` stays binary (pinned; agrees for free); `joinabilityMode` joins the doorway — a human MINOR by ADR 0161 §2, which the publish lane's LIVE test refused (HEAD's version must be a CI carrier's), so it rides the next CI patch and the contradiction is filed as an idea. The hub's `#mJoinability` clones `#mVisibility` rather than a factory — a third setting is where a factory pays. Rejected: folding the flag into `branding()` (memoized vs per-call; the manifest must stay the pushed value); a 200 for the open door (an older CLI would wait thirty minutes for an approval); re-deriving the mode inside the module; a second invite table.](<redacted>.md) | instance / project joinability |
285
- | 0195 | [**ADR numbers are checked like migration numbers, and the twenty existing collisions are grandfathered by name** ([task 1003164](https://cloudbongos.com/builders#/task/1003164)). Two ADRs could share a number and nothing flagged it — git sees no path collision, the index auto-merges into two rows, and every gate passes; it was found by a human reading `ls docs/adr/`, after a task note had already cited a number the collision made point at an unrelated document. Migrations had this guard already (`stemCollisions()` hard-fails CI, because a re-used migration number silently skips a migration); ADRs had only a best-effort allocator at claim time and nothing afterwards. `scripts/gds/adr-namespace.js` is the sibling module, wired into fitness as Check 31 and so into the required `unit` gate. Building it found twenty live collisions, not one, plus fourteen index defects of the same merge class — two ADRs listed twice and twelve with no row at all, ten of them the second file of a colliding pair, which is how a collision hides. The twenty pairs are grandfathered by EXACT FILENAME rather than renumbered: renumbering breaks every citation to them, which is the harm the check exists to prevent, and set-equality keeps the exemption as narrow as the history that earned it — a third file on a legacy number, or a rename of either half, still fails, and the list may only shrink. The index half is keyed on LINK TARGET, not number (a number rule cannot express a grandfathered pair): every file has exactly one row, every row points at a file that exists. Row ordering warns and never blocks — ten legacy rows are out of order and no PR author caused that (the ADR 0189 split). Rejected: renumbering; grandfathering by number; a numeric cutoff like LEGACY_BARE_CORE_MAX (a new ADR takes a number above the max, so it would catch nothing); fixing it in the claim-time allocator (which already tries, against a stale tree — that is how the collision happened).](<redacted>.md) | docs / numbering discipline |
286
- | 0196 | [**The chrome world becomes the token contract; the landing gets its own world sheet beside the hub's** ([task 1003298](https://cloudbongos.com/builders#/task/1003298), goal 1000080). The owner picked the chrome-blobs world (1003296) and the v3 composition with light rebuilt to the dark standard (1003297); this build makes it the platform's token contract. Decisions: (1) the neutral pack moves to the world's LIGHT values (pale grey paper, black ink, salmon accent, Manrope + JetBrains Mono) and design-tokens mirrors it; --accent-deep is #a3481a rather than the mock's #d96a35 because the hub and the gate pages paint TEXT with it and #a3481a is the lightest burnt orange clearing 4.5:1 on all three light grounds; the rgba hairlines are flattened to hex so hex-maths consumers survive. (2) theme.ui.backdrop becomes var(--bg): a literal there paints the light ground under the dark page in both modes, since the body rule outranks every sheet. (3) The landing gets its own world sheet public-landing/assets/world.css (contract tier = pack, derived tier both modes, root-absolute plate token PAIRS, the kill switch; NO composition switches) and cosmos.css stays the hub's component sheet until A4 — folding the two tiers would have collided (--line is a width in the world and a colour in the hub; the world's warm --ok/--warn/--err would repaint the hub's fixed green/amber/red chips). cosmos.css's dark tier moves to the world's black and its derived tier is retuned for the salmon accent (dark ink on the fill, never white). (4) Plates ship under public-landing/assets/world/ with provenance embedded: black plates verbatim JPEG, cutouts as WebP with alpha at 1600px (85–170 KB vs 1.1–2.4 MB PNG), sidecars; plate URLs in custom properties escape the ?v= stamp (edge cache 4h), accepted. (5) Live figures are BAKED inside apex-only sentinels (real, dated; a child renders empty until its own feed answers) and refreshed at render time from the five public feeds with fixed definitions: tasks shipped = the sum over every version row, builders = leaderboard entries holding credits, the ledger line = the first building version; live titles have em dashes normalised to a comma and a trailing parenthetical dropped. Every pack-reading fallback site (the claude-design adapter, the status explore page, provisioning onboarding, dev-box brand artifacts + tray icons, the two gate pages) is moved in the same task so no page keeps a private palette. Rejected: one merged token tier; a landing-private palette; PNG cutouts; a script-only ledger; #d96a35 as accent-deep.](<redacted>.md) | apex / design system |
287
- | 0197 | [**The ui-design module: design is a platform capability; the instance owns its world** ([task 1003324](https://cloudbongos.com/builders#/task/1003324), goal 1000080 — the first of the five ui-design tasks scoped with the owner 2026-08-28). ADR 0081 put the tool-agnostic design layer in core as Cloud Bongos's machinery; the whole-frontend pass then built the chrome world on it and left the three things a builder needs — the contract and its tooling, a way to look before shipping, the taste and the looks — scattered across core, scratchpads and personal skill installs. The owner's reframing: this is NOT the front door, it is a PLATFORM capability every builder on every instance gets, and Cloud Bongos's front door is its first customer, not its owner. Decisions: (1) `modules/ui-design/` is a default-on core-domain module beside grading/economy/memory — it ships METHOD, never a world; (2) ADR 0081's layer moves in (`src/ui/adapters/` → `adapters/`, the three `scripts/gds/` CLIs → `scripts/`, `docs/design-contract.md` → `docs/`) while `config/design-tokens.*`, `design-sources.json` and `DESIGN.md` STAY out as the host boundary — the ADR 0062 §7 split written for design; (3) declaration-only: three contributed skills, no routes/port/migrations, so `GET /modules` lists it and disabling it withdraws the skills and touches no served surface — and NO `ui` discipline yet, because the hall settings page and newcomer-restock don't know it; (4) the `/design` playbook is finally written — `discipline-modes.json` had routed `ui` tasks to a skill that did not exist since task 1267; (5) the module CLAUDE.md draws the platform FLOORS (the Fifteen Rule, the dark twins, no page `:root`, the 24px floor, AA in both modes, the Kill Switch) against the instance's WORLD (its packs + DESIGN.md), tightenable and never loosenable; (6) the CLIs keep thin `scripts/gds/` shims — the form `claude-materialize` rewrites to `bongos exec` on a packaged core, the form `doc-cli-guard` resolves, and the carve precedent — while inside the module they walk up from `__dirname` instead of the computed `require()` into `src/instance-config.js` the static boundary scanner could not see; (7) three reserved homes, each README'd for the task that fills it, the vendoring policy pointed at the skills task's ADR; (8) `public-landing` keeps its own key. Rejected: leaving the layer in core; moving the token files in; the module owning the front door; module-hosted CLIs with no shims; contributing the `ui` discipline now.](<redacted>.md) | modules / design system |
288
- | 0199 | [**An unreachable ACTIVE project gets a restart of its own, not a re-provision — and the restart never claims the verdict its probe cannot reach** ([task 1003238](https://cloudbongos.com/builders#/task/1003238), goal 1000058). The liveness overlay (task 1002696) could report a project unreachable while its status stayed `active`, and that reading came with no action: `PROVISION_ENQUEUE_FROM` deliberately excludes `active` (task 1002691 — a blind re-enqueue left a stale provision intent blocking the owner’s own teardown), so the card’s existing *Try again* / *Bring it back* doors 409 on an active project by construction, and `settings-apply` — the only leg that runs from `active` — refuses unless a stored value actually changed, which is exactly the case a stuck project does not have. Decisions: (1) a `restart` intent (migration `provisioning_011`, `INTENT_ACTIONS` in lockstep) behind `RESTART_FROM = {active}` + `RESTART_SHAPES = {co-tenant, standalone}`; the web tier still only ENQUEUES (ADR 0111 §2). (2) `restartInstance` is `settingsApplyInstance` MINUS the env patch — one `systemctl restart`, then the same patient healthz budget; it reads and writes no file, so ADR 0190’s in-place-patch hazard cannot arise. (3) The `probeIsVerdict` narrowness carries over and is load-bearing: the liveness sweep probes the PUBLIC `https://<domain>/healthz` while the runner verifies LOOPBACK, so a project can answer here while DNS/Caddy/TLS is what is broken — no up-verdict is written for a domained instance, and the event says “answering on the box; whether its public address answers is for the next sweep to say” rather than a sentence the still-`unreachable` card would contradict. (4) A project that stays down is `ok:true` — the restart RAN; re-pending it to the attempt cap would restart a broken project four more times and end by stamping an `error_note` over the owner’s own action. Only a THROW returns `ok:false`, and it returns rather than throws because the drain loop’s catch would relabel a merely-unreachable instance `error` (ADR 0190 §2). (5) `enqueueRestart` is idempotent against its own kind in EITHER open state — where it differs from the settings push, which must refuse to ride a RUNNING sibling that may have read the row already; a restart carries no payload, so one already running IS the ask. (6) A pending restart joins the teardown’s supersede list, more sharply than its siblings since the owner reaches for Restart exactly when they might next give up and delete. (7) The door shows only at `active && last_verified_up === false && shape !== dedicated && !open_intent`, mirrored on card and manage page; `null` (never probed) is NOT `false` and raises no door. (8) Every refusal names the action that IS available there, the state gate is checked before the shape gate, and no branch promises a duration (F16). The `INTENT_ACTIONS`↔migration set-equality assertion moved to the NEWEST migration’s test — pinned to 010 it would fail for every future action. Rejected: widening `PROVISION_ENQUEUE_FROM`; re-sending unchanged settings to borrow their restart; reporting a loopback pass as “answering again”; retrying a restart that left the project down; a Restart door on every active card.](<redacted>.md) | provisioning / project lifecycle |
289
- | 0198 | [**Third-party skill vendoring: a scan verdict the session cannot override, a licence allowlist, a provenance sidecar, one preamble block, and no auto-update** ([task 1003321](https://cloudbongos.com/builders#/task/1003321), goal 1000080 — the second of the five ui-design tasks). Twelve design skills installed on one builder's machine were to become the `ui-design` module's vendored set. Both origins reduced to DANGEROUS under `/scan-before-install` at the default tier: `Leonxlnx/taste-skill` on one floor critical (an injection-phrase hit the panel identifies as a filename placeholder) with a caution panel; `pbakaus/impeccable` on three floor criticals and a panel that upheld shipped PostToolUse/Stop hooks written into five harness configs, a child agent spawned with permissions bypassed from a loopback HTTP route, a directive telling the model to disregard its harness system prompt, a page-embedded live-server token and an unverified remote bundle download. DECISION: the scan verdict is the gate and a session cannot override it — `dangerous` lands a rebuild spec (a GDS task), never a copy, and vendoring one anyway is an owner ADR plus a test amendment; a licence allowlist (Apache-2.0, MIT, BSD-2/3, CC-BY-4.0); a `PROVENANCE.md` sidecar with the full pinned sha; ONE preamble block (`skills/PREAMBLE.md`, byte-identical, fenced by two markers so an upstream refresh is a three-way merge) that reads the instance's world before any taste rule; `skills/vendor/<name>/` as the home because `fitness-ratchets.js` already exempts `vendor/` from the first-party budgets; the manifest stays the single declaration and `claude-materialize.js` step 1b lands a declared module skill — sidecar included — in every instance's `.claude/skills/` and withdraws it with the module; no auto-update; the playwright plugin a documented prerequisite. The roster ships EMPTY and that is the policy working: the machinery is proven by fixture (`tests/ui_design_skills.mjs`), the twelve are named in `/design` with their verdicts, two rebuild-spec tasks hold the documented functionality. Rejected: treating the placeholder hit as a false positive; scanning a fork or subtree; vendoring impeccable with the hooks stripped; `.claude/skills/` as the home; a second roster file.](<redacted>.md) | modules / design system / security |
290
- | 0200 | [**A mistyped bring-your-own domain can be corrected — the address it leaves is released to a blackhole, and the attach leg stops regenerating web.env from the row** ([task 1003237](https://cloudbongos.com/builders#/task/1003237), goal 1000058). `POST /provisioning/instances/:id/domain` refused a second address with a deliberate `409 domain_already_set` ("A genuine BYO address stays un-re-pointable, as before") while direction §8 said "attach/change stays for custom domains" — so a founder who mistyped one character in the create wizard was permanently addressed at a hostname they did not own, with no path back but teardown. Decisions: (1) a BYO address re-points under the guards the FIRST attach already applied — `isReservedHostname`, the task-1002704 in-apex squat rule, `getInstanceByDomain` uniqueness, `DOMAIN_ATTACH_FROM` (which already admitted `active`) — none added, none weakened; the platform-assigned `<slug>.<apex>` is un-re-pointable because the squat rule says so. (2) The address it leaves is RELEASED: `provisioning_012` adds `released_domains text[]`, written in the SAME statement as the new domain (a crash between the two would leave a hostname aimed at platform infra with nothing in the row to say so), an ARRAY because a founder can mistype twice before the runner drains. Only a BYO address is queued — the assigned `<slug>.<apex>` is the platform’s own name and the revive path re-uses it (ADR 0181 §2). (3) Release is a REPOINT to `192.0.2.1` (RFC 5737 TEST-NET-1), not a delete — the box lane’s idea-309 shape: a delete opens an NXDOMAIN window resolvers negative-cache for the zone’s SOA neg-TTL (~30 min, not editable), and an UPSERT needs only the contract every operator `BOX_DNS_HOOK` already implements. The Caddy vhost and the tls-check allow-list release THEMSELVES (the snippet is per-slug and carries the current domain; tls-check answers from the row). (4) A failed release is `ok:true` + `releaseFailed`, and KEEPS the queue so the next attach retries — a release that failed must never fail a re-point that already landed. (5) `domainAttachInstance` now patches web.env IN PLACE via `upsertEnvVars`/`originEnvVarsFor`, closing the ADR 0190 §2 hazard (idea 1000842) on this path: the template re-write regenerated the file FROM THE ROW, blanking the OAuth creds `placeManifestCreds` placed, the App private key and the federation block — masked for a FIRST attach by wizard order, and NOT masked for a re-point, which runs on a project the founder is already signed in to. Dry-run plans; an unreadable/empty file is refused, never patched. (6) A CHANGED domain clears `last_verified_{at,up}` — the overlay is a verdict on an ADDRESS (task 1002696), so carrying it would let the card claim the new name "last answered 2 minutes ago" when only the old one was ever probed (F16). (7) The card gains a third door — *Use a different domain*, pre-filled so a typo is edited not retyped — and says what it costs where the button is. Rejected: amending direction §8 to call the address fixed; deleting the old record; sequencing behind task 1002897; a `previous_domain` scalar (drops the middle address).](<redacted>.md) | provisioning / project lifecycle |
291
- | 0201 | [**An application is the project's own `access_requests` row, vouched for by a hub-signed assertion carried in a HEADER; the record it carries is resolved live and never stored** ([task 1002283](https://cloudbongos.com/builders#/task/1002283), goal 1000045 — public builder profiles, C3/C4). The join relay (`POST /my-projects/join`) already posts `{github_login, display_name, note}` to the target's PUBLIC, unauthenticated `POST /access-requests` — presenting nothing the applicant could not have typed into the project's own landing page. Attaching a cross-project record to a login nobody vouched for turns the owner's queue into an impersonation surface: file a request naming a well-known builder, the owner reads that builder's track record beside it and approves, and whoever holds that GitHub account signs in. So the record is the thing that makes the unauthenticated write dangerous, and C4 cannot be built without closing it. Decisions: **D1** the application stays an `access_requests` row on the target — `status='invited'` IS what `hasInvitedAccessRequest` reads, so a parallel record would need a second branch in `firstTimeAdmissionBlocked`, the one decision that exists as one function to prevent drift; the migration adds only `applicant_github_id text` (core's own type) + `vouched_at`, the non-null id IS the vouch mark, and the console wave's `kind`/`created_by` stay a separate additive migration; the hub keeps a STATUSLESS echo `(github_id, client_id, applied_at)` so the applicant's own cards can say "you applied on <date>" without polling every project — it may never claim what the project decided (an approval becomes visible the way membership always does, via the check-in), and it ages out at 30 days; re-applying answers `requested`, not 409. **D2** the hub signs an audience-scoped `purpose: 'application'` assertion with the existing `mintHubAssertion`, verified OFFLINE by the existing `verifyHubAssertion` against the already-provisioned `<PREFIX>_HUB_PUBLIC_KEY` — no new secret, no apply-time hub call — and it travels in `<redacted>` because `validateOrRespond` is STRICT by default: a body field would 400 on every older pin, and `joinRelayOutcome` reads a 400 as "could not be reached", breaking applications to most projects for good (the CLI's device exchange carries its assertion in the body because that route was new; this one is not). A missing or invalid assertion writes the row UNVOUCHED, never refuses — refusing locks out an older hub and hands an attacker a denial vector. The record itself is never copied: the row stores an identity (+ the handle, since `GET /profiles/:handle` is the addressed read), and the REVIEWER'S BROWSER fetches the hub's already-public, already-CORS'd, already-`no-store` profile at render, where `profile_state='active' AND NOT hide_stats` is applied inside the module — so C2's live hide is honoured by construction with no cache to outlive it, and a hub outage degrades to "record unavailable" beside a queue that still works. An instance→hub server pull was rejected on ADR 0171 D1's own grounds. A private account's application carries name + handle + note in V1; the D3 sliver needs `project.recruit`, which the hub cannot check cross-origin. **D3** "project owner" is the project's OWN archon — the registrant is whoever `maybeSeatFirstAdmin` seated, delegates are whoever that archon grants on that instance, and because the instance IS the project the per-project-scoped-permission problem dissolves; the check is the atom that already guards the act, `access_request.review` (floor archon), and this goal mints NO new permission (widening the read half is the console wave's `project.recruit`). `platform_identity_projects.owner_github_id` stays advisory and is never a gate — a hub-held owner list consulted before an admission is the inversion ADR 0141 exists to prevent. **D4** three statuses, unchanged: `invited` IS the approval and the acceptance act is signing in (an `accepted` state would be a second place membership is asserted, free to drift from the one that admits); rescind is `invited → dismissed`, which the PATCH must learn to accept (it resolves only from `pending` today) and which must never pretend to remove a member who already signed in; an `invited` row expires read-time at 30 days whatever created it, while an application never expires because a stale `pending` admits nobody; approve/decline are idempotent on the value they hold. Rejected: a cross-project `applications` table on either side; a stored profile snapshot; the assertion in the body; refusing an unvouched application; the server-side pull; reading `owner_github_id` as authorization; a new "project owner" atom; an `accepted` status.](<redacted>.md) | platform identity / applications |
292
- | 0202 | [**A connection is one directed row on the platform account, and every degree label is computed over the graph the viewer is allowed to see** ([task 1002294](https://cloudbongos.com/builders#/task/1002294), goal 1000045 criterion C6 — the design spike for the connections wave). Criterion C6 asks for LinkedIn-shaped connections; the cross-project data plane it must sit on is account-global (ADR 0141) and governed by an owner-signed privacy matrix (`docs/specs/<redacted>.md` D1/D2), where ADR 0171 D4 already bans "a count plus a hidden total" as a leak by subtraction. Decisions: (1) **one directed request row** on a new `platform_identity_connections` — `requester`/`addressee` kept because the inbox, cancel and cooldown all need to know who asked, with `pair_lo`/`pair_hi` GENERATED from LEAST/GREATEST and a partial unique index over `pending|accepted` so a duplicate AND a simultaneous reciprocal request are database errors, not route bugs; module-owned and `platform_identity_`-namespaced (ADR 0083 §5). (2) The `goal_membership_requests` lifecycle (`pending → accepted|rejected|cancelled`, atomic resolve-and-act) with three deliberate divergences: a decline is **silent**, a re-ask inside its 90-day cooldown is **swallowed and answered as if it worked** (ADR 0194 §3 applied to a person — a 409 saying "declined" would disclose what the silence protects), and pending requests expire at **30 days read-time** (privacy spec D4), with expiry resolved in the same statement that inserts, since `now()` cannot live in an index predicate. Remove **hard-deletes** — a "A removed B" tombstone is data neither party consented to keep and no read path uses. (3) Degree = a **recursive CTE seeded at the viewer**, `DEGREE_MAX = 3` with `FANOUT_1`/`FANOUT_2` caps and a deterministic frontier order; past the caps the label is `null` ("out of network"), never a number the query did not establish. Envelope stated honestly: single-digit ms at hub scale, caps bite around 10⁴ accounts, and they convert a hub-heavy account's millions-wide 3-hop into a *missing label* on a page that still renders. (4) Connections get **no predicate of their own** — they consume task 1002967's single effective-visibility reader, published as JS and as `accountVisibleSql(alias)` (the ADR 0182 form); `profile_state='active'` floors the feature, a private account answers **404 not 403** and never appears in a public list, `hide_stats` suppresses the numbers beside an edge but not the edge, degree is an **own-scope read** so viewer-relative labels coexist with viewer-independent public responses — and every count is the length of the array the viewer may see. (5) "Builders you may know" from mutuals + shared projects + recency, one explainable linear score, a named project only where its catalogue row is publicly listed; shared goals refused (instance-local), guilds a reserved empty slot, and **Discord refused outright** (outside the trust boundary; the hub stores no Discord identity), as are the GitHub follower graph and address-book import. Rejected: two mirrored rows on accept; a symmetric edge with no requester; a core table beside `builders`; a materialised closure/adjacency table (privacy-filtered at write time = a cached visibility decision, which ADR 0171 D2 forbids); an in-process graph cache; a graph database. Deferred and named: a connections-specific hide toggle, blocking, what an accepted connection unlocks on a private account, and notifications beyond the existing attention card. Exposes to the guild/orb wave: `neighbours`, `degreeBetween`, `mutualCount` — **not** a stored strength scalar.](<redacted>.md) | platform identity / privacy / social graph |
293
- | 0203 | [**The doorway changelog leaves `src/module-api.js` — the CI auto-patch carrier appends to a markdown ledger instead** ([task 1003335](https://cloudbongos.com/builders#/task/1003335)). Amends [ADR 0161](<redacted>.md) §2. The `CORE_VERSION` release history lived as a comment block inside the doorway file, and ADR 0161's carrier appends two lines to it on EVERY merge-triggered release, pushed by `github-actions[bot]` straight to `main` — bypassing the `unit` gate it breaks. Release 1.19.407 took the file to 1501 lines, one over the `oversized_file_count` ratchet: `unit` went red on `main` and every open PR inherited it, self-inflicting and monotonic. Decision: the ledger moves to `docs/module-api-changelog.md` — markdown, which `fitness-ratchets.js` (`git ls-files '*.js'`) structurally never counts, so it can grow forever. All 1222 lines / 448 versions moved verbatim (only the `// ` marker stripped, kept in a fenced block so the hanging indent survives); the `const CORE_VERSION` literal and its line shape do NOT move, so every reader that greps it is untouched. `bump-version.js` transforms both files before writing either and refuses a half-applied bump; `publish.yml` stages both, pinned by `tests/bump_version.mjs`; the file is registered in `PUBLISH_ALLOWLIST` so the history keeps shipping inside `@bongos/core`; `src/module-api.js` keeps a pointer doc-comment where the block was. Rejected: trimming entries in place (buys one release), raising the baseline (out of policy per `config/fitness-baselines.json`'s own `_comment`), exempting the file from the ratchet.](<redacted>.md) | build pipeline / distribution |
294
- | 0204 | [**The apex is dark only; light is the builders' hall's** ([task 1003336](https://cloudbongos.com/builders#/task/1003336), goal 1000080). Supersedes in part [ADR 0196](<redacted>.md) (the light-mode half of the landing build) and the landing direction's 2026-08-27 pick of "light rebuilt to the dark standard". The owner, reviewing the planets: "only black or only white is a bit bland… leaning towards only dark mode for this page and keep the dark/light mode to be solely for the builder hall", confirmed as the WHOLE apex. Decision: `/`, `/projects`, `/privacy`, `/terms` and `/contact` pin `data-mode="dark"` on `<html>` — no pre-paint script, no `cb-color-mode`, no `?mode=`, no OS-preference read; the moon/sun glyph and its wiring leave both bars; the hall's own `hall-theme` switch is the platform's only light/dark control. The light tier of `world.css` / `cosmos.css` and the dark-twins rule STAY until the tier's own removal (the light-pair contrast tests keep measuring). The kit learns `"modes": ["dark"]` in a states file (validated; `render.js` takes it as the default). Rejected: a dark default with the toggle kept; deleting the light tier in the same diff; `--modes dark` on every kit call instead of the page's contract.](<redacted>.md) | apex / design system |
295
- | 0205 | [**A federated check-in may not speak for a person the hub never saw sign in: the membership carries the hub's own witness, and identity is never re-synced from client credentials** ([task 1003340](https://cloudbongos.com/builders#/task/1003340), goal 1000045 — closes security report 1000027, high, LIVE on the hub). `POST /sso/membership/check-in` authenticated the CALLER (`client_id` + `client_secret`) and took the SUBJECT (`github_id`, `github_login`, `avatar_url`) from the request body — client credentials say which PROJECT is calling, never who the person is. So any registered SSO client could (1) rewrite a real person's public `github_login` and `avatar_url`, which `upsertAccount`'s `ON CONFLICT` re-synced unconditionally and stored with no scheme check, and (2) mint the `<redacted>` row that [ADR 0171](<redacted>.md) D7's `membership_required` precheck reads — so the anti-impersonation gate on `POST /sso/activity/rollup` was satisfiable by the very call it was meant to gate, and forged credits/karma/rank reached a public profile. Decisions: (1) migration `<redacted>` adds `signin_verified_at`, written ONLY by `recordFederatedSignin` from the two moments the HUB itself authenticated a human for a client — `/sso/token` (a code minted under a live hub session, bound to the pair) and `/sso/device/poll` (a GitHub approval, audience bound from the typed origin) — so no request-body field reaches the statement and the column is evidence a caller structurally cannot write; (2) `upsertMembership` becomes `touchMembership`, an UPDATE gated on that column — a check-in can no longer CREATE a membership, answering `409 signin_required` and writing nothing, while signing in accepts a standing `pending` invite and leaves any other coarse kind to the instance to report; (3) a federated client gets insert-only `ensureAccount` (`DO NOTHING`), and `upsertAccount`'s identity re-sync belongs to hub-authenticated callers only — nothing is lost, because both of those already re-sync on every sign-in from GitHub's own answer; (4) the rollup gate reads `membershipBacksRollup` (non-pending AND witnessed), not row-existence; (5) `safeAvatarUrl` admits absolute https only, applied inside both account writers rather than at each render; (6) NO backfill — no predicate separates a legitimate check-in row from a forged one, and the first sign-in after deploy heals a real membership because the witness is stamped BEFORE the check-in and rollup that follow it, so a profile goes stale, never blank. No wire-format change: the request schemas are byte-identical and the ADR 0177 `membership_kind` window is untouched, so no federated instance upgrades. Proof `tests/sso_checkin_identity_spoof.mjs` (DB-free, in the default unit gate; 10 of 14 assertions fail against the pre-fix code, 2 controls prove the gate is not a wall). Rejected: carrying the hub assertion in the check-in body (strict validation would 400 every un-upgraded instance); ADR 0156-style source-guarding (answers "did the user set this", the wrong question when the writer is a third party); backfilling `signin_verified_at = joined_at`; a silent 200; retro-filtering `listMembershipsForAccount`.](<redacted>.md) | platform identity / security |
296
- | 0206 | [**A leaked bearer's remedy is the account holder's own revoke, not an absolute session ceiling** ([task 1003341](https://cloudbongos.com/builders#/task/1003341), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `lookupSession` rewrites `expires_at = now() + 24h` on every authenticated request, so a CLI bearer in daily use never expires; if it leaks, the holder keeps it alive simply by using it. `POST /auth/logout` kills only the token presented on that same request — useless when the token you want dead is the one you no longer hold — and the two endpoints that can kill another session are gated on the admin `auth.session.revoke` atom, so the victim had to find an Archon, and a single-owner self-hosted instance may have none. Decision: ship the OWN-scoped remedy (`GET /me/sessions`, `POST /me/sessions/revoke`, `POST /me/sessions/revoke-all` on `requireBuilder` alone, every statement keyed on the caller's own id from the presented session, the token value never selected or returned, `revoke-all` deliberately including the requesting session) and deliberately NOT an absolute ceiling on the sliding refresh: a ceiling is unconditional (migration 035 already had to backfill a 24h grace so it would not kill the session running it), any cap loose enough not to be a daily nuisance leaves a leaked token live for weeks, and the visibility this task ships — `created_at` beside `last_used_at` in the list — is the compounding half of the fix. A demotion already sweeps sessions, and a deactivated builder is denied on the hot path, so the authority-drift case is closed by other means. Rejected: a `created_at + N` cap now; a shorter TTL; three more routes inside the already-largest `routes/auth.js` (a separate file is what lets the no-caller-named-owner rule be asserted over a WHOLE file); a `keep_current` flag; 404 on a session id that is not yours (it would confirm which ids exist); lowering the `auth.session.revoke` floor. Revisit for machine principals, a compliance maximum, or evidence that people do not prune what the list shows them.](<redacted>.md) | auth / account security |
297
- | 0208 | [**Building the application lifecycle: the handle waits for its writer, and the vouch ADR 0201 D2.1 describes has no minter that can express it** ([task 1002284](https://cloudbongos.com/builders#/task/1002284), goal 1000045 — R08, criteria C3/C4). Builds [ADR 0201](<redacted>.md) D1 + D4 + D3.2 and records where the design could not be written literally. **The contradiction:** D1.2 says the migration adds "exactly" `applicant_github_id` + `vouched_at`; D2.6 says the applicant's handle is stored on the row. Decision — the handle waits for R09 ([task 1002285](https://cloudbongos.com/builders#/task/1002285)), because the ADR's own dependency table routes D2 *wholesale* (including the handle) there, and because R08 ships no vouch writer at all: a permanently-NULL `applicant_handle` would read as "this account has no handle" when it means "R09 has not shipped". **The hole:** D2.1's replay separation cannot be built as described. `mintHubAssertion` takes no `purpose`, and the asymmetry protecting `mintHubLogoutToken` works only because a logout token carries no identity — an application vouch carries both, so it collides with the login assertion's shape; worse, `verifyHubAssertion` does not reject a payload carrying a `purpose`, so a vouch minted that way would verify *as a login assertion* today. R09 needs a third mint/verify pair plus a tightening of `verifyHubAssertion` (free only while no minter emits a `purpose`), and a deliberate choice about the single-use `jti` store. Also decided: the echo's READ joins `platform_identity_sso_clients` (a `client_id` alone is not renderable) while the TABLE stays the three columns D1.3 named; a re-application refreshes `applied_at`, so the 30-day window runs from the most recent ask; an idempotent PATCH returns the row **without** re-stamping `resolved_by`/`resolved_at`, so the audit trail records who actually resolved it; `dismissed → invited` is refused `409 already_dismissed` naming `POST /access-requests/invite`; and `GET /my-projects` drops from `applied` any project the caller now builds on, so the hub never implies an outcome it is not allowed to hold. Honest limits: `vouched` is false for every real row until R09 lands (the two-class queue is proved by a seeded row, not by a live writer); `already_approved` still reaches the hub as "already_member" prose through `joinRelayOutcome`'s bare-409 branch, which is R10/R12's surface; and the proofs are DB-free, so the partial unique index is modelled rather than exercised — five mutations were run to show the fake pool bites. Rejected: adding `applicant_handle` anyway; bolting a `purpose` argument onto `mintHubAssertion`; a `status` column on the echo; refusing `dismissed → invited` with the existing `not_pending`; a new "project owner" atom.](<redacted>.md) | platform identity / admission lifecycle |
298
- | 0209 | [**Two routes answering one question spend one budget; whether the answer itself collapses is the owner's call** ([task 1003339](https://cloudbongos.com/builders#/task/1003339), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `GET /access-requests/status` (public, returns `member|invited|pending|dismissed|none`) and `GET /auth/web/admission-status` (its bare-boolean twin) both answer "does this GitHub login have an account here?", and neither was throttled — `rateLimitMiddleware` returns early for every non-write method, and the one read limiter is mounted only under `/public/*`. Scripted at line rate either becomes a login→membership roster: who builds here, and confirmation that a named person is present. Decision: ship the ceiling as ONE budget — `accountExistenceReadRateLimit`, 120 reads/60s per IP over a single bucket, exported as one middleware INSTANCE rather than a factory and mounted on both routes, because two limiters each configured 120/60s look identical in a diff, pass any test that counts to the limit per route, and hand an enumerator 240/min by alternating. The onboarding module reaches it through the doorway, so `src/bongos/middleware/rate-limit.js` joins `KERNEL_FILES` on `logger.js`'s argument: authz decides WHO may ask, the ceiling decides HOW OFTEN, and the file imports nothing. Deliberately NOT decided here: collapsing the response so a non-subject caller cannot tell `pending`/`dismissed`/`none` from "not admitted" — the consumer sweep found route 1 has exactly ONE consumer (`scripts/gds/login.js`), which already treats `pending` and `none` identically, so collapsing costs exactly one thing: the only message in the codebase that tells a declined applicant they were declined. That is a judgement about what an applicant is owed, so it goes to the owner as a blocker with the evidence. Noted either way: collapsing does not close the oracle — `admitted:true` still means member-or-invited. Rejected: a separate limiter per route; a `createReadRateLimit` factory (two matching call sites get two buckets — the generality loses the property); vendoring a fourth copy of the sliding window; hardening only the smaller route; collapsing on my own judgement because "nothing much" reads it.](<redacted>.md) | security / rate limiting |
299
- | 0207 | [**Building the connection graph: three departures from ADR 0202, and the three limits it is honest about** ([task 1002295](https://cloudbongos.com/builders#/task/1002295), goal 1000045 criterion C6 — the build of the connections wave). ADR 0202 decided the graph; building it surfaced three places where the decision's MECHANISM could not be written literally without losing the PROPERTY it was about, and three places where the shipped code cannot promise as much as a casual reading suggests. Departures: (1) **level-wise CTEs, not `WITH RECURSIVE`** — a recursive term can carry neither a per-level fan-out cap nor an ordered per-level truncation, which are the two things D3 actually decided, so a recursive build would have had a depth bound and no fan-out bound; a test asserts `RECURSIVE` appears in neither query. (2) **1st degree is an unbounded pair-index probe**, never a scan of the capped frontier — reading it off the `FANOUT_1`-truncated 1-hop reports "2nd" for someone in the viewer's own connections list once they pass 500 connections, and the rule this states is that **a cap may only ever make an answer less specific (`null`), never wrong**. (3) The shared visibility predicate lands in its own file, `modules/platform-identity/account-visibility.js` (`isAccountVisible` / `accountVisibleSql` / `accountActiveSql`), because task 1002967 had not shipped and `platform-identity.js` sat at the size ratchet with zero headroom — **1002967 adopts it rather than writing a second predicate**. Also decided: node visibility during traversal is viewer-relative and asymmetric on purpose (your OWN 1-hop floors on consent alone so a friend who goes private does not vanish from your list; every deeper hop clears the public predicate); cancel is a `DELETE` an ADDRESSEE may not use, so the 90-day cooldown cannot be dodged by calling a decline a delete; and the create response is `201 { status: 'pending' }` and nothing else, so a swallowed re-ask has nothing to fabricate. Limits stated rather than implied: a swallowed re-ask is silent in the RESPONSE and observable by its ABSENCE from the requester's outbox (the claim is narrowed, not the code — a decoy row would be a lie in the database to protect a lie in the API); the TARGET's 1-hop is capped too and that truncation is deliberately NOT reported, because saying so leaks one bit about a third party's degree; and the query COUNT is bounded while the frontier's WORK is O(sum of degree over the 1-hop), since `ORDER BY … LIMIT` materialises before it truncates — the revisit trigger is a measured p95, and the cheap next move is a `statement_timeout` returning the `null` D3 already chose. Deferred and named: a per-builder cap on OUTBOUND requests (the cooldown stops repeat asks to one person, not one builder asking a hundred — a real number is owner-signed), and a real-Postgres pass on the expire-then-insert write path, which CI cannot run.](<redacted>.md) | platform identity / social graph / privacy |
300
- | 0212 | [**The connections section is built on the profile page that exists, and it addresses a builder by asking the API to name them** ([task 1002296](https://cloudbongos.com/builders#/task/1002296), R20 · goal 1000045 criterion C6 — the UI half of the connections wave). The profile-page spec puts section 6 on the canonical `cloudbongos.com/u/<handle>`, which [task 1002806](https://cloudbongos.com/builders#/task/1002806) proved does not exist (no route, no page, no search entry, no sitemap row) and which belongs to goal 1000048 R14. Decisions: (1) **section 6 lands in the reserved slot R04 left on the hall profile page** (`modules/hall-ui/public/profile.{html,js}`), as a separate `profile-connections.js` so neither the page script nor `platform-identity.js` grows against its line ratchet, and it follows the CROSS-PROJECT section’s contract exactly — the markup ships `hidden` and reveals only where the hub answered, so an instance without `platform-identity` never renders it. It does not pre-empt the canonical page, which inherits the same three own-scope calls. (2) **Addressing: the page hands the API the `github_login` it holds and uses the `handle` the response echoes back for every write** — core `GET /builders/:id/profile` whitelists nine columns and `handle` is not one of them, and widening that whitelist is task 1002804’s canonical-URL work. `findAccountByHandle` already resolves current-or-previous handle and the route answers with the canonical one, which is the same move the API built for a stale Connect link. It fails closed into the deliberately indistinguishable 404 (unknown / provisional / private stranger), so the ONE honest gap is stated rather than discovered: **a twice-renamed handle has no connect button on this surface**, and it disappears when `/u/<handle>` lands. (3) Three rendering rules, enforced by `tests/profile_connections_ui.mjs` (real router over a WHERE-derived fake pool on a real socket, real client above it): `degree: null` renders "Out of network" and never "not connected", with copy that is BYTE-IDENTICAL whether `truncated` was set — varying it would rebuild the ADR 0207 L2 side channel in words; a sent request claims exactly `201 { status: 'pending' }`, so a re-ask swallowed by the 90-day cooldown simply shows "Connect" again next load with no explanation anywhere; and every count is the length of the list beside it, with `seen_at` and `truncated` never reaching the screen. Rejected: waiting for the hub page (it would block C6’s proof on another goal); building `/u/<handle>` here (the spec says only one such page gets built); adding `handle` to the core profile whitelist or to `cross_project` (the latter is `null` under `hide_stats`, which is exactly where the edge must still show — ADR 0202 D4 §3); resolving through public `GET /profiles/:handle` (it 404s for a private account the viewer IS connected to). Deferred and named: a public per-builder connections list (there is deliberately no such route), removal from the other person’s profile, and sections 7–8.](<redacted>.md) | platform identity / social graph / hall UI |
301
- | 0214 | [**The canonical profile URL gets one resolver, and it stays dark until the page it names actually resolves** ([task 1002804](https://cloudbongos.com/builders#/task/1002804), P08 · goal 1000045 criterion C1 — the canonical/redirect wiring). Profile-page spec Decision 1 wants `rel=canonical` on the instance, a link-out to the hub, and a numeric-id redirect; `/u/<handle>` does not exist (goal 1000048 R14 is `backlog`, and R01 — the ADR that fixes its PATH — is unwritten, while R03 proposes moving platform pages under `/bongos/…`). Decisions: (1) **one resolver** (`modules/platform-identity/canonical-profile.js`) answers where a profile lives, and it answers `null` while the page 404s — a canonical naming a non-indexable URL consolidates nothing and risks the one page that works, and the address is not even settled yet; one boolean flips all three surfaces together, and a test reds if a `/u/` route lands while it is still false. (2) **`{{hubOrigin}}`** joins `applyBrandTokens` (own `publicOrigin` on the hub, `loadIdpConfig().origin` when federated, `''` otherwise) — the server-side stamp ADR 0171 D1 asked for — delivered as a `<meta>`, never inside an `href`, because an empty value in an href degrades to a same-host URL claiming to be the hub. (3) **`handle` joins `GET /builders/:id/profile`** under the EXISTENCE predicate (`isAccountVisible`), not the rollup one: `hide_stats` suppresses numbers and never addressability, and the `handle`+`cross_project: null` pair is already published unauthenticated by `GET /profiles/:handle`. This closes ADR 0212’s stated gap — a twice-renamed builder now has a connect button. (4) **The hub redirects and a federated instance canonicalizes**, which is how Decision 1’s two apparently contradictory clauses reconcile: the hub serves the same person at both URLs, a federated instance serves genuinely different content. (5) **The redirect is not an existence oracle** — its non-redirect branch is the ordinary working page, absent/provisional/private are byte-identical, and the one bit it exposes is what `/profiles/:handle` already answers with 200-vs-404.](<redacted>.md) | platform identity / SEO / hall UI |
302
- | 0210 | [**The recruiting directory graduates on an instance-asserted ownership claim, and the recruiting opt-out gates the directory and nothing else** ([task 1002290](https://cloudbongos.com/builders#/task/1002290), R14, goal 1000045 criterion C5 — "project owners, not only Archons, browse builder profiles to recruit"). Two independent problems wore one task id. The reach flag `recruiter_discoverable` protected nobody — task 1002967 said so, and the Settings row stayed in the future tense because of it. And "widen the Archon-only /scouting" was already stale (ADR 0157 lowered `project.curate` to metic) while hiding the real question: **a project owner is not a rank**, and someone who registers a project is an Archon on their OWN instance and usually a bare `xenos` on the hub where the directory lives, so no floor reaches them without reaching every stranger. **D1** "project owner" on the hub is `<redacted>.membership_kind = 'owner'` — written by the PROJECT itself at `POST /sso/membership/check-in` from its own first-admin config, and refused unless the hub's own `signin_verified_at` witness stands (ADR 0205) — and never `platform_identity_projects.owner_github_id`, the hub-curated column ADR 0201 D3 names and forbids; the prohibition is about a hub-held list consulted before an ADMISSION on someone else's box, and this is the hub reading the instance's own mirrored claim before a read of the hub's OWN directory. **D2** the two doors are an OR in `modules/platform-identity/scouting-authz.js` (the `goal-authz.js` owner-or-rank shape, kept out of `platform-identity.js` at its size ratchet): ownership is asked first and a "no" falls through to the UNMODIFIED `requirePermission`, so a refusal keeps its exact code, `required`/`held` and floors map — the widening can only admit; the atom is spelled in the ROUTE's middleware list, because a gate hidden in the helper would make `route-rank-check.js` and `gen-api-docs.js` record this privileged route as `any-builder`; and a broken ownership read falls closed onto the rank door rather than admitting or 500ing. No atom is minted — `project.recruit` belongs to the console wave (spike 1002786 R3). **D3** `PAGE_GATES` gains an optional `alsoAdmits` naming a function on the `platform-identity.hub` port, declared on `requireProjectCuratePage` alone and consulted only after the permission check refused, so the `/people` shell admits exactly the two audiences its data route does; absent port, absent function or a throw all deny. **D4** the reach COALESCE gets its SQL twin in `account-visibility.js` (its sole owner) and composes into `scoutingListableSql` = rollup AND reach, which `listScoutingRollup` joins — but reach is deliberately NOT folded into `rollupVisibleSql`, because the community spec D3 says the opt-out governs the recruiting surface only and folding it down would silently empty a future public leaderboard. **D5** no migration: both columns exist and migration 003's `(github_id)` index already serves the probe. **D6** the ownership query JOINs `platform_identity_sso_clients` on `status='active'` — a membership row never expires and nothing ages it out, so suspending the client (the lever that already stops a project signing anyone in) had to revoke the read it derives, or suspending a rogue project would leave its owner reading every builder on the platform forever. The task's own adversarial review also found the sole-owner drift guard was matching only the UNALIASED spelling of the reach COALESCE — i.e. not the one that ships — and two fail-open defaults (a prototype-reachable `alsoAdmits`, and an alias guard resting on template evaluation order); all three fixed and mutation-checked. ⚠ Honest limits recorded, not fixed: the hall NAV still hides People from a project owner (task 1003349, sequenced with R15 which owns that surface), the `touchMembership` write path stays as ADR 0205 left it, and `openapi.json` cannot express "or owner". ⚠ And DELEGATES do not reach the directory yet (blocker 1000122 + task 1003350, linked) — an instance reports `owner` for its first admin only, so the registrant half works and the delegate half needs the instance to report `owner` for any archon, which changes a cross-version wire contract and a shipped bucketing; filed as blocker 1000122, and nothing here changes when it is answered. Rejected: lowering the floor further (a floor low enough to admit a registrant excludes nobody); a `project.owner` atom (ADR 0201 D3 rejected it); minting `project.recruit` here; gating on `owner_github_id`; folding reach into the rollup predicate; widening `membershipKind()` on our own judgement; shipping the route without the page.](<redacted>.md) | platform identity / authz + privacy |
303
- | 0215 | [**The pre-accept recruiter sliver is a CEILING, not a step: a whitelist keyed by consent-and-reach, on the recruiting surface’s own door** ([task 1002968](https://cloudbongos.com/builders#/task/1002968), PV3 · goal 1000045 criteria C2/C5; privacy spec D3, owner-signed 2026-08-16). Privacy spec D2 has a row nothing could serve — **private · discoverable** — and ADR 0210 D4 named the reason: *“the full rollup is never its disclosure — the D3 sliver is”*. Decisions: **D1** the audience predicate is `recruiterSliverVisibleSql` = `accountActiveSql AND recruiterReachSql`, composed in `account-visibility.js` (its sole owner) and the ONE composed answer that deliberately **omits the existence axis** — ANDing `accountVisibleSql` would empty the sliver of exactly the account it serves; dropping it is safe only because reach is `COALESCE(recruiter_discoverable, visibility=’public’)`, so a private account reaches this surface only by its owner’s explicit opt-IN — and, after adversarial review, that opt-IN must be the one they MEANT: `setAccountPrivacy` now resets `recruiter_discoverable` to NULL when a PATCH turns an account private without naming reach, because a `true` set while PUBLIC meant “recruiters may see my public profile” and this predicate would have activated that stale consent silently. `profile_state=’active’` still floors it (a provisional account is never recruiter-visible) and `hide_stats` is absent, because hide changes the sliver’s SHAPE, never who appears in it; the guard PINS that existence-less predicate to its one reader, since a future public read joining it would look adopted while serving private accounts. **D2** the projection is a literal whitelist — `handle`, `display_name`, `avatar_url`, `disciplines`, `active_recently`, and under `hide_stats` the last two are **absent, not null**, with the activity query never issued at all (the cheapest way for a later edit to leak a number is for it to already be in hand). `last_active` is compared to the 30-day window IN SQL and only the boolean returns, so the date never enters the process; `github_id`/`github_login` are never KEYS, every count and any profile URL stay out — with one limit stated rather than glossed: the avatar D3 grants is GitHub’s own URL and embeds the account id, so the proof pins WHERE the id may appear instead of claiming an absence the code does not deliver. **D3** the handle is an ADDRESS, not a door — `GET /profiles/:handle` joins `accountVisibleSql` and 404s for the very account whose sliver was just read (asserted, with a 200 control), and a PREVIOUS handle resolves to nothing here unlike the public page’s 301, because a recruiting read has no shared links to preserve and a redirect would confirm a rename. **D4** one route, `GET /scouting/:handle`, beside the directory in `routes/scouting.js` but deliberately NOT sharing its gate: **no `project.recruit` atom is minted** (ADR 0201 D3 and ADR 0210 both reserve it for the console wave that defines it), and until it exists the sliver holds spec D1’s declared floor for that atom — `api.requireRank('archon')`. The first draft reused the directory’s door and that was WIDER than the owner-signed floor on both halves (`project.curate` at metic, OR any project owner — typically a bare xenos), on the first surface that discloses a PRIVATE account; narrowing after people are disclosed is not recoverable, widening later is. The audience question is the owner’s and is FILED, not taken — blocker 1000126, with the C5 tension it carries and a one-line swap either way. 404 never 403 (a 403 confirms the handle), `no-store` (ADR 0171 D5). **D5** it grants nothing (ADR 0016): the module is required by exactly ONE first-party server file — the route that serializes it — enumerated from source, and no rank/role/permission/membership travels with it. **D6** the Settings copy widens back from “the People directory” (narrowed by task 1002290 while this was unbuilt) to name **both** recruiting surfaces — “both” and not “every”, because a guard asserts `routes/scouting.js` mounts exactly two routes and that both readers compose `recruiterReachSql`. The half that was actually WRONG was the grant itself: the copy promised a hidden account “your name and nothing else” while the sliver also ships the AVATAR, never named the picture in the shown branch, and never told a private account this is the one thing a recruiter sees while their page answers “not found”. The guard now iterates SLIVER_KEYS from the module instead of checking transcribed phrases, so a sixth granted field cannot ship undescribed. **D7** no migration; the port lives beside `getPublicProfileExtras` in the MODULE, not the file, which is on the 1500-line ratchet. The proof is a whitelist over the module’s own exported key list, planted improbable sentinels asserted absent from the whole body with controls against vacuity, and a fake pool that reads boolean STRUCTURE — the house interpreter collects terms and ANDs them, so rewriting this predicate’s `AND` to `OR` (which in Postgres serves EVERY active account) passed all three test files green until each term was made to be CONSUMED and the residue required to be pure conjunction. That hole is still open in five sibling proofs (idea 1000877). Four further defects came out of the same review: the credits/karma sentinels sat on rows the sliver never reads, a “control” compared a fixture to itself, and the one-reader pin missed the predicate’s JS twin. ⚠ Honest limits: **no UI consumes it yet** (the recruiting view is the console wave’s and task 1002972’s); **the audience was unresolved when this shipped and is settled now** (blocker 1000126, answered 2026-08-29 — the owner KEPT the archon floor; see ADR 0217 D2) — the floor is still narrower than goal C5 describes, so project owners cannot use the sliver today, a tension ADR 0217 D1 resolves by making C5’s subject (browsing) ungated rather than by widening this surface; the two-route guard counts only `routes/scouting.js`, so a recruiting read added elsewhere would not trip it; the route is unmetered (idea 1000876); and `active_recently` understates for a builder whose only project never reports a rollup. Rejected: flooring on `accountVisibleSql`; folding reach into `rollupVisibleSql`; minting `project.recruit`; **reusing the directory’s door**; leaving a stale `recruiter_discoverable = true` to read as consent after an account goes private; a second route file; null-instead-of-absent rollup keys; a profile URL / `github_login` / a project count / a raw `last_active`; 301-ing a previous handle; 403 for a non-discoverable builder.](<redacted>.md) | platform identity / privacy |
304
- | 0213 | [**The platform T&C acceptance is the hub's own witness, written once, and consent becomes two columns** ([task 1002913](https://cloudbongos.com/builders#/task/1002913), goal 1000045). ADR 0141 §4 deferred consent for cross-project aggregation to "the platform T&C" and ADR 0171 D3 wrote its own definition around the moment that acceptance "can later be captured" - but no acceptance record existed, and while `/terms` DOES exist (task 1003251) it says nothing about work crossing between projects, so the public-profile premise ("already carries platform T&C consent") was untrue for every account. The scouting.js:6 pointer the task named had also been overwritten by ADR 0210, so the debt was going invisible rather than getting paid. Decision: `terms_accepted_at` + `terms_version` on `platform_identity_accounts` (migration 019), written ONLY by `upsertAccount`'s `activate` branch - one caller, /sso/authorize, where `activate: true` is a source literal and every field comes from the hub's resolved session (the ADR 0205 D3 property; a record a caller could assert is evidence of nothing). COALESCE makes it write-once, so the column keeps meaning WHEN consent was given. The consent axis in `account-visibility.js` becomes `profile_state = 'active' AND terms_accepted_at IS NOT NULL`, composed into every other predicate, which is what makes the tie structural rather than a promise. NO backfill (D4): existing active accounts re-accept on their next hub sign-in - ADR 0171 D3's own rule and P03's ratified rationale - and that visible consequence, plus the wording itself, went to the owner on blocker 1000123. Rejected: an acceptances TABLE (no event stream yet), a click-through gate (an interstitial in a redirect built to be invisible; additive later), backfilling (records a consent that never happened), deriving profile_state from the timestamp, and putting the predicate in `terms-acceptance.js` (a second owner of an effective-visibility answer, which ADR 0202 D4 forbids).](<redacted>.md) | platform identity / consent / privacy |
305
- | 0216 | [**A fake-pool interpreter must FAIL LOUDLY on what it cannot model; the refusal is uniform, the interpreters stay separate** ([task 1003354](https://cloudbongos.com/builders#/task/1003354), folding in idea 1000877 from the adversarial review of [task 1002968](https://cloudbongos.com/builders#/task/1002968)). Nine test files carry a **fake pool** whose row filter is derived from each statement’s own WHERE text — the design that makes an isolation proof say something about the real query rather than about a canned response. It has one silent failure mode: a term the interpreter does not recognise is simply not applied, so the boundary goes vacuous and every assertion above it keeps passing. Each file guarded a piece of that (unread `$N`, unknown literal, unmodelled `IS NULL`) and every one of those is a **term** guard — and the term list is not the predicate. Three reviews found the same shape. **The missing term:** ADR 0213 made consent two columns and `scouting_owner_access_and_reach.mjs` modelled only the first; a NULL test is a third syntax neither sweep can see, so the suite reported 17 passed while modelling a predicate the database no longer had, inside a file whose own header calls that impossible. **The rewritten joiner:** an interpreter that COLLECTS recognised terms and ANDs them re-imposes an `AND` the SQL may no longer contain — rewrite a predicate’s `AND` to `OR` and Postgres serves every active account on the platform, or lets any builder revoke any session by id, while the fake answers exactly as before. **Recognition is not application:** ADR 0213’s own first draft checked only that a column name was recognised, and a mutation deleting the filter still passed. Decisions: **D1** the governing rule — an interpreter that cannot model a term must REFUSE the statement, never ignore the term; incompleteness is allowed, silence is not, because a refusal fails the build in the session that changed the SQL while a silent drop fails nothing ever. **D2** the mechanism is a CONSUMING parse: each recognised term is spliced out of the text and the residue must be a pure conjunction (`/^[\s()]*(?:AND[\s()]*)*$/`), which subsumes every term guard, catches `OR`/`NOT`/`CASE`, and retires ADR 0213’s `modelled`-set bookkeeping because an unapplied term is by construction still there; a legitimate `OR` (the rename window, the either-side clause, the expiry disjunction, the rescind guard) is consumed as a whole unit. **D3** the interpreters stay SEPARATE and only the refusal is uniform — the term vocabulary differs completely across six tables, so a shared registry would be a small SQL engine and would couple unrelated proofs; instead every copy carries the identical guard and its OWN calibration test, which is strictly better anti-drift than extraction (a helper proves the guard exists somewhere; a calibration test proves it is reached by that file’s own statements). **D4** a modelled term proves nothing until a fixture makes it observable — three fixtures added, each differing from its control in exactly one column, so the mutation that deletes the term has somewhere to land; where a file deliberately lacks one because another owns that matrix, the comment says so. **D5** every copy verified BY MUTATION against the real source — ten mutations, ten reds, each a 0-failure pass beforehand. ⚠ Honest limits: no fitness check asserts a NEW fake pool carries the guard (identifying “a fake pool” needs a heuristic, and a heuristic gate over test internals has a false-positive cost) — the per-file calibration tests hold the nine that exist and a tenth is a review question; `canonical_profile_url.mjs` is untouched because its residue must be EMPTY, strictly stronger already. Rejected: one shared interpreter module; a fitness heuristic; keeping `KNOWN_NULL_TESTS` + `modelled` beside the structure guard; parsing the boolean tree properly; accepting green as verification.](<redacted>.md) | testing / proof integrity |
306
- | 0217 | [**Rank gates INVITING, not VIEWING: the owner’s answers to the account-security review’s five blockers** ([task 1003358](https://cloudbongos.com/builders#/task/1003358) · goal 1000045; answers blockers 1000121/1000122/1000123/1000124/1000126). The 2026-08-29 account-security batch shipped fourteen tasks and deliberately refused five audience/consent questions. Four turned out to be the same question, and one answer reframed the rest. Decisions: **D1** the governing line — *rank gates the ability to INVITE; it does not gate the ability to VIEW*. Seeing what someone built across projects is not privileged (the T&C already promises that data is public by consent, to search engines included); reaching out to them costs them something, and that is where a floor belongs. So `membership_kind` is **NOT** widened — `membershipKind()` stays FIRST_ADMIN-only, the federated wire contract is unchanged, the My Projects owned/building split keeps its meaning, the cross-version window never opens, and task 1003350 is **abandoned**. A **per-project INVITE rank floor defaulting to archon** replaces it (task 1003362), set by that project’s own archons — strictly better than a founder requesting a platform-side `project.curate` grant per teammate. Viewing gains no gate, on a new cross-project achievements page that per the owner **lives on cloudbongos.com, not the hall variant** (task 1003363) — so `public-landing/` inside the apex-only region, not `modules/hall-ui/`. The risk to hold as it is built: public consented profile data and a PRIVATE account’s recruiter sliver are different material, and the sliver must not ride along — not as a row, a facet count, or a search-result absence that reveals presence. **D2** the sliver keeps its **archon floor** (blocker 1000126), and ADR 0215 D4’s defensive hold becomes a positive agreement: the invite floor also defaults to archon, and the C5 tension dissolves because C5 was about browsing. Whoever builds 1003362 must check whether it IS the console wave’s `project.recruit` atom before shipping a rival knob. **D3** the consent pair — `/terms` said acceptance is recorded “by signing in here” but the stamp is reachable only via `/sso/authorize`, which a CHILD PROJECT reaches, so a hub-only builder was never stamped and (NULL `terms_accepted_at` → `isAccountActive` false) was invisible everywhere, permanently, under no-backfill. Owner took **both halves**: correct the sentence now (task 1003353) and **capture at the hub’s own sign-in** next (task 1003361), through the `platform-identity.hub` seam so child instances see no change. On grandfathering the owner chose **B — grandfather** (task 1003360) against the recommendation, having been shown that it records a consent which did not happen; the ledger does not absorb that silently, so a grandfathered acceptance is **marked** (queryably distinct from a sign-in, via a recorded source not a magic timestamp) and `/terms` **says so**. ADR 0213’s honest property is not “every acceptance was a sign-in” but **the record never claims more than what happened**, and that survives. **D4** `/access-requests/status` **collapses** to `admitted` + one `status` of `member`|`invited`|`not_admitted` (task 1003359, the fix ADR 0209 already named). It does **not** close the oracle and must not claim to — `admitted:true` still means member-or-invited; the 1003339 limiter is what reduces enumeration, and collapsing narrows what leaks about **applicants**. The cost is paid in the same change: `login.js` is the ONLY place telling a declined applicant they were declined, so its 30-minute timeout copy is rewritten to be honest rather than promising an approval that is never coming. ⚠ Also: report 1000027 (the live SSO check-in identity hole) marked **fixed**, held open until now so the ledger would not claim a fix while the hole still served; `verifyHubAssertion`’s purpose-confusion weakness filed as report **1000028** (medium, latent — zero call sites, becomes an admission bypass the moment ADR 0201 D2.1’s vouch is minted, so the fix must land BEFORE the feature that arms it). **New invariant:** any gate added to a viewing surface, or any recruiting/invite surface below the archon floor, contradicts this ADR and needs one that supersedes it.](<redacted>.md) | platform identity / privacy / permissions |
307
- | 0218 | [**The apex-root web surface: a module may own the front door, and says so with a flag, not an empty prefix** ([task 1003322](https://cloudbongos.com/builders#/task/1003322) · goal 1000080 — the last of the five tasks scoped with the owner on 2026-08-28). Carves the repo-root `public-landing/` into `modules/public-landing/`, the THIRD web-surface module after status-UI (BV1.R84) and hall-UI (BV1.R85) and the first that is not a subdomain. The obstacle: `moduleWebSurfaces()`'s whole routing vocabulary was a Host-header PREFIX, and the apex root is not a prefix of anything — it is whatever is left once the subdomains are taken. So a surface now names where it answers in exactly one of two ways, `{ host: "status.", dir }` or `{ apexRoot: true, dir }`, never both and never neither. **`host: ""` is rejected on purpose:** the routers match with `host.startsWith(s.host)` and every string starts with `""`, so an empty prefix would answer on `builders.`/`status.` too AND make `isModuleSurfaceHost()` true for every request — the predicate eleven `/builders` guards use to step aside — silently un-serving the hall. A boolean cannot be mistaken for a prefix. The landing keeps its DEDICATED serving block (the hall precedent): the apex-only strip, the branding stamp, the `?v=` content stamp and the six routes are untouched; only `LANDING_DIR` changes, from a hardcoded path to a lookup through the loader, resolved by the `apexRoot` PREDICATE so neither `platform-server.js` nor `serve-internal.js` names a module key (ADR 0083 §Decision #4). Disabling the module removes the front door outright and the apex answers the game-less 404 tail — the module contract's "disable without breaking anything" in its most literal form. At most one apex-root surface: the first discovered wins and a second is logged. Also: the BV1.R76 host-identity gate learned that an **apex-only region is exempt** — the server deletes it on every non-hub instance, so it cannot make a child render the hub's brand, which is the failure that gate names. Evidence: the five apex pages hashed as served, before and after, differ by exactly the same 22 bytes — `"public-landing":true,`, the module's own `window.__MODULES__` entry — and are otherwise identical; the 106 moved files hash identically on disk.](<redacted>.md) |
308
- | 0219 | [**A look is a branding pack: the style library ships looks as `theme.ui` overlays, never as sheets** ([task 1003323](https://cloudbongos.com/builders#/task/1003323) · goal 1000083). The owner scoped "varying ui styles" for the ui-design module; the shape was decided by three facts: every served sheet already reads only the contract and is measured by a suite that resolves its own expressions under the pack (ADR 0196), the pack is one-mode (light values; dark is each sheet's black tier), and the world round's candidate token layers were sheets, not packs. Decisions: **D1** a look is `modules/ui-design/styles/<name>/` = `pack.json` (the `theme.ui` overlay, exactly the thirteen colours + the two font stacks and their links) + `DESIGN.md` (the derived recipes in the root DESIGN.md's frontmatter shape, thirteen in lockstep with the pack) + `mock.html` (a specimen with no `:root`, served through the stub with the pack spliced in), never a fork of a sheet; a sixteenth token is a contract change. **D2** `chrome-world` is canonical and pinned byte-for-byte to the neutral pack; its DESIGN.md points at the root one. **D3** the alternates `expedition` (natgeo-editorial) and `grove` (organic-cards) are ported by retuning the thirteen against the three sheets' own pair lists in both modes until every pair clears AA (the grove's sage lifted and its inks deepened for the hub's status vocabulary and the hall's chips); dark under both is the sheets' black. **D4** the picker rides `GDS_BRANDING_FILE`, the fixed knob `src/branding.js` already has: `STYLE=<name>` on the stub, `--style` on render/probe, `--pack <name>` on contrast/check-mock; no core change. **D5** the three token suites take `UI_DESIGN_PACK` and their drift tests keep measuring against the neutral pack; `tests/ui_design_styles.mjs` runs them once per alternate and proves the knob live with a pack that must fail. **D6** an instance adopts a look by copying `theme` into `config/branding.json`; what stays the platform's whichever look is on: the dark ground, the hall's verification vocabulary, every sheet's derived tier, the floors. Rejected: sheets per look beside world.css; a dark tier in the contract; a picker that writes the host boundary.](<redacted>.md) | ui-design / branding contract |
309
- | 0220 | [**An in-house rebuilt skill is a first-party skill: it lands at `skills/<name>/`, opens with the same preamble, carries a "Rebuilt from" section instead of a sidecar, and is counted** ([task 1003325](https://cloudbongos.com/builders#/task/1003325) · goal 1000083). ADR 0198 landed both scanned origins as rebuild specs and left the roster empty; this is the first rebuild — the taste core: `design-taste-frontend`, `design-taste-frontend-v1`, `high-end-visual-design`, `redesign-existing-projects` — and the policy had not said where a rebuild lives, what replaces `PROVENANCE.md`, whether a preamble written for vendored copies still fits, or whether five byte-identical copies of it trip the doc-entropy ratchet (they would have: baseline 6). Decisions: **D1** a rebuild is `modules/ui-design/skills/<name>/SKILL.md`, declared in `module.json`, materialised by step 1b's own-dir branch (no sidecar gate), first-party and counted by every ratchet — no `rebuilt/` exemption like `vendor/`, because these are our lines. **D2** one preamble worded for both origins ("vendored from a scanned origin, or rebuilt in-house from a spec"), still byte-identical everywhere; a rebuild's provenance is its `## Rebuilt from` section first after the block: the origin the SPEC was derived from (documentation only), the verdict, the spec task, "no upstream bytes consulted or copied", this ADR, and what changed in translation; no `PROVENANCE.md`, no `LICENSE`. **D3** the translation rules for every rebuilt taste skill: the palette is the pack and variants come from the style library (ADR 0219); the design system is the instance's `DESIGN.md`; imagery is hero plates and hero objects by token and never a step; every motion rule lives inside the Kill Switch (springs, magnetic hover, pointer-tracked borders are named *not available*); the upstream's bans are defaults an instance's `DESIGN.md` overrides, and where the world calls for what an audit would flag, the world is the answer key. **D4** `docs-entropy.js` skips a paragraph opening with the preamble marker beside its generated-block skip, calibrated against a plain duplicate that is still flagged. **D5** v1 is a distinct skill differing in content (the fixed triple, the stack conventions, the pattern vocabulary, the tile-grid archetypes), not a version stamp. **D6** the pins in `tests/ui_design_skills.mjs`: frontmatter name = dir, preamble first and once, the section's task link + ADR 0198 + "no upstream bytes", the style library and "hero plates" named, no image mandate, no sidecar, every door lists the four. Rejected: a sidecar with the spec's commit; a second preamble; a `rebuilt/` segment; vendoring anyway (an owner override, not taken); folding v1 into v2; raising the doc baseline.](<redacted>.md) | ui-design / skills |
310
- | 0221 | [**The impeccable method is one skill with sub-command playbooks: one preamble, one playbook per request, the three homes are the instance's own files, and the origin's mechanisms are dropped rather than disabled** ([task 1003326](https://cloudbongos.com/builders#/task/1003326) · goal 1000083). `pbakaus/impeccable` scanned `dangerous` (ADR 0198: hooks written into five harness configs, a child agent with permissions bypassed, an instruction override, a live-server credential in page source, an unverified remote bundle, default-on telemetry), so its method is rebuilt from the spec. ADR 0220 fixed where a rebuild lives; it had not said how a family of sub-commands lands, what the origin's three persisted homes become, or what to do with the six spec areas whose substance is the condemned mechanism. Decisions: **D1** one skill at `skills/impeccable/`: `SKILL.md` (invocation, the menu from live signals, session setup, the three homes, the routing table, the craft floor, the bounded finish, what is not here) plus `reference/<sub-command>.md`, eighteen playbooks, exactly one loaded per request (the spec's own entry rule); the preamble pinned once in `SKILL.md` and governing every playbook; the materialiser's tree copy ships `reference/` unchanged. **D2** the three homes are the instance's files (product truth = `docs/project-context.template.md` + the pack's identity; the visual system = the root `DESIGN.md` and a look's; the brief = the task's body + a fixed brief block in the ship notes); the skill creates none, `shape` proposes diffs, `document` drafts on the claim's branch with the refresh / overwrite / merge choice. **D3** dropped, not disabled, each with a destination: hooks → none (the ship gate is the sweep, the pre-flight is the kit); the bypass agent → an ordinary read-only subagent via the Agent tool; telemetry, the remote bundle, the credential → none; the live loop → the playwright plugin driving the kit's stub, variants as states, accept = commit, discard = checkout, nothing injected; the detector → task 1003329; the asset producer → the hero-plates rule and task 1003328; native → declines to `/design`; `init`/`doctor`/`pin`/installer → the materialiser is the install. **D4** the finish is the kit in bounded rounds (build, one batched inspection, one batch of fixes, at most one confirming round, stop, one budget) and the reviewer answers with one of `clean` / `recapture` / `defects` / `contradicted` after an evidence check and its own mock inventory. **D5** the craft floor is mechanics loaded late and never recited, translated for a world that may be hairlines by rule. **D6** the pins: sixteen routed playbooks, no orphan, no preamble copy in a playbook, and nothing in the tree directing a bypass, a hook write, a settings edit, telemetry, a remote download, a credential or an override. Rejected: eighteen skills; one inline file; a hooks-lite opt-in; an elevated reviewer; a `doctor`; the native playbooks.](<redacted>.md) | ui-design / skills |
311
- | 0222 | [**The image family under the hero-only rule: plates and hero objects, never screens; the mark is drawn; the board is a contact sheet; the pair and the sidecar are the output contract** ([task 1003328](https://cloudbongos.com/builders#/task/1003328) · goal 1000083). The four image skills of `Leonxlnx/taste-skill` (scanned `dangerous`, ADR 0198) are built around pictures of interfaces: one reference image per section, one app screen per flow step in a phone mockup, an identity board with the brand name rendered in, and a build loop whose truth is the generated image; the panel flagged the eagerness mandate, the image-count rule and the must-invoke-any-tool directive by name, and the owner's rule (2026-08-26/27) forbids the category. Decisions: **D1** the subject is the composition's plate slots and the count is the slot count; variety survives as anchors (field, beside-bleeding, masthead, orb, backdrop; text-left image-right refused), the palette lock is the pack, cross-frame consistency is one family (material, light, lens, ground). **D2** a screen is the surface itself: the mobile skill makes portrait plates for 390 / 320, the flow is the states file rendered by the kit, the bible is `DESIGN.md`, no phone mockup, native declines to `/design`. **D3** the source of truth for UI is the owner's references and the world; `image-to-code` reads them (analysis, extraction spec, faithful translation, drift as the failure mode, the clarity checklist) and the only image it may cause is the hero plate. **D4** the mark is drawn as inline SVG, the colour system is the pack, the type specimen is the faces on a real page, the applications are the kit's renders, the image direction is a hero-object family, the nine-panel board is a contact sheet of real parts. **D5** the output contract is DESIGN.md's pair and sidecar (JPEG dark plate, WebP alpha cutout, `<file>.<ext>.json` beside each, `--plate-*` tokens on `body`) through the instance's art pipeline key, the spend logged, a white ground through the remover, alpha verified with ffmpeg, never a crop. **D6** the worked example is the grove specimen's hero object, not the front door. **D7** the pins: the four declared, each naming the sidecar, "text-free" and the art key; no eagerness / count / per-section / must-invoke directive. Rejected: per-section images for reference only; phone mockups around real shots; a generated logo as a start; the plate on the front door; a shared base skill with wrappers.](<redacted>.md) | ui-design / skills / imagery |
268
+ | 0182 | [**Project visibility is TWO axes: a derived publish gate and a stored join door** ([task 1002320](https://cloudbongos.com/builders#/task/1002320), goal 1000046). Goal 1000046 seeded 31 tasks each citing "per R01" — this ADR — so nothing in the goal could start until it landed: it gated **29 open tasks**, the largest single chokepoint in the repo, while itself parked at `backlog` with no dependencies. The spec its planning task cites was never written, so this is the design of record. The goal's criterion says **four** states (Public/Private/Stealth/Unlisted) and R04 says **three** stored plus a derived dark-matter; both are right and conflating them is the trap. There are two INDEPENDENT axes: **publishability** (DERIVED from the identity gate — name + description, per the owner interview) decides whether a project is on the map at all, and **the join door** (STORED `visibility ∈ public|private|stealth`, owner-set) decides how it renders and who gets in. Four user-facing states = 3 × publishable + one derived state that collapses all three. The column name `visibility` is a mild lie kept for consistency with `goals.visibility` (ADR 0112) — it does NOT decide visibility, publishability does — and the ADR is the mitigation. All three stored states are VISIBLE; only the derived state is invisible, which is what makes the 3+1 split honest rather than a workaround. `private` inherits ADR 0112's exact meaning (visible-but-closed); `stealth` exposes EXISTENCE but not IDENTITY, and its redaction is **server-side in the feed** — the naive client-side version leaks to anyone opening the network tab, and the orbs page is a static site OUTSIDE this repo (R09) so it could never be enforced there. Three mechanisms touch "does this appear" and are separated: `status` (operational), `visibility` (owner), `featured` (Archon editorial) — on the map iff `status='active' AND publishable`, with `featured` only ranking among those. That is a real behaviour change: today's predicate is `featured=true AND status='active'`, so **featuring is currently load-bearing for visibility at all** and the goal's owner-control premise silently does not work without it. Visibility is owned by the INSTANCE and the hub row is a CACHE (ADR 0141 §3 + the admin-console spike's D1) — so `owner_github_id` being "advisory, not an authority" is CORRECT rather than a gap, R05 moves to an instance surface, and R08's absent-report default is fail-closed (unreported ⇒ dark matter, off the map rather than exposed by omission). Per-state join rules compose with the separate `open|closed` join policy ([task 1002817](https://cloudbongos.com/builders#/task/1002817)) by one rule: **visibility sets the ceiling, join policy may only narrow** — otherwise the two settings are a coin-flip decided by evaluation order. Finally, `stealth` COLLIDES with the existing instance-wide pre-launch password gate (`STEALTH_PASSWORD`, `src/bongos/stealth-gate.js`), which means nearly the opposite (a whole instance made unreadable vs a project publicly on the map, anonymously); per ADR 0174 the product meaning keeps the word and the infra rename is filed separately rather than decided by an ADR about visibility. Rejected: a stored 4-valued enum (makes `unlisted` compete with `private`, and lets publishable+unlisted disagree), a separate `published` boolean (can drift from the identity it summarises; derived cannot), and keeping `featured` as the visibility gate (an owner who sets public and appears nowhere has not been given control).](0182-project-visibility-two-axes.md) | platform-identity / project visibility |
269
+ | 0183 | [**A done-when criterion closes itself; the review queue becomes a place to look** ([task 1002069](https://cloudbongos.com/builders#/task/1002069), goal 1000072, owner request 2026-07-05). ADR 0086 §6 made `satisfied` reachable exactly one way — a Metic+ calling `POST /done-when/:id/satisfy` — even though the signal it gated on is unambiguous (**every** gating task shipped) and a human confirming it adds no information. When nobody runs `/goal-review` the result is not a careful pause but a **lying board**: this session measured goal 1000072 at `6/9 shipped, 0/5 criteria`. Now one helper, `doneWhen.autoSatisfyShippedCriteria(exec, {taskId})`, runs from **two** places for two different reasons — inside `shipTask`'s txn (scoped, **immediate**, on the txn client so a rolled-back ship cannot leave a goal achieved) and unscoped on every `publish-reconciler` tick (**backlog drain** for criteria whose tasks shipped before this existed, since nothing will ship against them again, plus **self-heal**). Idempotent by construction — it reads only `satisfied=false` rows — which is what makes an every-tick sweep safe. **The load-bearing subtlety is the delivery guard.** Auto-close needs BOTH "nothing still pending" AND **≥ 1 task actually `shipped`**; the second is not redundant, because since [task 1002651](https://cloudbongos.com/builders#/task/1002651) made `abandoned` terminal-not-pending, a criterion whose tasks were **all cut** satisfies the first clause *vacuously* and would auto-close on ZERO delivered work, cascading its goal to `achieved` (measured: `buildCriterionProgress` returns `pending_review:true` for exactly that shape). Yet `pending_review` **keeps** that wider meaning on purpose — it asks "should a human LOOK at this", and 1002651 chose that an all-abandoned criterion surfaces so someone decides whether it still means anything. So: everything that auto-closes was flagged; not everything flagged auto-closes. An earlier draft "aligned" the two predicates and broke that intent — `tests/criterion_progress.mjs` caught it; **do not re-align them**, the gap is the design. `/goal-review` is kept, demoted from gate to viewing surface; manual `POST /satisfy` stays for what automation cannot judge (a criterion with no linked tasks never auto-closes). Honest limits recorded: auto-satisfy cannot help a criterion nothing is LINKED to, and the in-txn close logs before commit (accepted — threading the summary out through `withTx` would cost ~5 lines in the over-threshold `db.js` under shrink-only ratchet policy). Rejected: sweep-only (up to 5 min to see your own goal close), hook-only (never drains the backlog), a stored `pending_review` column (second source of truth), deleting `/goal-review`.](0183-criteria-close-themselves.md) | lifecycle / goal tier |
270
+ | 0184-a | [**A task's visual is its value summary in another medium, and inherits that audience** ([task 1003109](https://cloudbongos.com/builders#/task/1003109), goal 1000072, from owner idea 1000400 filed in Discord `#ideas` 2026-06-24). A shipped task can now carry ONE optional image beside its `value_summary` — a screenshot of the surface that changed, a before/after, a diagram — so a reader who does not read code can **see** what shipped instead of reading about it. The rest of that idea had already landed (the note itself; the spend half via `/cost`); the picture was the whole gap, and the only path a builder had was a Discord post — outside the ledger, on a CDN whose links expire. The decision that keeps it from rotting is a REFUSAL to invent a second privacy axis: **a visual has no audience of its own.** One predicate says who may see it — `task-visuals.isPubliclyViewable` (public = `shipped|confirmed`, byte-for-byte the test `routes/public.js:projectPublicTask` already applies to a title/summary) — so any future surface rendering `value_summary` inherits a settled answer for the picture beside it. Three consequences are load-bearing. (1) The serve gate lives **inside** the handler, not in mounted middleware, because the audience is a property of the ROW; the stored filename therefore carries the task id (`task-<id>-<16hex>.<ext>`), which is simultaneously the path-traversal defense and how the route finds the task, and every step fails CLOSED (unparseable name → absent task → not-public → `requireBuilder`, never past it). (2) The public shipped feed **mirrors the predicate in SQL** — `db.recentShippedTasks` nulls the URL for a `security_sensitive` row, rather than selecting the flag and filtering in JS, so the unauthenticated dashboard never advertises an image the serve route would refuse AND never learns the flag; change one, change the other. (3) The single deliberate narrowing vs `value_summary`: a security-sensitive task's image stays builders-only even once shipped — a summary is a sentence its author chose, a screenshot is whatever was on the screen. Nothing is HIDDEN by that; non-public means un-published, not invisible. **Optionality is a contract, not a default** ("where relevant" is the owner's wording): `ship.js --visual` screens the file BEFORE the claim resolves (bad input costs a retyped command, never a claim) and uploads AFTER (a failed picture warns and prints the re-attach command; it never fails a ship that already landed), a task with no visual renders **nothing at all** — no placeholder, so it never reads as unfinished — and `DELETE /tasks/:id/visual` exists because an image is not prose (task 1003102's write-once rule guards a written CLAIM, not the picture beside it). Not an arbitrary-file host: extension allowlist + magic-byte sniff + 4 MB cap + server-minted name, **SVG deliberately excluded** (an executable document served from our own origin), `nosniff`/`inline`/`default-src 'none'; sandbox` on the way out, files under `var/task-visuals/` outside the git tree so a deploy's `git reset --hard` cannot wipe them. Rejected: a per-visual visibility flag (two axes over one fact drift, and the first disagreement leaves a reader guessing which is true); requiring `modules/discord/bug-attachments.js` (ADR 0083 forbids module→module, and the intake genuinely differs — that one DOWNLOADS from a CDN and needs an SSRF allowlist; duplication is re-stated under ADR 0091 §2 and cross-referenced in both headers); full-size images in every feed (the ledger, a profile and the dashboard are scannable lists — they get a thumbnail linking to the file; the task page is where you go to look).](0184-a-task-visual-inherits-its-summary-audience.md) | lifecycle / hall-ui / status-ui |
271
+ | 0184-b | [**A ship must account for itself: `shipTask` requires a land proof or a declared no-artifact** ([task 1002483](https://cloudbongos.com/builders#/task/1002483), goal 1000024). `db.shipTask` is the confirmed→shipped flip and, since [ADR 0120](0120-pay-on-land-and-builder-owned-rebase-gate.md), where the CREDIT is booked — yet its only precondition was `WHERE status = 'confirmed'`, nothing about whether code reached main. The two AUTOMATED paths each proved the land first; the MANUAL route passed nothing, so a task confirmed with no pushed code could be shipped by its claim holder **or by any Metic+ sweeping `/merge-mode`**, booking its `task.shipped` credit. It stayed untracked for two months because its tracking task was closed in June asserting task 1001547 had delivered the gate — it had not, blocker 1000047's own final update says so, and the code agreed with the blocker rather than the closure note: **a hole hidden by a record nobody checked**, which is the same failure the gate is about. Decision: the gate lives at the WRITE, not the route (a route gate leaves the invariant unenforced at the layer that moves the ledger, so the next caller reopens it silently), and `shipTask` is FAIL-CLOSED on a required `landAssurance`. TWO kinds, and the second is why this does not make things worse: `proof` (this task's commit is on main) and **`no_artifact`** — a deliberate DB-only ship, an explicitly supported shape (CLAUDE.md "Direct DB writes are work too") that [task 1002478](https://cloudbongos.com/builders#/task/1002478) documents as a dead end below Archon. A naive proof-only gate would have made 1002478 strictly worse; naming no-artifact as a KIND turns it from an absence the gate must be talked out of into a recorded answer. Both carry a reason, audited post-commit and best-effort (an audit miss must never roll back a landed ship). `ship.js` now declares its own `--db-only`/`--allow-empty` lands, without which this fix would have regressed the very flow CLAUDE.md calls supported. Rejected: (b) gate only the route — leaves the write unguarded; (c) record the residual as tolerable — defensible on blast radius and genuinely close, but it would have been the FOURTH record asserting this was handled with nothing checkable behind it. Does NOT gate confirm: a codeless confirm is still accepted, it simply cannot reach `shipped` or pay.](0184-ship-requires-an-assurance.md) | lifecycle / integrity |
272
+ | 0185 | [**The spark-handoff credit split: 70% developer, 30% spark author** ([task 1002923](https://cloudbongos.com/builders#/task/1002923), BV1.R147, goal 1000062 — completing the chain [task 1002920](https://cloudbongos.com/builders#/task/1002920) mark → [task 1002921](https://cloudbongos.com/builders#/task/1002921) queue → [task 1002922](https://cloudbongos.com/builders#/task/1002922) pickup). [ADR 0172](0172-per-craft-compensation-ideator-credit-lane.md) built the ideator streams and paid `captured_by`, the only person an idea then had. The spark handoff created a second: R146 develops a spark IN PLACE, preserving the author while recording a `developed_by`. **R146 therefore shipped paying NOTHING on purpose** — not an oversight but a refusal to guess, because `awardIdeaFullPassCredit`'s idempotency predicate is `(builder_id, reason)`, so paying the DEVELOPER would not have blocked a later payment to the AUTHOR and one idea would have paid twice; the cost of that caution was that a developed Full Idea received stream 2 **never** unless its author resubmitted. Owner decision (2026-08-22): **70/30 favouring the developer**, on BOTH existing streams, one ratio so there is one rule to explain. The reasoning is behavioural, not sentimental — developing someone else's spark is the scarce act the queue depends on, and at an even split it pays HALF what authoring your own Full Idea pays for the same writing work, so the rational move is always to write your own and the R145 queue stays a list nobody reads. The author keeps 30% plus their share of stream 1 on every task shipped under the idea, forever. `IDEA_DEVELOPER_SHARE = 0.70` is a **policy constant in `credits.js`** beside the pass bar and quick factor (ADR 0172's own consequence, the ADR 0023 precedent): a share of someone's earnings deserves a review and a commit, not a runtime edit. **The structural half is what retires R146's caution:** the economy module now derives the payees itself (`ideaCreditParties` reads `captured_by` + `developed_by`), so the caller can no longer choose a recipient — the develop route hands over an idea and a score, never a payee, and books the split like any other path (fail-open; the development has already committed). Three arithmetic properties, each a real bug if missed: the total is **conserved** (developer rounded, author gets the REMAINDER — rounding both pays 6 out of a 5-credit payout); a **self-developed** idea collapses to ONE payee, because two rows for one builder would silently UNDER-pay under the per-builder idempotency predicate, not double-pay (the comparison normalises via `String()` since `bigint` arrives as a string, so a type mismatch cannot become a phantom payee); and **zero-value rows are dropped**. Ids otherwise pass through unconverted — `Number()`-ing a `bigint` risks precision. `builder_id` in the idempotency predicate, which entered as an index optimisation (the R143/IC2 grader finding), is now **load-bearing for correctness**: two payees share one `reason`, so a reason-only predicate would refuse the second, and a partially-booked split completes on the next call. **Not decided here:** stream 3's split (ships with the Board Room, task 1002999), and whether `origin='develop'` in `ideas_task_lineage` belongs to whoever promotes a DEVELOPED idea into a task — ADR 0172 §5 says the row is "set at promotion and at develop (R146)", but R146 creates no task, so that origin stays deliberately unwired rather than invented; it changes who stream 1 pays.](0185-spark-handoff-credit-split.md) | economy / ideator lane |
273
+ | 0186 | [**Per-project idea templates: the seam is already built; the economics are the open problem** ([task 1002932](https://cloudbongos.com/builders#/task/1002932), BV1.R156, goal 1000062 — a SPIKE, output is this ADR plus a task list, no implementation). The literal question — how does a founder define their own idea shape without a schema change or forking the scorer — is **already answered by shipped work**: R141 gave every template function an optional `registry` defaulting to the built-in one, so validation and scoring both read the declaration as data; `ideas_grades.template_key` is per-row with a format CHECK whose own comment says a project key "stays addressable"; and `provisioning_instances.settings jsonb` is an existing per-project home. **No schema change, no fork.** Decision A: a **`project-settings` kernel port**, sibling of `builder-settings`, read via `resolveOptional` so a vanilla instance with no provisioning module degrades to the built-in registry; `base-v1` stays the default and the fallback for an unresolvable key. Rejected a new `ideas_templates` table (second home for data that has one) and a config file (per-instance, but one instance hosts many projects). **Decision B is the finding that matters.** Completeness is normalised 0–100 WITHIN its template (mean of per-field ratios), while karma reads it against an absolute bar (`IDEA_FULL_PASS_BAR = 60`, `IDEA_FULL_PASS_CREDIT = 5`) — so template choice becomes a credit-minting lever: `base-v1` needs ~200 characters for 5 credits, a 1-field template with `substanceMin` 10 needs ~10, a ~20× arbitrage handed to exactly the projects this ADR empowers. Shipping A without B would make the feature a way to print credits. The fix separates the two questions one number is answering: **completeness stays relative** (it is the right signal for triage ordering and the hall meter) and **karma stops reading it**, switching to an absolute substance total the scorer already computes per field and currently discards after averaging. An interim guard (≥3 required fields, ≥120 total `substanceMin`) bounds the arbitrage without fixing it. Decision C: an optional per-field `weight` (default 1) turning the unweighted mean into a weighted one — today's behaviour is the all-weights-1 case, so it is a data addition and one arithmetic line. Decision D: the five base fields are **not** mandatory (forcing them would make "define your own" cosmetic), replaced by two constraints — a template must declare ≥1 required field, and a stored template is validated at WRITE time so a malformed one is refused there rather than discovered inside the capture path, which must never throw. **Boundary note:** goal 1000062's wall is `[ideas, hall-ui]`, so the port task (provisioning + kernel) and the karma task (economy) cannot be claimed under it.](0186-per-project-idea-templates.md) | ideas / templates |
274
+ | 0187-a | [**Collab's four carried-forward questions, answered** ([task 1003170](https://cloudbongos.com/builders#/task/1003170), goal 1000075). The goal deliberately carried four open questions under the owner's instruction to "answer them in the goal, do not let them be decided by accident" — each being the kind an implementation settles silently by picking whichever branch was easier to code. **(1) Closeness is a WEIGHTED BLEND** of co-claims, co-build sessions, goals shipped together and (per 4a) comms/mentions — not one signal. Two parts of the decision, not follow-ups: the weights are DATA rather than a buried constant, and "why am I close to them" must be answerable on the surface that shows it, because a number that ranks colleagues and cannot say what it is made of becomes unfair with nothing to point at. Consent is unchanged — ADR 0171 fail-closed, hide enforced in the READ path, a hidden builder is not a node in anyone's graph. **(2) Passing RECOMMENDS, never transfers**: the recipient still claims it, claim/release stays the only path to holding work, declining is free, and the ledger records who recommended it and why. Transfer-on-pass was rejected because it would stop the claim ledger meaning "this person chose this work". **(3) Co-build is a GOAL-level fact, not a shared claim — and the reward question was WRONG-PREMISED.** Owner, verbatim: nobody will "co run the task on their claims"; collaboration happens at the goal. So each builder holds and is paid for their own task exactly as today and there is NO split to design — recorded because it is a deletion: a reward-splitting mechanism would otherwise have been designed, reviewed and shipped for a situation that does not occur. **(4) Mingle pairs CROSS-CRAFT** (engineer with artist or ideator), same-craft rejected as duplicating the craft-matched Collab path; the two-switch consent rule stands, since one switch is not consent for a real-world social commitment. **4a adds scope:** Discord mirrors INWARD so comms and mentions are visible in-hall — which puts attacker-authorable text on an in-hall surface, so the trust posture for mirrored content is this goal's to answer rather than assume (Discord is outbound-only and outside the boundary today, ADR 0032/0033). It is BLOCKED on task 1002946 (the Agora, goal 1000064, unstarted pending team buy-in), and this goal must not reimplement the Agora to get around it. No criterion was added for it deliberately — one blocked on another goal's unstarted work would make 1000075 unachievable for reasons unrelated to collaboration.](0187-collab-four-decisions.md) | collab / social hall |
275
+ | 0187-b | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](0187-oauth-handshake-cookie-lifetime.md) | auth / security posture |
276
+ | 0188 | [**The weekly patrol runs on CI, and a declared cadence must have something that fires it** ([task 1003209](https://cloudbongos.com/builders#/task/1003209), goal 1000079). Goal 1000076 shipped a weekly red-team patrol, a routine describing how to run it, a recipe, and a manifest row — and nothing ever ran it; the follow-up audit §7 named all three halves (no committed scheduler, no cadence for a scheduler to read, and a manifest green anyway because its wiring predicate was satisfied by the SKILL.md mentioning its own scanner). The symmetric fix — an in-process timer beside `peer-vote-tally` in `routine-timers.js` — is **wrong on correctness, not cost**: all three patrol checks read things only the REPO has (full git history, `.github/workflows/`, an auditable lockfile), and a deployed instance runs a VENDORED core out of `node_modules/@bongos/core` that has none of them, so the timer would have scanned nothing and reported clean — worse than unscheduled. The runner is therefore CI: a weekly Actions lane with `fetch-depth: 0` and a pinned, checksum-verified gitleaks (the missing binary the audit listed separately), invoking `run-routine.js redteam-weekly` so the SKILL.md's steps stay the single source of truth. `cadence:` becomes a real frontmatter field — declarative only, scheduler-agnostic. The load-bearing piece is `routine-schedule.js` (fitness Check 25), whose predicate is **relational, not existential**: it classifies the cron into the period it actually realizes and fails when that disagrees with the declared cadence — the one rot mode no substring or existence check can EVER see, since editing weekly→monthly leaves every "is it wired?" predicate green. The manifest row was re-pointed to match ("wired" now means the SCHEDULER names the routine). Finally the exit code says **a finding was LOST, not that a finding exists**: findings are advisory (ADR 0162), so filing three is a SUCCESS and red is reserved for findings that cannot be recorded — failing on findings would have gone red on run one and stayed there (the unrotated staging credential in `092e473` is a standing history hit), and an always-red weekly job is one nobody reads. Filing is credential-gated but OPTIONAL — with no session secret the patrol still runs and writes its findings to the job summary, which matters because this goal's C1 is about eliminating long-lived tokens, not adding one. Rejected: the in-process timer; teaching `routine-timers.js` generic cadence discovery (a scheduler built for nobody — its only candidate user cannot run there); failing whenever findings exist; requiring the secret; a deeper manifest predicate instead of a new check.](0188-patrol-runs-on-ci-not-the-server.md) | security / enforcement durability |
277
+ | 0189 | [**A frozen baseline must justify itself: declared floors and the freeze-forever detector** ([task 1003210](https://cloudbongos.com/builders#/task/1003210), goal 1000079). The shrink-only ratchet makes a quality metric unable to REGRESS and, by the same design, unable to sit still noticeably: the follow-up audit measured the whole file at "every metric equal to baseline, 0 reduced" and the ratchet reported a clean PASS, because one green covers both actively-paying-down and nobody-has-looked-in-a-year. The ratchet has no concept of time; this adds it. **Age comes from git, never a stored `since:` field** — a timestamp beside each metric would be faster and hermetic and would also be the field that silently stops being updated, leaving old debt permanently fresh-looking, which is worse than no check because it produces confident green; an unavailable history reports UNKNOWN, not fresh (the `--require-roster` doctrine). A metric's age is the date of the OLDEST commit still carrying its current value, so a baseline recorded once and never revisited dates to its introduction — exactly the case being hunted. **The half that matters is the declared floor.** The naive "flag anything unmoved for N weeks" fires forever on metrics that are FINISHED (`require_cycle_count`'s four deliberate lazy requires; the ~28 duplicate windows ADR 0091 §2 forces because a module may not import core), and a check that cries wolf weekly is one everyone learns to skip — the same alert-fatigue failure ADR 0188 §2.4 avoids. So a metric may declare `floors: {name: {value, why}}` and a metric AT its floor is done, not stale; the `why` is mandatory and length-checked, because a floor exempts a metric from the clock permanently and must therefore be a written claim someone can disagree with — **silence was the problem**, and a floor without a reason is just a mute button. A floor exempts only DOWN TO the floor, so `duplicate_window_count`'s 59-vs-28 keeps its ~31 reducible windows on the clock. **Two severities, split by whose fault it is:** declaration rot (a floor naming a dead metric, a floor with no reason, a floor the metric already dropped below) is hermetic and caused by the diff at hand, so it HARD-FAILS the merge gate; staleness is nobody's diff and would punish whoever pushed next, so it warns in fitness and is FILED weekly by the patrol — the "repeatable, scheduled version" the audit asked for. Also fixed a latent eater: `--update-baselines` wrote a fixed object literal, so one refresh destroyed the accumulated `_comment` and (once floors landed) every floor, deleting a finished metric's justification from the repo while turning it into permanent weekly noise. Nothing is flagged today — the file is six days old — so the detector is verified by shrinking the window, not by waiting. Rejected: a stored `since:`; no exemptions; a floor with no required reason; hard-failing on staleness; patrol-only placement.](0189-baseline-floors-and-the-freeze-detector.md) | quality ratchets / debt governance |
278
+ | 0190 | [**A changed project setting reaches a running instance by an in-place env patch and a restart — never a template re-write** ([task 1003140](https://cloudbongos.com/builders#/task/1003140), goal 1000058). Task 1003139 made a NEW instance read `platform_visibility` / `joinability` from the web.env the runner writes; a RUNNING instance loads its branding pack once, memoized and deep-frozen for the process lifetime, so a changed value could not reach it at all — the settings were write-only state on the platform. Decision: `PATCH /provisioning/instances/:id/settings` writes the platform row first, then for an ACTIVE instance on a shape the runner can reach enqueues a `settings-apply` intent (migration provisioning_010); the control-plane runner reads the instance's web.env as it stands, upserts ONLY the two settings lines (`upsertEnvVars`), and RESTARTS the service. The in-place patch is the load-bearing choice: the natural move — re-running `webEnvBody` as domain-attach does — regenerates the file from the row, and the row does not know the OAuth client secret or the App private key that `placeManifestCreds` placed later, so a template re-write silently blanks sign-in on the very instance the owner just touched. `SETTINGS_APPLY_FROM = {active}` because a pre-active instance gets the same values at standup and a torn-down one has nothing to restart (the provision intent cannot stand in — it excludes `active`, task 1002691); `SETTINGS_APPLY_SHAPES = {co-tenant, standalone}` because a dedicated droplet has no post-hoc exec path, and a push that cannot land is refused at the route AND the runner rather than reported. The response is a receipt, not a claim: it says the restart is coming and that the instance keeps its current setting until then, and the read-back lives on the instance's own public `GET /instance` manifest, whose `brand.project` now carries the resolved policy (`clientBranding` allow-list widened deliberately). Rejected: the template re-write; re-provisioning as the push; skipping the restart (impossible while the pack is memoized); claiming the new state on PATCH return.](0190-settings-apply-in-place-env-patch.md) | provisioning / settings channel |
279
+
280
+ | 0191 | [**Majority, and an author who may vote but never decide alone** ([task 1003273](https://cloudbongos.com/builders#/task/1003273); diagnosis and companion in [task 1003271](https://cloudbongos.com/builders#/task/1003271)). Supersedes ADR 0175 §3's pass-rule set and §§7/11's flat anti-self rule. The owner reported voting as broken; nothing was broken. Nine items sat open under `first_ratifier` with four archons active — any one of three non-authors could have ratified any of them in a click — but every item was authored by the one person looking at them, and ADR 0175's anti-self rule was enforced by rendering NO BALLOT, so from the owner's seat the feature did not exist. The lesson: a rule enforced by removing the control is indistinguishable from a broken feature. Two decisions. (1) `majority` joins PASS_RULES: strictly more than half of the resolved MEMBERSHIP must vote yes — of the membership, not of the votes cast, so low turnout cannot quietly pass things; a tie is not a majority. The three rules are NOT a ladder of strictness, they differ on WHAT SILENCE MEANS — `first_ratifier` and `consent` pass an unvoted item, `majority` returns it — which is the axis that matters and now the comment in config.js. (2) An author MAY vote and may NEVER DECIDE ALONE: the rule moved from refusing the cast to the two places something is actually decided — `shouldCloseAfterVote` needs a non-author vote under every rule, and the majority tally needs a non-author YES. Their yes still counts toward the numerator; their `no` is excluded from the objection test (else silence would be an author's safest act); a self-cast earns no karma. The ADR 0175 §7 amendment carve-out survives as the author rule's SCOPE, not an exception: `authorRuleApplies` is full_idea-only, so a sole Archon still ratifies their own amendment. Also records why the CLOCK is a consequence of majority and not an independent fix — `tallyOutcome` passes on ZERO votes, so adding a deadline under `first_ratifier` would have closed all nine items as ratified-unread, paying karma and credits on ideas nobody read; `BOARD_DEFAULTS.window_minutes` therefore stays `null` in the neutral core. States the cost plainly: three affirmative votes are needed on a four-member board, and a solo instance still cannot ratify its own ideas. Discord stays outbound-only (ADR 0175 §8 re-affirmed).](0191-majority-and-the-author-who-may-not-decide-alone.md) | government / board room |
281
+ | 0192-a | [**A gated project refuses strangers at the app's front door, and the hub reads what the project runs from the project itself** ([task 1003043](https://cloudbongos.com/builders#/task/1003043), goal 1000058). Task 1003040 gave every project a `platform_visibility` setting and 1003139/1003140 carried it into the instance's `branding().project.platformVisibility` — resolved, enforced by nothing. Decision: ONE app-level middleware (`src/bongos/platform-visibility-gate.js`), mounted beside the pre-launch gate ahead of every human and public-read surface — the landing, the hub pages, the status host, the hall shell, `/public/*` and every module's `rank: public` route — rather than per-surface wiring, which misses the next surface nobody told it about. "Member" is a live, non-deactivated session on this instance, the same `resolveSession` lookup `requireBuilder` uses, per request, no cache; a lookup that throws fails CLOSED. The exempt list is the door, the manifest and the probes and nothing wider: `/api/*/auth/*` (a door that cannot be opened locks the members out too), `/api/*/instance` (what a stranger reads to learn how to sign in AND what the runner reads after a settings restart to learn whether the value was consumed — including the push that turns this gate on), `/healthz`, `/version`, `/metrics`; the pre-launch gate's blanket `/api/` exemption is deliberately not copied. A refused page gets a self-contained 401 door (no stylesheet, no branding bundle, no script) with one control — Sign in, returning (relatively) to the page knocked on; a refused API call gets `res.fail('unauthenticated', 401)`, exactly requireBuilder's envelope, which is why it mounts after `attachFail`. A member's answer leaves marked `private, no-store` whatever the route said — the public read API sends `public, max-age`, and a CDN edge would otherwise serve a member's copy to the next stranger. The exempt list covers every API prefix (the versioned contract too), `/downloads/*` and Caddy's TLS ask; a request's session is resolved once (`resolveSession` memoizes per request). The hub's manage page shows the CHOICE from the platform's row and what the project RUNS from the project's own public manifest (`brand.project`), and never confuses the two: `push.message` is rendered verbatim, `saved` moves only the radio, the "runs" line is written only from the manifest and says "doesn't report this setting yet" for a core that predates the channel. Rejected: per-surface wiring; a 302 into OAuth for a refused page (sends assets and crawlers to GitHub); copying the `/api/` exemption; painting the radio from the manifest or the runs line from `saved`; reusing the pre-launch gate.](0192-platform-visibility-member-door.md) | instance / project visibility |
282
+ | 0192-b | [**A work category orients a human and authorises nothing** ([task 1003275](https://cloudbongos.com/builders#/task/1003275), goal 1000072, criterion `legibility-categories`). A goal gains a human CATEGORY from an owner-named set of eight (Builders Hall, Context Management, Government (Decision Making), Modules, Security, Economics, Legal, User/ABI Experience) so a person scanning the board can tell at a glance which goals are about money, which about the walls, which about the hall — today each goal's own words say so, but only if you read all thirty-eight. The DECISION is the constraint, not the feature: the owner said twice that this organisation has no hierarchical purpose and no effect on the agentic management of the work, and the system ALREADY has a load-bearing hierarchy (ADR 0086 — the scope_modules wall, rank floors, goal membership). The goal's own prose predicts the failure: a second thing that looks like a hierarchy gets mistaken for one within a release, and the mistake arrives as ONE plausible line in a gate ('skip Legal goals in the feed'), after which work is refused because of a label nobody meant as a permission. So: no authority, claim-eligibility, feed or dependency surface may read a category — enforced by the `scripts/gds/category-advisory-guard.js` fitness check, which scans every decision surface with comments stripped (a gate may CITE the rule, never branch on it) AND asserts the feature still exists, so a renamed-away subject fails loudly instead of passing vacuously (the ADR 0022 assurance-of-assurance finding). Uncategorised is LEGAL — nullable, no default, required by no write — because the criterion is explicit that Claude failing to place a goal is a designed prompt, not an error. A task stores no category and INHERITS its goal's through `tasks.goal_id`; a stored copy would be a second answer to one question. The set is a table, not an enum (runtime-extensible without a migration per addition) and retiring is `active = false`, never a DELETE, so a goal keeps rendering a label someone tidied away. The three route-rank pins are a tripwire: if one ever needs a WIDER gate than its neighbours, the label has grown into a permission. Deliberately NOT built: a per-category page or filtered navigation view — a chip is a label, a drill-down destination is a tier. Making a category gate anything is an owner decision and needs its own ADR first.](0192-a-category-orients-and-authorises-nothing.md) | GDS / goals |
283
+ | 0193 | [**Pause task-scoped box slices: a box serves its rank scope (the full repo for Metic+)** ([task 1003285](https://cloudbongos.com/builders#/task/1003285), owner decision from the 2026-08-25 dev-box efficiency review). Pauses, does not repeal, ADR 0148 / goal 1000051: `routes/box.js` skips the lifecycle-port `scopeKeys` resolution unless `BOX_TASK_SCOPE=on`, so `decideSourceAccess` takes its documented rank fallback — full repo for Metic+, starter surface below. WHY: the slice was defense-in-depth git cannot hard-enforce (ADR 0031 §6's honest limit) while its costs were real on the live fleet — sessions on a box that cannot see a module (or its nested CLAUDE.md) misdiagnose themselves as stuck, the scope machinery kept failing in the field (task 1002729's base-slice collapse), and every live box user is Metic+ anyway, so the slice narrowed trusted builders while newcomers were already gated by rank. UNCHANGED: live-rank allow/deny per request (ADR 0016), the provision floor and demotion clawback, the read-only per-request credential (ADR 0055 — push still 403s, re-verified on hardware in task 1003253), and sub-Metic starter boxes. `GET /box/scope` now reports `task_scoping_paused` so the hall/app can say why `task_scope` is false; the app task-picker's "scoped to `<modules>` and nothing else" copy overstates while paused (cosmetic). Revert = set the env flag; make-permanent = delete the resolution + a follow-up ADR.](0193-pause-task-scoped-box-slices.md) | dev-box |
284
+ | 0194 | [**A project's membership door is one decision, answered the same way everywhere a stranger meets it** ([task 1003044](https://cloudbongos.com/builders#/task/1003044), goal 1000058). The owner's 2026-08-16 joinability setting — open / apply / invite_only — was stored (1003040), pushed (1003139/1003140) and resolved into `branding().project.joinability`, enforced by nothing. Decision: ONE reader, `joinabilityMode()` in `auth-admission.js`, and `openEnrollmentEnabled()` becomes `mode === 'open'`, so the four sign-in call sites, `webAdmissionStatus` and the manifest's `admission.policy` follow untouched (SR-19: one gate, no drift). The legacy `OPEN_XENOS_ENROLLMENT` flag still opens an APPLY project — a self-hosted instance that set it keeps its door across the upgrade — but never reopens an explicit invite_only; an unreadable pack or a value this core cannot read fails CLOSED (invite_only), the ADR 0192 rule. The public `POST /access-requests` answers the door FIRST — before validation, before any lookup — `403 invite_only` / `409 open_enrollment` with plain sentences: no query, nothing revealed about who exists, and DB-free enough for the real booted app to prove. An Archon's invite is the row the gate already reads: `POST /access-requests/invite` writes `access_requests` at 'invited' (resolving a pending one in place), so sign-in consults nothing new — surfaced as "Invite by GitHub username" on the Watch page. The door is SAID wherever a stranger meets it, in the project's words: the hall landing renders one door per mode from the injected pack, the web 403 names the invite-only door and the way in, the CLI stops instead of filing, the hub's join relay names the door in the hub's own words (a project is trusted to build, not to write prose into the platform's UI) instead of painting a refusal as "requested" or "unreachable". `admission.policy` stays binary (pinned; agrees for free); `joinabilityMode` joins the doorway — a human MINOR by ADR 0161 §2, which the publish lane's LIVE test refused (HEAD's version must be a CI carrier's), so it rides the next CI patch and the contradiction is filed as an idea. The hub's `#mJoinability` clones `#mVisibility` rather than a factory — a third setting is where a factory pays. Rejected: folding the flag into `branding()` (memoized vs per-call; the manifest must stay the pushed value); a 200 for the open door (an older CLI would wait thirty minutes for an approval); re-deriving the mode inside the module; a second invite table.](0194-joinability-one-door-decision.md) | instance / project joinability |
285
+ | 0195 | [**ADR numbers are checked like migration numbers, and the twenty existing collisions are grandfathered by name** ([task 1003164](https://cloudbongos.com/builders#/task/1003164)). Two ADRs could share a number and nothing flagged it — git sees no path collision, the index auto-merges into two rows, and every gate passes; it was found by a human reading `ls docs/adr/`, after a task note had already cited a number the collision made point at an unrelated document. Migrations had this guard already (`stemCollisions()` hard-fails CI, because a re-used migration number silently skips a migration); ADRs had only a best-effort allocator at claim time and nothing afterwards. `scripts/gds/adr-namespace.js` is the sibling module, wired into fitness as Check 31 and so into the required `unit` gate. Building it found twenty live collisions, not one, plus fourteen index defects of the same merge class — two ADRs listed twice and twelve with no row at all, ten of them the second file of a colliding pair, which is how a collision hides. The twenty pairs are grandfathered by EXACT FILENAME rather than renumbered: renumbering breaks every citation to them, which is the harm the check exists to prevent, and set-equality keeps the exemption as narrow as the history that earned it — a third file on a legacy number, or a rename of either half, still fails, and the list may only shrink. The index half is keyed on LINK TARGET, not number (a number rule cannot express a grandfathered pair): every file has exactly one row, every row points at a file that exists. Row ordering warns and never blocks — ten legacy rows are out of order and no PR author caused that (the ADR 0189 split). Rejected: renumbering; grandfathering by number; a numeric cutoff like LEGACY_BARE_CORE_MAX (a new ADR takes a number above the max, so it would catch nothing); fixing it in the claim-time allocator (which already tries, against a stale tree — that is how the collision happened).](0195-adr-numbers-are-checked-like-migration-numbers.md) | docs / numbering discipline |
286
+ | 0196 | [**The chrome world becomes the token contract; the landing gets its own world sheet beside the hub's** ([task 1003298](https://cloudbongos.com/builders#/task/1003298), goal 1000080). The owner picked the chrome-blobs world (1003296) and the v3 composition with light rebuilt to the dark standard (1003297); this build makes it the platform's token contract. Decisions: (1) the neutral pack moves to the world's LIGHT values (pale grey paper, black ink, salmon accent, Manrope + JetBrains Mono) and design-tokens mirrors it; --accent-deep is #a3481a rather than the mock's #d96a35 because the hub and the gate pages paint TEXT with it and #a3481a is the lightest burnt orange clearing 4.5:1 on all three light grounds; the rgba hairlines are flattened to hex so hex-maths consumers survive. (2) theme.ui.backdrop becomes var(--bg): a literal there paints the light ground under the dark page in both modes, since the body rule outranks every sheet. (3) The landing gets its own world sheet public-landing/assets/world.css (contract tier = pack, derived tier both modes, root-absolute plate token PAIRS, the kill switch; NO composition switches) and cosmos.css stays the hub's component sheet until A4 — folding the two tiers would have collided (--line is a width in the world and a colour in the hub; the world's warm --ok/--warn/--err would repaint the hub's fixed green/amber/red chips). cosmos.css's dark tier moves to the world's black and its derived tier is retuned for the salmon accent (dark ink on the fill, never white). (4) Plates ship under public-landing/assets/world/ with provenance embedded: black plates verbatim JPEG, cutouts as WebP with alpha at 1600px (85–170 KB vs 1.1–2.4 MB PNG), sidecars; plate URLs in custom properties escape the ?v= stamp (edge cache 4h), accepted. (5) Live figures are BAKED inside apex-only sentinels (real, dated; a child renders empty until its own feed answers) and refreshed at render time from the five public feeds with fixed definitions: tasks shipped = the sum over every version row, builders = leaderboard entries holding credits, the ledger line = the first building version; live titles have em dashes normalised to a comma and a trailing parenthetical dropped. Every pack-reading fallback site (the claude-design adapter, the status explore page, provisioning onboarding, dev-box brand artifacts + tray icons, the two gate pages) is moved in the same task so no page keeps a private palette. Rejected: one merged token tier; a landing-private palette; PNG cutouts; a script-only ledger; #d96a35 as accent-deep.](0196-chrome-world-token-contract-and-the-landing-sheets.md) | apex / design system |
287
+ | 0197 | [**The ui-design module: design is a platform capability; the instance owns its world** ([task 1003324](https://cloudbongos.com/builders#/task/1003324), goal 1000080 — the first of the five ui-design tasks scoped with the owner 2026-08-28). ADR 0081 put the tool-agnostic design layer in core as Cloud Bongos's machinery; the whole-frontend pass then built the chrome world on it and left the three things a builder needs — the contract and its tooling, a way to look before shipping, the taste and the looks — scattered across core, scratchpads and personal skill installs. The owner's reframing: this is NOT the front door, it is a PLATFORM capability every builder on every instance gets, and Cloud Bongos's front door is its first customer, not its owner. Decisions: (1) `modules/ui-design/` is a default-on core-domain module beside grading/economy/memory — it ships METHOD, never a world; (2) ADR 0081's layer moves in (`src/ui/adapters/` → `adapters/`, the three `scripts/gds/` CLIs → `scripts/`, `docs/design-contract.md` → `docs/`) while `config/design-tokens.*`, `design-sources.json` and `DESIGN.md` STAY out as the host boundary — the ADR 0062 §7 split written for design; (3) declaration-only: three contributed skills, no routes/port/migrations, so `GET /modules` lists it and disabling it withdraws the skills and touches no served surface — and NO `ui` discipline yet, because the hall settings page and newcomer-restock don't know it; (4) the `/design` playbook is finally written — `discipline-modes.json` had routed `ui` tasks to a skill that did not exist since task 1267; (5) the module CLAUDE.md draws the platform FLOORS (the Fifteen Rule, the dark twins, no page `:root`, the 24px floor, AA in both modes, the Kill Switch) against the instance's WORLD (its packs + DESIGN.md), tightenable and never loosenable; (6) the CLIs keep thin `scripts/gds/` shims — the form `claude-materialize` rewrites to `bongos exec` on a packaged core, the form `doc-cli-guard` resolves, and the carve precedent — while inside the module they walk up from `__dirname` instead of the computed `require()` into `src/instance-config.js` the static boundary scanner could not see; (7) three reserved homes, each README'd for the task that fills it, the vendoring policy pointed at the skills task's ADR; (8) `public-landing` keeps its own key. Rejected: leaving the layer in core; moving the token files in; the module owning the front door; module-hosted CLIs with no shims; contributing the `ui` discipline now.](0197-the-ui-design-module.md) | modules / design system |
288
+ | 0199 | [**An unreachable ACTIVE project gets a restart of its own, not a re-provision — and the restart never claims the verdict its probe cannot reach** ([task 1003238](https://cloudbongos.com/builders#/task/1003238), goal 1000058). The liveness overlay (task 1002696) could report a project unreachable while its status stayed `active`, and that reading came with no action: `PROVISION_ENQUEUE_FROM` deliberately excludes `active` (task 1002691 — a blind re-enqueue left a stale provision intent blocking the owner’s own teardown), so the card’s existing *Try again* / *Bring it back* doors 409 on an active project by construction, and `settings-apply` — the only leg that runs from `active` — refuses unless a stored value actually changed, which is exactly the case a stuck project does not have. Decisions: (1) a `restart` intent (migration `provisioning_011`, `INTENT_ACTIONS` in lockstep) behind `RESTART_FROM = {active}` + `RESTART_SHAPES = {co-tenant, standalone}`; the web tier still only ENQUEUES (ADR 0111 §2). (2) `restartInstance` is `settingsApplyInstance` MINUS the env patch — one `systemctl restart`, then the same patient healthz budget; it reads and writes no file, so ADR 0190’s in-place-patch hazard cannot arise. (3) The `probeIsVerdict` narrowness carries over and is load-bearing: the liveness sweep probes the PUBLIC `https://<domain>/healthz` while the runner verifies LOOPBACK, so a project can answer here while DNS/Caddy/TLS is what is broken — no up-verdict is written for a domained instance, and the event says “answering on the box; whether its public address answers is for the next sweep to say” rather than a sentence the still-`unreachable` card would contradict. (4) A project that stays down is `ok:true` — the restart RAN; re-pending it to the attempt cap would restart a broken project four more times and end by stamping an `error_note` over the owner’s own action. Only a THROW returns `ok:false`, and it returns rather than throws because the drain loop’s catch would relabel a merely-unreachable instance `error` (ADR 0190 §2). (5) `enqueueRestart` is idempotent against its own kind in EITHER open state — where it differs from the settings push, which must refuse to ride a RUNNING sibling that may have read the row already; a restart carries no payload, so one already running IS the ask. (6) A pending restart joins the teardown’s supersede list, more sharply than its siblings since the owner reaches for Restart exactly when they might next give up and delete. (7) The door shows only at `active && last_verified_up === false && shape !== dedicated && !open_intent`, mirrored on card and manage page; `null` (never probed) is NOT `false` and raises no door. (8) Every refusal names the action that IS available there, the state gate is checked before the shape gate, and no branch promises a duration (F16). The `INTENT_ACTIONS`↔migration set-equality assertion moved to the NEWEST migration’s test — pinned to 010 it would fail for every future action. Rejected: widening `PROVISION_ENQUEUE_FROM`; re-sending unchanged settings to borrow their restart; reporting a loopback pass as “answering again”; retrying a restart that left the project down; a Restart door on every active card.](0199-restart-an-unreachable-active-project.md) | provisioning / project lifecycle |
289
+ | 0198 | [**Third-party skill vendoring: a scan verdict the session cannot override, a licence allowlist, a provenance sidecar, one preamble block, and no auto-update** ([task 1003321](https://cloudbongos.com/builders#/task/1003321), goal 1000080 — the second of the five ui-design tasks). Twelve design skills installed on one builder's machine were to become the `ui-design` module's vendored set. Both origins reduced to DANGEROUS under `/scan-before-install` at the default tier: `Leonxlnx/taste-skill` on one floor critical (an injection-phrase hit the panel identifies as a filename placeholder) with a caution panel; `pbakaus/impeccable` on three floor criticals and a panel that upheld shipped PostToolUse/Stop hooks written into five harness configs, a child agent spawned with permissions bypassed from a loopback HTTP route, a directive telling the model to disregard its harness system prompt, a page-embedded live-server token and an unverified remote bundle download. DECISION: the scan verdict is the gate and a session cannot override it — `dangerous` lands a rebuild spec (a GDS task), never a copy, and vendoring one anyway is an owner ADR plus a test amendment; a licence allowlist (Apache-2.0, MIT, BSD-2/3, CC-BY-4.0); a `PROVENANCE.md` sidecar with the full pinned sha; ONE preamble block (`skills/PREAMBLE.md`, byte-identical, fenced by two markers so an upstream refresh is a three-way merge) that reads the instance's world before any taste rule; `skills/vendor/<name>/` as the home because `fitness-ratchets.js` already exempts `vendor/` from the first-party budgets; the manifest stays the single declaration and `claude-materialize.js` step 1b lands a declared module skill — sidecar included — in every instance's `.claude/skills/` and withdraws it with the module; no auto-update; the playwright plugin a documented prerequisite. The roster ships EMPTY and that is the policy working: the machinery is proven by fixture (`tests/ui_design_skills.mjs`), the twelve are named in `/design` with their verdicts, two rebuild-spec tasks hold the documented functionality. Rejected: treating the placeholder hit as a false positive; scanning a fork or subtree; vendoring impeccable with the hooks stripped; `.claude/skills/` as the home; a second roster file.](0198-third-party-skill-vendoring-policy.md) | modules / design system / security |
290
+ | 0200 | [**A mistyped bring-your-own domain can be corrected — the address it leaves is released to a blackhole, and the attach leg stops regenerating web.env from the row** ([task 1003237](https://cloudbongos.com/builders#/task/1003237), goal 1000058). `POST /provisioning/instances/:id/domain` refused a second address with a deliberate `409 domain_already_set` ("A genuine BYO address stays un-re-pointable, as before") while direction §8 said "attach/change stays for custom domains" — so a founder who mistyped one character in the create wizard was permanently addressed at a hostname they did not own, with no path back but teardown. Decisions: (1) a BYO address re-points under the guards the FIRST attach already applied — `isReservedHostname`, the task-1002704 in-apex squat rule, `getInstanceByDomain` uniqueness, `DOMAIN_ATTACH_FROM` (which already admitted `active`) — none added, none weakened; the platform-assigned `<slug>.<apex>` is un-re-pointable because the squat rule says so. (2) The address it leaves is RELEASED: `provisioning_012` adds `released_domains text[]`, written in the SAME statement as the new domain (a crash between the two would leave a hostname aimed at platform infra with nothing in the row to say so), an ARRAY because a founder can mistype twice before the runner drains. Only a BYO address is queued — the assigned `<slug>.<apex>` is the platform’s own name and the revive path re-uses it (ADR 0181 §2). (3) Release is a REPOINT to `192.0.2.1` (RFC 5737 TEST-NET-1), not a delete — the box lane’s idea-309 shape: a delete opens an NXDOMAIN window resolvers negative-cache for the zone’s SOA neg-TTL (~30 min, not editable), and an UPSERT needs only the contract every operator `BOX_DNS_HOOK` already implements. The Caddy vhost and the tls-check allow-list release THEMSELVES (the snippet is per-slug and carries the current domain; tls-check answers from the row). (4) A failed release is `ok:true` + `releaseFailed`, and KEEPS the queue so the next attach retries — a release that failed must never fail a re-point that already landed. (5) `domainAttachInstance` now patches web.env IN PLACE via `upsertEnvVars`/`originEnvVarsFor`, closing the ADR 0190 §2 hazard (idea 1000842) on this path: the template re-write regenerated the file FROM THE ROW, blanking the OAuth creds `placeManifestCreds` placed, the App private key and the federation block — masked for a FIRST attach by wizard order, and NOT masked for a re-point, which runs on a project the founder is already signed in to. Dry-run plans; an unreadable/empty file is refused, never patched. (6) A CHANGED domain clears `last_verified_{at,up}` — the overlay is a verdict on an ADDRESS (task 1002696), so carrying it would let the card claim the new name "last answered 2 minutes ago" when only the old one was ever probed (F16). (7) The card gains a third door — *Use a different domain*, pre-filled so a typo is edited not retyped — and says what it costs where the button is. Rejected: amending direction §8 to call the address fixed; deleting the old record; sequencing behind task 1002897; a `previous_domain` scalar (drops the middle address).](0200-re-point-a-bring-your-own-domain.md) | provisioning / project lifecycle |
291
+ | 0201 | [**An application is the project's own `access_requests` row, vouched for by a hub-signed assertion carried in a HEADER; the record it carries is resolved live and never stored** ([task 1002283](https://cloudbongos.com/builders#/task/1002283), goal 1000045 — public builder profiles, C3/C4). The join relay (`POST /my-projects/join`) already posts `{github_login, display_name, note}` to the target's PUBLIC, unauthenticated `POST /access-requests` — presenting nothing the applicant could not have typed into the project's own landing page. Attaching a cross-project record to a login nobody vouched for turns the owner's queue into an impersonation surface: file a request naming a well-known builder, the owner reads that builder's track record beside it and approves, and whoever holds that GitHub account signs in. So the record is the thing that makes the unauthenticated write dangerous, and C4 cannot be built without closing it. Decisions: **D1** the application stays an `access_requests` row on the target — `status='invited'` IS what `hasInvitedAccessRequest` reads, so a parallel record would need a second branch in `firstTimeAdmissionBlocked`, the one decision that exists as one function to prevent drift; the migration adds only `applicant_github_id text` (core's own type) + `vouched_at`, the non-null id IS the vouch mark, and the console wave's `kind`/`created_by` stay a separate additive migration; the hub keeps a STATUSLESS echo `(github_id, client_id, applied_at)` so the applicant's own cards can say "you applied on <date>" without polling every project — it may never claim what the project decided (an approval becomes visible the way membership always does, via the check-in), and it ages out at 30 days; re-applying answers `requested`, not 409. **D2** the hub signs an audience-scoped `purpose: 'application'` assertion with the existing `mintHubAssertion`, verified OFFLINE by the existing `verifyHubAssertion` against the already-provisioned `<PREFIX>_HUB_PUBLIC_KEY` — no new secret, no apply-time hub call — and it travels in `<redacted>` because `validateOrRespond` is STRICT by default: a body field would 400 on every older pin, and `joinRelayOutcome` reads a 400 as "could not be reached", breaking applications to most projects for good (the CLI's device exchange carries its assertion in the body because that route was new; this one is not). A missing or invalid assertion writes the row UNVOUCHED, never refuses — refusing locks out an older hub and hands an attacker a denial vector. The record itself is never copied: the row stores an identity (+ the handle, since `GET /profiles/:handle` is the addressed read), and the REVIEWER'S BROWSER fetches the hub's already-public, already-CORS'd, already-`no-store` profile at render, where `profile_state='active' AND NOT hide_stats` is applied inside the module — so C2's live hide is honoured by construction with no cache to outlive it, and a hub outage degrades to "record unavailable" beside a queue that still works. An instance→hub server pull was rejected on ADR 0171 D1's own grounds. A private account's application carries name + handle + note in V1; the D3 sliver needs `project.recruit`, which the hub cannot check cross-origin. **D3** "project owner" is the project's OWN archon — the registrant is whoever `maybeSeatFirstAdmin` seated, delegates are whoever that archon grants on that instance, and because the instance IS the project the per-project-scoped-permission problem dissolves; the check is the atom that already guards the act, `access_request.review` (floor archon), and this goal mints NO new permission (widening the read half is the console wave's `project.recruit`). `platform_identity_projects.owner_github_id` stays advisory and is never a gate — a hub-held owner list consulted before an admission is the inversion ADR 0141 exists to prevent. **D4** three statuses, unchanged: `invited` IS the approval and the acceptance act is signing in (an `accepted` state would be a second place membership is asserted, free to drift from the one that admits); rescind is `invited → dismissed`, which the PATCH must learn to accept (it resolves only from `pending` today) and which must never pretend to remove a member who already signed in; an `invited` row expires read-time at 30 days whatever created it, while an application never expires because a stale `pending` admits nobody; approve/decline are idempotent on the value they hold. Rejected: a cross-project `applications` table on either side; a stored profile snapshot; the assertion in the body; refusing an unvouched application; the server-side pull; reading `owner_github_id` as authorization; a new "project owner" atom; an `accepted` status.](0201-cross-project-application-vouched-row-live-record.md) | platform identity / applications |
292
+ | 0202 | [**A connection is one directed row on the platform account, and every degree label is computed over the graph the viewer is allowed to see** ([task 1002294](https://cloudbongos.com/builders#/task/1002294), goal 1000045 criterion C6 — the design spike for the connections wave). Criterion C6 asks for LinkedIn-shaped connections; the cross-project data plane it must sit on is account-global (ADR 0141) and governed by an owner-signed privacy matrix (`docs/specs/<redacted>.md` D1/D2), where ADR 0171 D4 already bans "a count plus a hidden total" as a leak by subtraction. Decisions: (1) **one directed request row** on a new `platform_identity_connections` — `requester`/`addressee` kept because the inbox, cancel and cooldown all need to know who asked, with `pair_lo`/`pair_hi` GENERATED from LEAST/GREATEST and a partial unique index over `pending|accepted` so a duplicate AND a simultaneous reciprocal request are database errors, not route bugs; module-owned and `platform_identity_`-namespaced (ADR 0083 §5). (2) The `goal_membership_requests` lifecycle (`pending → accepted|rejected|cancelled`, atomic resolve-and-act) with three deliberate divergences: a decline is **silent**, a re-ask inside its 90-day cooldown is **swallowed and answered as if it worked** (ADR 0194 §3 applied to a person — a 409 saying "declined" would disclose what the silence protects), and pending requests expire at **30 days read-time** (privacy spec D4), with expiry resolved in the same statement that inserts, since `now()` cannot live in an index predicate. Remove **hard-deletes** — a "A removed B" tombstone is data neither party consented to keep and no read path uses. (3) Degree = a **recursive CTE seeded at the viewer**, `DEGREE_MAX = 3` with `FANOUT_1`/`FANOUT_2` caps and a deterministic frontier order; past the caps the label is `null` ("out of network"), never a number the query did not establish. Envelope stated honestly: single-digit ms at hub scale, caps bite around 10⁴ accounts, and they convert a hub-heavy account's millions-wide 3-hop into a *missing label* on a page that still renders. (4) Connections get **no predicate of their own** — they consume task 1002967's single effective-visibility reader, published as JS and as `accountVisibleSql(alias)` (the ADR 0182 form); `profile_state='active'` floors the feature, a private account answers **404 not 403** and never appears in a public list, `hide_stats` suppresses the numbers beside an edge but not the edge, degree is an **own-scope read** so viewer-relative labels coexist with viewer-independent public responses — and every count is the length of the array the viewer may see. (5) "Builders you may know" from mutuals + shared projects + recency, one explainable linear score, a named project only where its catalogue row is publicly listed; shared goals refused (instance-local), guilds a reserved empty slot, and **Discord refused outright** (outside the trust boundary; the hub stores no Discord identity), as are the GitHub follower graph and address-book import. Rejected: two mirrored rows on accept; a symmetric edge with no requester; a core table beside `builders`; a materialised closure/adjacency table (privacy-filtered at write time = a cached visibility decision, which ADR 0171 D2 forbids); an in-process graph cache; a graph database. Deferred and named: a connections-specific hide toggle, blocking, what an accepted connection unlocks on a private account, and notifications beyond the existing attention card. Exposes to the guild/orb wave: `neighbours`, `degreeBetween`, `mutualCount` — **not** a stored strength scalar.](0202-connections-graph-and-bounded-degree.md) | platform identity / privacy / social graph |
293
+ | 0203 | [**The doorway changelog leaves `src/module-api.js` — the CI auto-patch carrier appends to a markdown ledger instead** ([task 1003335](https://cloudbongos.com/builders#/task/1003335)). Amends [ADR 0161](0161-publish-on-merge.md) §2. The `CORE_VERSION` release history lived as a comment block inside the doorway file, and ADR 0161's carrier appends two lines to it on EVERY merge-triggered release, pushed by `github-actions[bot]` straight to `main` — bypassing the `unit` gate it breaks. Release 1.19.407 took the file to 1501 lines, one over the `oversized_file_count` ratchet: `unit` went red on `main` and every open PR inherited it, self-inflicting and monotonic. Decision: the ledger moves to `docs/module-api-changelog.md` — markdown, which `fitness-ratchets.js` (`git ls-files '*.js'`) structurally never counts, so it can grow forever. All 1222 lines / 448 versions moved verbatim (only the `// ` marker stripped, kept in a fenced block so the hanging indent survives); the `const CORE_VERSION` literal and its line shape do NOT move, so every reader that greps it is untouched. `bump-version.js` transforms both files before writing either and refuses a half-applied bump; `publish.yml` stages both, pinned by `tests/bump_version.mjs`; the file is registered in `PUBLISH_ALLOWLIST` so the history keeps shipping inside `@bongos/core`; `src/module-api.js` keeps a pointer doc-comment where the block was. Rejected: trimming entries in place (buys one release), raising the baseline (out of policy per `config/fitness-baselines.json`'s own `_comment`), exempting the file from the ratchet.](0203-doorway-changelog-out-of-module-api.md) | build pipeline / distribution |
294
+ | 0204 | [**The apex is dark only; light is the builders' hall's** ([task 1003336](https://cloudbongos.com/builders#/task/1003336), goal 1000080). Supersedes in part [ADR 0196](0196-chrome-world-token-contract-and-the-landing-sheets.md) (the light-mode half of the landing build) and the landing direction's 2026-08-27 pick of "light rebuilt to the dark standard". The owner, reviewing the planets: "only black or only white is a bit bland… leaning towards only dark mode for this page and keep the dark/light mode to be solely for the builder hall", confirmed as the WHOLE apex. Decision: `/`, `/projects`, `/privacy`, `/terms` and `/contact` pin `data-mode="dark"` on `<html>` — no pre-paint script, no `cb-color-mode`, no `?mode=`, no OS-preference read; the moon/sun glyph and its wiring leave both bars; the hall's own `hall-theme` switch is the platform's only light/dark control. The light tier of `world.css` / `cosmos.css` and the dark-twins rule STAY until the tier's own removal (the light-pair contrast tests keep measuring). The kit learns `"modes": ["dark"]` in a states file (validated; `render.js` takes it as the default). Rejected: a dark default with the toggle kept; deleting the light tier in the same diff; `--modes dark` on every kit call instead of the page's contract.](0204-the-apex-is-dark-only.md) | apex / design system |
295
+ | 0205 | [**A federated check-in may not speak for a person the hub never saw sign in: the membership carries the hub's own witness, and identity is never re-synced from client credentials** ([task 1003340](https://cloudbongos.com/builders#/task/1003340), goal 1000045 — closes security report 1000027, high, LIVE on the hub). `POST /sso/membership/check-in` authenticated the CALLER (`client_id` + `client_secret`) and took the SUBJECT (`github_id`, `github_login`, `avatar_url`) from the request body — client credentials say which PROJECT is calling, never who the person is. So any registered SSO client could (1) rewrite a real person's public `github_login` and `avatar_url`, which `upsertAccount`'s `ON CONFLICT` re-synced unconditionally and stored with no scheme check, and (2) mint the `<redacted>` row that [ADR 0171](0171-profile-rollup-join-and-consent.md) D7's `membership_required` precheck reads — so the anti-impersonation gate on `POST /sso/activity/rollup` was satisfiable by the very call it was meant to gate, and forged credits/karma/rank reached a public profile. Decisions: (1) migration `<redacted>` adds `signin_verified_at`, written ONLY by `recordFederatedSignin` from the two moments the HUB itself authenticated a human for a client — `/sso/token` (a code minted under a live hub session, bound to the pair) and `/sso/device/poll` (a GitHub approval, audience bound from the typed origin) — so no request-body field reaches the statement and the column is evidence a caller structurally cannot write; (2) `upsertMembership` becomes `touchMembership`, an UPDATE gated on that column — a check-in can no longer CREATE a membership, answering `409 signin_required` and writing nothing, while signing in accepts a standing `pending` invite and leaves any other coarse kind to the instance to report; (3) a federated client gets insert-only `ensureAccount` (`DO NOTHING`), and `upsertAccount`'s identity re-sync belongs to hub-authenticated callers only — nothing is lost, because both of those already re-sync on every sign-in from GitHub's own answer; (4) the rollup gate reads `membershipBacksRollup` (non-pending AND witnessed), not row-existence; (5) `safeAvatarUrl` admits absolute https only, applied inside both account writers rather than at each render; (6) NO backfill — no predicate separates a legitimate check-in row from a forged one, and the first sign-in after deploy heals a real membership because the witness is stamped BEFORE the check-in and rollup that follow it, so a profile goes stale, never blank. No wire-format change: the request schemas are byte-identical and the ADR 0177 `membership_kind` window is untouched, so no federated instance upgrades. Proof `tests/sso_checkin_identity_spoof.mjs` (DB-free, in the default unit gate; 10 of 14 assertions fail against the pre-fix code, 2 controls prove the gate is not a wall). Rejected: carrying the hub assertion in the check-in body (strict validation would 400 every un-upgraded instance); ADR 0156-style source-guarding (answers "did the user set this", the wrong question when the writer is a third party); backfilling `signin_verified_at = joined_at`; a silent 200; retro-filtering `listMembershipsForAccount`.](0205-federated-checkin-needs-the-hubs-own-signin-witness.md) | platform identity / security |
296
+ | 0206 | [**A leaked bearer's remedy is the account holder's own revoke, not an absolute session ceiling** ([task 1003341](https://cloudbongos.com/builders#/task/1003341), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `lookupSession` rewrites `expires_at = now() + 24h` on every authenticated request, so a CLI bearer in daily use never expires; if it leaks, the holder keeps it alive simply by using it. `POST /auth/logout` kills only the token presented on that same request — useless when the token you want dead is the one you no longer hold — and the two endpoints that can kill another session are gated on the admin `auth.session.revoke` atom, so the victim had to find an Archon, and a single-owner self-hosted instance may have none. Decision: ship the OWN-scoped remedy (`GET /me/sessions`, `POST /me/sessions/revoke`, `POST /me/sessions/revoke-all` on `requireBuilder` alone, every statement keyed on the caller's own id from the presented session, the token value never selected or returned, `revoke-all` deliberately including the requesting session) and deliberately NOT an absolute ceiling on the sliding refresh: a ceiling is unconditional (migration 035 already had to backfill a 24h grace so it would not kill the session running it), any cap loose enough not to be a daily nuisance leaves a leaked token live for weeks, and the visibility this task ships — `created_at` beside `last_used_at` in the list — is the compounding half of the fix. A demotion already sweeps sessions, and a deactivated builder is denied on the hot path, so the authority-drift case is closed by other means. Rejected: a `created_at + N` cap now; a shorter TTL; three more routes inside the already-largest `routes/auth.js` (a separate file is what lets the no-caller-named-owner rule be asserted over a WHOLE file); a `keep_current` flag; 404 on a session id that is not yours (it would confirm which ids exist); lowering the `auth.session.revoke` floor. Revisit for machine principals, a compliance maximum, or evidence that people do not prune what the list shows them.](0206-own-scoped-session-revocation-no-absolute-ceiling.md) | auth / account security |
297
+ | 0208 | [**Building the application lifecycle: the handle waits for its writer, and the vouch ADR 0201 D2.1 describes has no minter that can express it** ([task 1002284](https://cloudbongos.com/builders#/task/1002284), goal 1000045 — R08, criteria C3/C4). Builds [ADR 0201](0201-cross-project-application-vouched-row-live-record.md) D1 + D4 + D3.2 and records where the design could not be written literally. **The contradiction:** D1.2 says the migration adds "exactly" `applicant_github_id` + `vouched_at`; D2.6 says the applicant's handle is stored on the row. Decision — the handle waits for R09 ([task 1002285](https://cloudbongos.com/builders#/task/1002285)), because the ADR's own dependency table routes D2 *wholesale* (including the handle) there, and because R08 ships no vouch writer at all: a permanently-NULL `applicant_handle` would read as "this account has no handle" when it means "R09 has not shipped". **The hole:** D2.1's replay separation cannot be built as described. `mintHubAssertion` takes no `purpose`, and the asymmetry protecting `mintHubLogoutToken` works only because a logout token carries no identity — an application vouch carries both, so it collides with the login assertion's shape; worse, `verifyHubAssertion` does not reject a payload carrying a `purpose`, so a vouch minted that way would verify *as a login assertion* today. R09 needs a third mint/verify pair plus a tightening of `verifyHubAssertion` (free only while no minter emits a `purpose`), and a deliberate choice about the single-use `jti` store. Also decided: the echo's READ joins `platform_identity_sso_clients` (a `client_id` alone is not renderable) while the TABLE stays the three columns D1.3 named; a re-application refreshes `applied_at`, so the 30-day window runs from the most recent ask; an idempotent PATCH returns the row **without** re-stamping `resolved_by`/`resolved_at`, so the audit trail records who actually resolved it; `dismissed → invited` is refused `409 already_dismissed` naming `POST /access-requests/invite`; and `GET /my-projects` drops from `applied` any project the caller now builds on, so the hub never implies an outcome it is not allowed to hold. Honest limits: `vouched` is false for every real row until R09 lands (the two-class queue is proved by a seeded row, not by a live writer); `already_approved` still reaches the hub as "already_member" prose through `joinRelayOutcome`'s bare-409 branch, which is R10/R12's surface; and the proofs are DB-free, so the partial unique index is modelled rather than exercised — five mutations were run to show the fake pool bites. Rejected: adding `applicant_handle` anyway; bolting a `purpose` argument onto `mintHubAssertion`; a `status` column on the echo; refusing `dismissed → invited` with the existing `not_pending`; a new "project owner" atom.](0208-application-lifecycle-the-handle-waits-and-the-vouch-has-no-minter.md) | platform identity / admission lifecycle |
298
+ | 0209 | [**Two routes answering one question spend one budget; whether the answer itself collapses is the owner's call** ([task 1003339](https://cloudbongos.com/builders#/task/1003339), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `GET /access-requests/status` (public, returns `member|invited|pending|dismissed|none`) and `GET /auth/web/admission-status` (its bare-boolean twin) both answer "does this GitHub login have an account here?", and neither was throttled — `rateLimitMiddleware` returns early for every non-write method, and the one read limiter is mounted only under `/public/*`. Scripted at line rate either becomes a login→membership roster: who builds here, and confirmation that a named person is present. Decision: ship the ceiling as ONE budget — `accountExistenceReadRateLimit`, 120 reads/60s per IP over a single bucket, exported as one middleware INSTANCE rather than a factory and mounted on both routes, because two limiters each configured 120/60s look identical in a diff, pass any test that counts to the limit per route, and hand an enumerator 240/min by alternating. The onboarding module reaches it through the doorway, so `src/bongos/middleware/rate-limit.js` joins `KERNEL_FILES` on `logger.js`'s argument: authz decides WHO may ask, the ceiling decides HOW OFTEN, and the file imports nothing. Deliberately NOT decided here: collapsing the response so a non-subject caller cannot tell `pending`/`dismissed`/`none` from "not admitted" — the consumer sweep found route 1 has exactly ONE consumer (`scripts/gds/login.js`), which already treats `pending` and `none` identically, so collapsing costs exactly one thing: the only message in the codebase that tells a declined applicant they were declined. That is a judgement about what an applicant is owed, so it goes to the owner as a blocker with the evidence. Noted either way: collapsing does not close the oracle — `admitted:true` still means member-or-invited. Rejected: a separate limiter per route; a `createReadRateLimit` factory (two matching call sites get two buckets — the generality loses the property); vendoring a fourth copy of the sliding window; hardening only the smaller route; collapsing on my own judgement because "nothing much" reads it.](0209-one-budget-for-the-account-existence-oracle.md) | security / rate limiting |
299
+ | 0207 | [**Building the connection graph: three departures from ADR 0202, and the three limits it is honest about** ([task 1002295](https://cloudbongos.com/builders#/task/1002295), goal 1000045 criterion C6 — the build of the connections wave). ADR 0202 decided the graph; building it surfaced three places where the decision's MECHANISM could not be written literally without losing the PROPERTY it was about, and three places where the shipped code cannot promise as much as a casual reading suggests. Departures: (1) **level-wise CTEs, not `WITH RECURSIVE`** — a recursive term can carry neither a per-level fan-out cap nor an ordered per-level truncation, which are the two things D3 actually decided, so a recursive build would have had a depth bound and no fan-out bound; a test asserts `RECURSIVE` appears in neither query. (2) **1st degree is an unbounded pair-index probe**, never a scan of the capped frontier — reading it off the `FANOUT_1`-truncated 1-hop reports "2nd" for someone in the viewer's own connections list once they pass 500 connections, and the rule this states is that **a cap may only ever make an answer less specific (`null`), never wrong**. (3) The shared visibility predicate lands in its own file, `modules/platform-identity/account-visibility.js` (`isAccountVisible` / `accountVisibleSql` / `accountActiveSql`), because task 1002967 had not shipped and `platform-identity.js` sat at the size ratchet with zero headroom — **1002967 adopts it rather than writing a second predicate**. Also decided: node visibility during traversal is viewer-relative and asymmetric on purpose (your OWN 1-hop floors on consent alone so a friend who goes private does not vanish from your list; every deeper hop clears the public predicate); cancel is a `DELETE` an ADDRESSEE may not use, so the 90-day cooldown cannot be dodged by calling a decline a delete; and the create response is `201 { status: 'pending' }` and nothing else, so a swallowed re-ask has nothing to fabricate. Limits stated rather than implied: a swallowed re-ask is silent in the RESPONSE and observable by its ABSENCE from the requester's outbox (the claim is narrowed, not the code — a decoy row would be a lie in the database to protect a lie in the API); the TARGET's 1-hop is capped too and that truncation is deliberately NOT reported, because saying so leaks one bit about a third party's degree; and the query COUNT is bounded while the frontier's WORK is O(sum of degree over the 1-hop), since `ORDER BY … LIMIT` materialises before it truncates — the revisit trigger is a measured p95, and the cheap next move is a `statement_timeout` returning the `null` D3 already chose. Deferred and named: a per-builder cap on OUTBOUND requests (the cooldown stops repeat asks to one person, not one builder asking a hundred — a real number is owner-signed), and a real-Postgres pass on the expire-then-insert write path, which CI cannot run.](0207-connections-implementation-departures-and-honest-limits.md) | platform identity / social graph / privacy |
300
+ | 0212 | [**The connections section is built on the profile page that exists, and it addresses a builder by asking the API to name them** ([task 1002296](https://cloudbongos.com/builders#/task/1002296), R20 · goal 1000045 criterion C6 — the UI half of the connections wave). The profile-page spec puts section 6 on the canonical `cloudbongos.com/u/<handle>`, which [task 1002806](https://cloudbongos.com/builders#/task/1002806) proved does not exist (no route, no page, no search entry, no sitemap row) and which belongs to goal 1000048 R14. Decisions: (1) **section 6 lands in the reserved slot R04 left on the hall profile page** (`modules/hall-ui/public/profile.{html,js}`), as a separate `profile-connections.js` so neither the page script nor `platform-identity.js` grows against its line ratchet, and it follows the CROSS-PROJECT section’s contract exactly — the markup ships `hidden` and reveals only where the hub answered, so an instance without `platform-identity` never renders it. It does not pre-empt the canonical page, which inherits the same three own-scope calls. (2) **Addressing: the page hands the API the `github_login` it holds and uses the `handle` the response echoes back for every write** — core `GET /builders/:id/profile` whitelists nine columns and `handle` is not one of them, and widening that whitelist is task 1002804’s canonical-URL work. `findAccountByHandle` already resolves current-or-previous handle and the route answers with the canonical one, which is the same move the API built for a stale Connect link. It fails closed into the deliberately indistinguishable 404 (unknown / provisional / private stranger), so the ONE honest gap is stated rather than discovered: **a twice-renamed handle has no connect button on this surface**, and it disappears when `/u/<handle>` lands. (3) Three rendering rules, enforced by `tests/profile_connections_ui.mjs` (real router over a WHERE-derived fake pool on a real socket, real client above it): `degree: null` renders "Out of network" and never "not connected", with copy that is BYTE-IDENTICAL whether `truncated` was set — varying it would rebuild the ADR 0207 L2 side channel in words; a sent request claims exactly `201 { status: 'pending' }`, so a re-ask swallowed by the 90-day cooldown simply shows "Connect" again next load with no explanation anywhere; and every count is the length of the list beside it, with `seen_at` and `truncated` never reaching the screen. Rejected: waiting for the hub page (it would block C6’s proof on another goal); building `/u/<handle>` here (the spec says only one such page gets built); adding `handle` to the core profile whitelist or to `cross_project` (the latter is `null` under `hide_stats`, which is exactly where the edge must still show — ADR 0202 D4 §3); resolving through public `GET /profiles/:handle` (it 404s for a private account the viewer IS connected to). Deferred and named: a public per-builder connections list (there is deliberately no such route), removal from the other person’s profile, and sections 7–8.](0212-connections-on-the-profile-surface-and-addressing.md) | platform identity / social graph / hall UI |
301
+ | 0214 | [**The canonical profile URL gets one resolver, and it stays dark until the page it names actually resolves** ([task 1002804](https://cloudbongos.com/builders#/task/1002804), P08 · goal 1000045 criterion C1 — the canonical/redirect wiring). Profile-page spec Decision 1 wants `rel=canonical` on the instance, a link-out to the hub, and a numeric-id redirect; `/u/<handle>` does not exist (goal 1000048 R14 is `backlog`, and R01 — the ADR that fixes its PATH — is unwritten, while R03 proposes moving platform pages under `/bongos/…`). Decisions: (1) **one resolver** (`modules/platform-identity/canonical-profile.js`) answers where a profile lives, and it answers `null` while the page 404s — a canonical naming a non-indexable URL consolidates nothing and risks the one page that works, and the address is not even settled yet; one boolean flips all three surfaces together, and a test reds if a `/u/` route lands while it is still false. (2) **`{{hubOrigin}}`** joins `applyBrandTokens` (own `publicOrigin` on the hub, `loadIdpConfig().origin` when federated, `''` otherwise) — the server-side stamp ADR 0171 D1 asked for — delivered as a `<meta>`, never inside an `href`, because an empty value in an href degrades to a same-host URL claiming to be the hub. (3) **`handle` joins `GET /builders/:id/profile`** under the EXISTENCE predicate (`isAccountVisible`), not the rollup one: `hide_stats` suppresses numbers and never addressability, and the `handle`+`cross_project: null` pair is already published unauthenticated by `GET /profiles/:handle`. This closes ADR 0212’s stated gap — a twice-renamed builder now has a connect button. (4) **The hub redirects and a federated instance canonicalizes**, which is how Decision 1’s two apparently contradictory clauses reconcile: the hub serves the same person at both URLs, a federated instance serves genuinely different content. (5) **The redirect is not an existence oracle** — its non-redirect branch is the ordinary working page, absent/provisional/private are byte-identical, and the one bit it exposes is what `/profiles/:handle` already answers with 200-vs-404.](0214-canonical-profile-url-one-resolver-dark-until-the-page-exists.md) | platform identity / SEO / hall UI |
302
+ | 0210 | [**The recruiting directory graduates on an instance-asserted ownership claim, and the recruiting opt-out gates the directory and nothing else** ([task 1002290](https://cloudbongos.com/builders#/task/1002290), R14, goal 1000045 criterion C5 — "project owners, not only Archons, browse builder profiles to recruit"). Two independent problems wore one task id. The reach flag `recruiter_discoverable` protected nobody — task 1002967 said so, and the Settings row stayed in the future tense because of it. And "widen the Archon-only /scouting" was already stale (ADR 0157 lowered `project.curate` to metic) while hiding the real question: **a project owner is not a rank**, and someone who registers a project is an Archon on their OWN instance and usually a bare `xenos` on the hub where the directory lives, so no floor reaches them without reaching every stranger. **D1** "project owner" on the hub is `<redacted>.membership_kind = 'owner'` — written by the PROJECT itself at `POST /sso/membership/check-in` from its own first-admin config, and refused unless the hub's own `signin_verified_at` witness stands (ADR 0205) — and never `platform_identity_projects.owner_github_id`, the hub-curated column ADR 0201 D3 names and forbids; the prohibition is about a hub-held list consulted before an ADMISSION on someone else's box, and this is the hub reading the instance's own mirrored claim before a read of the hub's OWN directory. **D2** the two doors are an OR in `modules/platform-identity/scouting-authz.js` (the `goal-authz.js` owner-or-rank shape, kept out of `platform-identity.js` at its size ratchet): ownership is asked first and a "no" falls through to the UNMODIFIED `requirePermission`, so a refusal keeps its exact code, `required`/`held` and floors map — the widening can only admit; the atom is spelled in the ROUTE's middleware list, because a gate hidden in the helper would make `route-rank-check.js` and `gen-api-docs.js` record this privileged route as `any-builder`; and a broken ownership read falls closed onto the rank door rather than admitting or 500ing. No atom is minted — `project.recruit` belongs to the console wave (spike 1002786 R3). **D3** `PAGE_GATES` gains an optional `alsoAdmits` naming a function on the `platform-identity.hub` port, declared on `requireProjectCuratePage` alone and consulted only after the permission check refused, so the `/people` shell admits exactly the two audiences its data route does; absent port, absent function or a throw all deny. **D4** the reach COALESCE gets its SQL twin in `account-visibility.js` (its sole owner) and composes into `scoutingListableSql` = rollup AND reach, which `listScoutingRollup` joins — but reach is deliberately NOT folded into `rollupVisibleSql`, because the community spec D3 says the opt-out governs the recruiting surface only and folding it down would silently empty a future public leaderboard. **D5** no migration: both columns exist and migration 003's `(github_id)` index already serves the probe. **D6** the ownership query JOINs `platform_identity_sso_clients` on `status='active'` — a membership row never expires and nothing ages it out, so suspending the client (the lever that already stops a project signing anyone in) had to revoke the read it derives, or suspending a rogue project would leave its owner reading every builder on the platform forever. The task's own adversarial review also found the sole-owner drift guard was matching only the UNALIASED spelling of the reach COALESCE — i.e. not the one that ships — and two fail-open defaults (a prototype-reachable `alsoAdmits`, and an alias guard resting on template evaluation order); all three fixed and mutation-checked. ⚠ Honest limits recorded, not fixed: the hall NAV still hides People from a project owner (task 1003349, sequenced with R15 which owns that surface), the `touchMembership` write path stays as ADR 0205 left it, and `openapi.json` cannot express "or owner". ⚠ And DELEGATES do not reach the directory yet (blocker 1000122 + task 1003350, linked) — an instance reports `owner` for its first admin only, so the registrant half works and the delegate half needs the instance to report `owner` for any archon, which changes a cross-version wire contract and a shipped bucketing; filed as blocker 1000122, and nothing here changes when it is answered. Rejected: lowering the floor further (a floor low enough to admit a registrant excludes nobody); a `project.owner` atom (ADR 0201 D3 rejected it); minting `project.recruit` here; gating on `owner_github_id`; folding reach into the rollup predicate; widening `membershipKind()` on our own judgement; shipping the route without the page.](0210-scouting-graduates-on-an-instance-asserted-owner-claim.md) | platform identity / authz + privacy |
303
+ | 0215 | [**The pre-accept recruiter sliver is a CEILING, not a step: a whitelist keyed by consent-and-reach, on the recruiting surface’s own door** ([task 1002968](https://cloudbongos.com/builders#/task/1002968), PV3 · goal 1000045 criteria C2/C5; privacy spec D3, owner-signed 2026-08-16). Privacy spec D2 has a row nothing could serve — **private · discoverable** — and ADR 0210 D4 named the reason: *“the full rollup is never its disclosure — the D3 sliver is”*. Decisions: **D1** the audience predicate is `recruiterSliverVisibleSql` = `accountActiveSql AND recruiterReachSql`, composed in `account-visibility.js` (its sole owner) and the ONE composed answer that deliberately **omits the existence axis** — ANDing `accountVisibleSql` would empty the sliver of exactly the account it serves; dropping it is safe only because reach is `COALESCE(recruiter_discoverable, visibility=’public’)`, so a private account reaches this surface only by its owner’s explicit opt-IN — and, after adversarial review, that opt-IN must be the one they MEANT: `setAccountPrivacy` now resets `recruiter_discoverable` to NULL when a PATCH turns an account private without naming reach, because a `true` set while PUBLIC meant “recruiters may see my public profile” and this predicate would have activated that stale consent silently. `profile_state=’active’` still floors it (a provisional account is never recruiter-visible) and `hide_stats` is absent, because hide changes the sliver’s SHAPE, never who appears in it; the guard PINS that existence-less predicate to its one reader, since a future public read joining it would look adopted while serving private accounts. **D2** the projection is a literal whitelist — `handle`, `display_name`, `avatar_url`, `disciplines`, `active_recently`, and under `hide_stats` the last two are **absent, not null**, with the activity query never issued at all (the cheapest way for a later edit to leak a number is for it to already be in hand). `last_active` is compared to the 30-day window IN SQL and only the boolean returns, so the date never enters the process; `github_id`/`github_login` are never KEYS, every count and any profile URL stay out — with one limit stated rather than glossed: the avatar D3 grants is GitHub’s own URL and embeds the account id, so the proof pins WHERE the id may appear instead of claiming an absence the code does not deliver. **D3** the handle is an ADDRESS, not a door — `GET /profiles/:handle` joins `accountVisibleSql` and 404s for the very account whose sliver was just read (asserted, with a 200 control), and a PREVIOUS handle resolves to nothing here unlike the public page’s 301, because a recruiting read has no shared links to preserve and a redirect would confirm a rename. **D4** one route, `GET /scouting/:handle`, beside the directory in `routes/scouting.js` but deliberately NOT sharing its gate: **no `project.recruit` atom is minted** (ADR 0201 D3 and ADR 0210 both reserve it for the console wave that defines it), and until it exists the sliver holds spec D1’s declared floor for that atom — `api.requireRank('archon')`. The first draft reused the directory’s door and that was WIDER than the owner-signed floor on both halves (`project.curate` at metic, OR any project owner — typically a bare xenos), on the first surface that discloses a PRIVATE account; narrowing after people are disclosed is not recoverable, widening later is. The audience question is the owner’s and is FILED, not taken — blocker 1000126, with the C5 tension it carries and a one-line swap either way. 404 never 403 (a 403 confirms the handle), `no-store` (ADR 0171 D5). **D5** it grants nothing (ADR 0016): the module is required by exactly ONE first-party server file — the route that serializes it — enumerated from source, and no rank/role/permission/membership travels with it. **D6** the Settings copy widens back from “the People directory” (narrowed by task 1002290 while this was unbuilt) to name **both** recruiting surfaces — “both” and not “every”, because a guard asserts `routes/scouting.js` mounts exactly two routes and that both readers compose `recruiterReachSql`. The half that was actually WRONG was the grant itself: the copy promised a hidden account “your name and nothing else” while the sliver also ships the AVATAR, never named the picture in the shown branch, and never told a private account this is the one thing a recruiter sees while their page answers “not found”. The guard now iterates SLIVER_KEYS from the module instead of checking transcribed phrases, so a sixth granted field cannot ship undescribed. **D7** no migration; the port lives beside `getPublicProfileExtras` in the MODULE, not the file, which is on the 1500-line ratchet. The proof is a whitelist over the module’s own exported key list, planted improbable sentinels asserted absent from the whole body with controls against vacuity, and a fake pool that reads boolean STRUCTURE — the house interpreter collects terms and ANDs them, so rewriting this predicate’s `AND` to `OR` (which in Postgres serves EVERY active account) passed all three test files green until each term was made to be CONSUMED and the residue required to be pure conjunction. That hole is still open in five sibling proofs (idea 1000877). Four further defects came out of the same review: the credits/karma sentinels sat on rows the sliver never reads, a “control” compared a fixture to itself, and the one-reader pin missed the predicate’s JS twin. ⚠ Honest limits: **no UI consumes it yet** (the recruiting view is the console wave’s and task 1002972’s); **the audience was unresolved when this shipped and is settled now** (blocker 1000126, answered 2026-08-29 — the owner KEPT the archon floor; see ADR 0217 D2) — the floor is still narrower than goal C5 describes, so project owners cannot use the sliver today, a tension ADR 0217 D1 resolves by making C5’s subject (browsing) ungated rather than by widening this surface; the two-route guard counts only `routes/scouting.js`, so a recruiting read added elsewhere would not trip it; the route is unmetered (idea 1000876); and `active_recently` understates for a builder whose only project never reports a rollup. Rejected: flooring on `accountVisibleSql`; folding reach into `rollupVisibleSql`; minting `project.recruit`; **reusing the directory’s door**; leaving a stale `recruiter_discoverable = true` to read as consent after an account goes private; a second route file; null-instead-of-absent rollup keys; a profile URL / `github_login` / a project count / a raw `last_active`; 301-ing a previous handle; 403 for a non-discoverable builder.](0215-the-recruiter-sliver-is-a-ceiling-not-a-step.md) | platform identity / privacy |
304
+ | 0213 | [**The platform T&C acceptance is the hub's own witness, written once, and consent becomes two columns** ([task 1002913](https://cloudbongos.com/builders#/task/1002913), goal 1000045). ADR 0141 §4 deferred consent for cross-project aggregation to "the platform T&C" and ADR 0171 D3 wrote its own definition around the moment that acceptance "can later be captured" - but no acceptance record existed, and while `/terms` DOES exist (task 1003251) it says nothing about work crossing between projects, so the public-profile premise ("already carries platform T&C consent") was untrue for every account. The scouting.js:6 pointer the task named had also been overwritten by ADR 0210, so the debt was going invisible rather than getting paid. Decision: `terms_accepted_at` + `terms_version` on `platform_identity_accounts` (migration 019), written ONLY by `upsertAccount`'s `activate` branch - one caller, /sso/authorize, where `activate: true` is a source literal and every field comes from the hub's resolved session (the ADR 0205 D3 property; a record a caller could assert is evidence of nothing). COALESCE makes it write-once, so the column keeps meaning WHEN consent was given. The consent axis in `account-visibility.js` becomes `profile_state = 'active' AND terms_accepted_at IS NOT NULL`, composed into every other predicate, which is what makes the tie structural rather than a promise. NO backfill (D4): existing active accounts re-accept on their next hub sign-in - ADR 0171 D3's own rule and P03's ratified rationale - and that visible consequence, plus the wording itself, went to the owner on blocker 1000123. Rejected: an acceptances TABLE (no event stream yet), a click-through gate (an interstitial in a redirect built to be invisible; additive later), backfilling (records a consent that never happened), deriving profile_state from the timestamp, and putting the predicate in `terms-acceptance.js` (a second owner of an effective-visibility answer, which ADR 0202 D4 forbids).](0213-platform-terms-acceptance-is-the-hubs-own-witness.md) | platform identity / consent / privacy |
305
+ | 0216 | [**A fake-pool interpreter must FAIL LOUDLY on what it cannot model; the refusal is uniform, the interpreters stay separate** ([task 1003354](https://cloudbongos.com/builders#/task/1003354), folding in idea 1000877 from the adversarial review of [task 1002968](https://cloudbongos.com/builders#/task/1002968)). Nine test files carry a **fake pool** whose row filter is derived from each statement’s own WHERE text — the design that makes an isolation proof say something about the real query rather than about a canned response. It has one silent failure mode: a term the interpreter does not recognise is simply not applied, so the boundary goes vacuous and every assertion above it keeps passing. Each file guarded a piece of that (unread `$N`, unknown literal, unmodelled `IS NULL`) and every one of those is a **term** guard — and the term list is not the predicate. Three reviews found the same shape. **The missing term:** ADR 0213 made consent two columns and `scouting_owner_access_and_reach.mjs` modelled only the first; a NULL test is a third syntax neither sweep can see, so the suite reported 17 passed while modelling a predicate the database no longer had, inside a file whose own header calls that impossible. **The rewritten joiner:** an interpreter that COLLECTS recognised terms and ANDs them re-imposes an `AND` the SQL may no longer contain — rewrite a predicate’s `AND` to `OR` and Postgres serves every active account on the platform, or lets any builder revoke any session by id, while the fake answers exactly as before. **Recognition is not application:** ADR 0213’s own first draft checked only that a column name was recognised, and a mutation deleting the filter still passed. Decisions: **D1** the governing rule — an interpreter that cannot model a term must REFUSE the statement, never ignore the term; incompleteness is allowed, silence is not, because a refusal fails the build in the session that changed the SQL while a silent drop fails nothing ever. **D2** the mechanism is a CONSUMING parse: each recognised term is spliced out of the text and the residue must be a pure conjunction (`/^[\s()]*(?:AND[\s()]*)*$/`), which subsumes every term guard, catches `OR`/`NOT`/`CASE`, and retires ADR 0213’s `modelled`-set bookkeeping because an unapplied term is by construction still there; a legitimate `OR` (the rename window, the either-side clause, the expiry disjunction, the rescind guard) is consumed as a whole unit. **D3** the interpreters stay SEPARATE and only the refusal is uniform — the term vocabulary differs completely across six tables, so a shared registry would be a small SQL engine and would couple unrelated proofs; instead every copy carries the identical guard and its OWN calibration test, which is strictly better anti-drift than extraction (a helper proves the guard exists somewhere; a calibration test proves it is reached by that file’s own statements). **D4** a modelled term proves nothing until a fixture makes it observable — three fixtures added, each differing from its control in exactly one column, so the mutation that deletes the term has somewhere to land; where a file deliberately lacks one because another owns that matrix, the comment says so. **D5** every copy verified BY MUTATION against the real source — ten mutations, ten reds, each a 0-failure pass beforehand. ⚠ Honest limits: no fitness check asserts a NEW fake pool carries the guard (identifying “a fake pool” needs a heuristic, and a heuristic gate over test internals has a false-positive cost) — the per-file calibration tests hold the nine that exist and a tenth is a review question; `canonical_profile_url.mjs` is untouched because its residue must be EMPTY, strictly stronger already. Rejected: one shared interpreter module; a fitness heuristic; keeping `KNOWN_NULL_TESTS` + `modelled` beside the structure guard; parsing the boolean tree properly; accepting green as verification.](0216-a-fake-pool-interpreter-must-fail-loudly-not-silently-ignore.md) | testing / proof integrity |
306
+ | 0217 | [**Rank gates INVITING, not VIEWING: the owner’s answers to the account-security review’s five blockers** ([task 1003358](https://cloudbongos.com/builders#/task/1003358) · goal 1000045; answers blockers 1000121/1000122/1000123/1000124/1000126). The 2026-08-29 account-security batch shipped fourteen tasks and deliberately refused five audience/consent questions. Four turned out to be the same question, and one answer reframed the rest. Decisions: **D1** the governing line — *rank gates the ability to INVITE; it does not gate the ability to VIEW*. Seeing what someone built across projects is not privileged (the T&C already promises that data is public by consent, to search engines included); reaching out to them costs them something, and that is where a floor belongs. So `membership_kind` is **NOT** widened — `membershipKind()` stays FIRST_ADMIN-only, the federated wire contract is unchanged, the My Projects owned/building split keeps its meaning, the cross-version window never opens, and task 1003350 is **abandoned**. A **per-project INVITE rank floor defaulting to archon** replaces it (task 1003362), set by that project’s own archons — strictly better than a founder requesting a platform-side `project.curate` grant per teammate. Viewing gains no gate, on a new cross-project achievements page that per the owner **lives on cloudbongos.com, not the hall variant** (task 1003363) — so `public-landing/` inside the apex-only region, not `modules/hall-ui/`. The risk to hold as it is built: public consented profile data and a PRIVATE account’s recruiter sliver are different material, and the sliver must not ride along — not as a row, a facet count, or a search-result absence that reveals presence. **D2** the sliver keeps its **archon floor** (blocker 1000126), and ADR 0215 D4’s defensive hold becomes a positive agreement: the invite floor also defaults to archon, and the C5 tension dissolves because C5 was about browsing. Whoever builds 1003362 must check whether it IS the console wave’s `project.recruit` atom before shipping a rival knob. **D3** the consent pair — `/terms` said acceptance is recorded “by signing in here” but the stamp is reachable only via `/sso/authorize`, which a CHILD PROJECT reaches, so a hub-only builder was never stamped and (NULL `terms_accepted_at` → `isAccountActive` false) was invisible everywhere, permanently, under no-backfill. Owner took **both halves**: correct the sentence now (task 1003353) and **capture at the hub’s own sign-in** next (task 1003361), through the `platform-identity.hub` seam so child instances see no change. On grandfathering the owner chose **B — grandfather** (task 1003360) against the recommendation, having been shown that it records a consent which did not happen; the ledger does not absorb that silently, so a grandfathered acceptance is **marked** (queryably distinct from a sign-in, via a recorded source not a magic timestamp) and `/terms` **says so**. ADR 0213’s honest property is not “every acceptance was a sign-in” but **the record never claims more than what happened**, and that survives. **D4** `/access-requests/status` **collapses** to `admitted` + one `status` of `member`|`invited`|`not_admitted` (task 1003359, the fix ADR 0209 already named). It does **not** close the oracle and must not claim to — `admitted:true` still means member-or-invited; the 1003339 limiter is what reduces enumeration, and collapsing narrows what leaks about **applicants**. The cost is paid in the same change: `login.js` is the ONLY place telling a declined applicant they were declined, so its 30-minute timeout copy is rewritten to be honest rather than promising an approval that is never coming. ⚠ Also: report 1000027 (the live SSO check-in identity hole) marked **fixed**, held open until now so the ledger would not claim a fix while the hole still served; `verifyHubAssertion`’s purpose-confusion weakness filed as report **1000028** (medium, latent — zero call sites, becomes an admission bypass the moment ADR 0201 D2.1’s vouch is minted, so the fix must land BEFORE the feature that arms it). **New invariant:** any gate added to a viewing surface, or any recruiting/invite surface below the archon floor, contradicts this ADR and needs one that supersedes it.](0217-rank-gates-inviting-not-viewing.md) | platform identity / privacy / permissions |
307
+ | 0218 | [**The apex-root web surface: a module may own the front door, and says so with a flag, not an empty prefix** ([task 1003322](https://cloudbongos.com/builders#/task/1003322) · goal 1000080 — the last of the five tasks scoped with the owner on 2026-08-28). Carves the repo-root `public-landing/` into `modules/public-landing/`, the THIRD web-surface module after status-UI (BV1.R84) and hall-UI (BV1.R85) and the first that is not a subdomain. The obstacle: `moduleWebSurfaces()`'s whole routing vocabulary was a Host-header PREFIX, and the apex root is not a prefix of anything — it is whatever is left once the subdomains are taken. So a surface now names where it answers in exactly one of two ways, `{ host: "status.", dir }` or `{ apexRoot: true, dir }`, never both and never neither. **`host: ""` is rejected on purpose:** the routers match with `host.startsWith(s.host)` and every string starts with `""`, so an empty prefix would answer on `builders.`/`status.` too AND make `isModuleSurfaceHost()` true for every request — the predicate eleven `/builders` guards use to step aside — silently un-serving the hall. A boolean cannot be mistaken for a prefix. The landing keeps its DEDICATED serving block (the hall precedent): the apex-only strip, the branding stamp, the `?v=` content stamp and the six routes are untouched; only `LANDING_DIR` changes, from a hardcoded path to a lookup through the loader, resolved by the `apexRoot` PREDICATE so neither `platform-server.js` nor `serve-internal.js` names a module key (ADR 0083 §Decision #4). Disabling the module removes the front door outright and the apex answers the game-less 404 tail — the module contract's "disable without breaking anything" in its most literal form. At most one apex-root surface: the first discovered wins and a second is logged. Also: the BV1.R76 host-identity gate learned that an **apex-only region is exempt** — the server deletes it on every non-hub instance, so it cannot make a child render the hub's brand, which is the failure that gate names. Evidence: the five apex pages hashed as served, before and after, differ by exactly the same 22 bytes — `"public-landing":true,`, the module's own `window.__MODULES__` entry — and are otherwise identical; the 106 moved files hash identically on disk.](0218-the-apex-root-web-surface.md) |
308
+ | 0219 | [**A look is a branding pack: the style library ships looks as `theme.ui` overlays, never as sheets** ([task 1003323](https://cloudbongos.com/builders#/task/1003323) · goal 1000083). The owner scoped "varying ui styles" for the ui-design module; the shape was decided by three facts: every served sheet already reads only the contract and is measured by a suite that resolves its own expressions under the pack (ADR 0196), the pack is one-mode (light values; dark is each sheet's black tier), and the world round's candidate token layers were sheets, not packs. Decisions: **D1** a look is `modules/ui-design/styles/<name>/` = `pack.json` (the `theme.ui` overlay, exactly the thirteen colours + the two font stacks and their links) + `DESIGN.md` (the derived recipes in the root DESIGN.md's frontmatter shape, thirteen in lockstep with the pack) + `mock.html` (a specimen with no `:root`, served through the stub with the pack spliced in), never a fork of a sheet; a sixteenth token is a contract change. **D2** `chrome-world` is canonical and pinned byte-for-byte to the neutral pack; its DESIGN.md points at the root one. **D3** the alternates `expedition` (natgeo-editorial) and `grove` (organic-cards) are ported by retuning the thirteen against the three sheets' own pair lists in both modes until every pair clears AA (the grove's sage lifted and its inks deepened for the hub's status vocabulary and the hall's chips); dark under both is the sheets' black. **D4** the picker rides `GDS_BRANDING_FILE`, the fixed knob `src/branding.js` already has: `STYLE=<name>` on the stub, `--style` on render/probe, `--pack <name>` on contrast/check-mock; no core change. **D5** the three token suites take `UI_DESIGN_PACK` and their drift tests keep measuring against the neutral pack; `tests/ui_design_styles.mjs` runs them once per alternate and proves the knob live with a pack that must fail. **D6** an instance adopts a look by copying `theme` into `config/branding.json`; what stays the platform's whichever look is on: the dark ground, the hall's verification vocabulary, every sheet's derived tier, the floors. Rejected: sheets per look beside world.css; a dark tier in the contract; a picker that writes the host boundary.](0219-a-look-is-a-branding-pack-the-style-library.md) | ui-design / branding contract |
309
+ | 0220 | [**An in-house rebuilt skill is a first-party skill: it lands at `skills/<name>/`, opens with the same preamble, carries a "Rebuilt from" section instead of a sidecar, and is counted** ([task 1003325](https://cloudbongos.com/builders#/task/1003325) · goal 1000083). ADR 0198 landed both scanned origins as rebuild specs and left the roster empty; this is the first rebuild — the taste core: `design-taste-frontend`, `design-taste-frontend-v1`, `high-end-visual-design`, `redesign-existing-projects` — and the policy had not said where a rebuild lives, what replaces `PROVENANCE.md`, whether a preamble written for vendored copies still fits, or whether five byte-identical copies of it trip the doc-entropy ratchet (they would have: baseline 6). Decisions: **D1** a rebuild is `modules/ui-design/skills/<name>/SKILL.md`, declared in `module.json`, materialised by step 1b's own-dir branch (no sidecar gate), first-party and counted by every ratchet — no `rebuilt/` exemption like `vendor/`, because these are our lines. **D2** one preamble worded for both origins ("vendored from a scanned origin, or rebuilt in-house from a spec"), still byte-identical everywhere; a rebuild's provenance is its `## Rebuilt from` section first after the block: the origin the SPEC was derived from (documentation only), the verdict, the spec task, "no upstream bytes consulted or copied", this ADR, and what changed in translation; no `PROVENANCE.md`, no `LICENSE`. **D3** the translation rules for every rebuilt taste skill: the palette is the pack and variants come from the style library (ADR 0219); the design system is the instance's `DESIGN.md`; imagery is hero plates and hero objects by token and never a step; every motion rule lives inside the Kill Switch (springs, magnetic hover, pointer-tracked borders are named *not available*); the upstream's bans are defaults an instance's `DESIGN.md` overrides, and where the world calls for what an audit would flag, the world is the answer key. **D4** `docs-entropy.js` skips a paragraph opening with the preamble marker beside its generated-block skip, calibrated against a plain duplicate that is still flagged. **D5** v1 is a distinct skill differing in content (the fixed triple, the stack conventions, the pattern vocabulary, the tile-grid archetypes), not a version stamp. **D6** the pins in `tests/ui_design_skills.mjs`: frontmatter name = dir, preamble first and once, the section's task link + ADR 0198 + "no upstream bytes", the style library and "hero plates" named, no image mandate, no sidecar, every door lists the four. Rejected: a sidecar with the spec's commit; a second preamble; a `rebuilt/` segment; vendoring anyway (an owner override, not taken); folding v1 into v2; raising the doc baseline.](0220-an-in-house-rebuilt-skill-is-a-first-party-skill.md) | ui-design / skills |
310
+ | 0221 | [**The impeccable method is one skill with sub-command playbooks: one preamble, one playbook per request, the three homes are the instance's own files, and the origin's mechanisms are dropped rather than disabled** ([task 1003326](https://cloudbongos.com/builders#/task/1003326) · goal 1000083). `pbakaus/impeccable` scanned `dangerous` (ADR 0198: hooks written into five harness configs, a child agent with permissions bypassed, an instruction override, a live-server credential in page source, an unverified remote bundle, default-on telemetry), so its method is rebuilt from the spec. ADR 0220 fixed where a rebuild lives; it had not said how a family of sub-commands lands, what the origin's three persisted homes become, or what to do with the six spec areas whose substance is the condemned mechanism. Decisions: **D1** one skill at `skills/impeccable/`: `SKILL.md` (invocation, the menu from live signals, session setup, the three homes, the routing table, the craft floor, the bounded finish, what is not here) plus `reference/<sub-command>.md`, eighteen playbooks, exactly one loaded per request (the spec's own entry rule); the preamble pinned once in `SKILL.md` and governing every playbook; the materialiser's tree copy ships `reference/` unchanged. **D2** the three homes are the instance's files (product truth = `docs/project-context.template.md` + the pack's identity; the visual system = the root `DESIGN.md` and a look's; the brief = the task's body + a fixed brief block in the ship notes); the skill creates none, `shape` proposes diffs, `document` drafts on the claim's branch with the refresh / overwrite / merge choice. **D3** dropped, not disabled, each with a destination: hooks → none (the ship gate is the sweep, the pre-flight is the kit); the bypass agent → an ordinary read-only subagent via the Agent tool; telemetry, the remote bundle, the credential → none; the live loop → the playwright plugin driving the kit's stub, variants as states, accept = commit, discard = checkout, nothing injected; the detector → task 1003329; the asset producer → the hero-plates rule and task 1003328; native → declines to `/design`; `init`/`doctor`/`pin`/installer → the materialiser is the install. **D4** the finish is the kit in bounded rounds (build, one batched inspection, one batch of fixes, at most one confirming round, stop, one budget) and the reviewer answers with one of `clean` / `recapture` / `defects` / `contradicted` after an evidence check and its own mock inventory. **D5** the craft floor is mechanics loaded late and never recited, translated for a world that may be hairlines by rule. **D6** the pins: sixteen routed playbooks, no orphan, no preamble copy in a playbook, and nothing in the tree directing a bypass, a hook write, a settings edit, telemetry, a remote download, a credential or an override. Rejected: eighteen skills; one inline file; a hooks-lite opt-in; an elevated reviewer; a `doctor`; the native playbooks.](0221-the-impeccable-method-is-one-skill-with-sub-command-playbooks.md) | ui-design / skills |
311
+ | 0222 | [**The image family under the hero-only rule: plates and hero objects, never screens; the mark is drawn; the board is a contact sheet; the pair and the sidecar are the output contract** ([task 1003328](https://cloudbongos.com/builders#/task/1003328) · goal 1000083). The four image skills of `Leonxlnx/taste-skill` (scanned `dangerous`, ADR 0198) are built around pictures of interfaces: one reference image per section, one app screen per flow step in a phone mockup, an identity board with the brand name rendered in, and a build loop whose truth is the generated image; the panel flagged the eagerness mandate, the image-count rule and the must-invoke-any-tool directive by name, and the owner's rule (2026-08-26/27) forbids the category. Decisions: **D1** the subject is the composition's plate slots and the count is the slot count; variety survives as anchors (field, beside-bleeding, masthead, orb, backdrop; text-left image-right refused), the palette lock is the pack, cross-frame consistency is one family (material, light, lens, ground). **D2** a screen is the surface itself: the mobile skill makes portrait plates for 390 / 320, the flow is the states file rendered by the kit, the bible is `DESIGN.md`, no phone mockup, native declines to `/design`. **D3** the source of truth for UI is the owner's references and the world; `image-to-code` reads them (analysis, extraction spec, faithful translation, drift as the failure mode, the clarity checklist) and the only image it may cause is the hero plate. **D4** the mark is drawn as inline SVG, the colour system is the pack, the type specimen is the faces on a real page, the applications are the kit's renders, the image direction is a hero-object family, the nine-panel board is a contact sheet of real parts. **D5** the output contract is DESIGN.md's pair and sidecar (JPEG dark plate, WebP alpha cutout, `<file>.<ext>.json` beside each, `--plate-*` tokens on `body`) through the instance's art pipeline key, the spend logged, a white ground through the remover, alpha verified with ffmpeg, never a crop. **D6** the worked example is the grove specimen's hero object, not the front door. **D7** the pins: the four declared, each naming the sidecar, "text-free" and the art key; no eagerness / count / per-section / must-invoke directive. Rejected: per-section images for reference only; phone mockups around real shots; a generated logo as a start; the plate on the front door; a shared base skill with wrappers.](0222-the-image-family-under-the-hero-only-rule.md) | ui-design / skills / imagery |
312
312
  | 0223 | [**The anti-pattern detector is two tiers inside the kit: mechanical floors fail, craft tells warn, a false positive is waived in the file with its reason, and nothing new is installed** ([task 1003329](https://cloudbongos.com/builders#/task/1003329) · goal 1000083). The [detector] group of impeccable's rebuild spec (about sixty rules, a CLI plus a browser bundle wired to hooks, per-project ignores, in-file waivers, "a clean scan is a floor and never proof") lands as RULES in the kit that already exists, per the task body: no second tool, no bundle, no hook. Decisions: **D1** `kit/tells.js` carries the static rules and `check-mock.js` runs them after the platform rules and taste bans; `lib.js`'s `factsInPage` collects the computed-value twins as `tells` and `render.js` prints them as `~ tell:` lines per render, recorded in `report.json`, never counted as problems. Two tiers: floors FAIL (a bounce easing; a card nested in a card) and tells WARN (the icon-heading-text grid, hero metrics, section numbering, a kicker above the h1, a side stripe, a hard offset shadow, a coloured halo, decorative blur, a default typeface, a wide measure, a long headline, the same entrance everywhere, a geometric mask on a plate, glyphs as icons, a rainbow gradient, the browser surfaces nobody themed, proportional numerals, tiny type; rendered: measure, tiny text, numerals in cells, headline lines, an overshooting curve). **D2** the world decides which tells are floors: a kicker above the h1 is a FAIL where the instance's `DESIGN.md` bans the eyebrow (a new `eyebrow` taste key), a WARN elsewhere. **D3** a false positive is waived in the file with its reason (`kit-ignore: <rule prefix> (reason)` → a WAIVE row), `--raw` runs with none, `--no-tells` runs the platform rules only, `--json` prints rows with `fails` / `warns` counts; no per-project ignore config. **D4** not ported: the purple-to-blue tell (a literal colour already fails; hue is unresolvable under the fifteen), an injected overlay, the screenshot sampler (`contrast.js --url` is that pass), a cascade resolver (the browser is the resolver), native code, every hook. **D5** the pins: a clean fixture passes every rule, every rule class trips at its tier with the case count equal to the rule count, the promotion, the waiver / raw / json paths through the real CLI, `craftTells` as warnings; the hub mock and the three specimens still pass, `/projects` and the grove specimen still render ALL CLEAN. Rejected: a second CLI; every tell as a floor (the kit as the taste and the instance as the exception); a project-wide ignore config; an edit-time hook.](0223-the-detector-is-two-tiers-in-the-kit.md) | ui-design / kit |
313
- | 0224 | [**Module-owned skills reach the core checkout as untracked, self-ignored copies with a manifest: the self-copy guard stays, the dev path is a flag, and the file map skips what the module documents** ([task 1003330](https://cloudbongos.com/builders#/task/1003330) · goal 1000083). `materializeClaude`'s self-copy guard is right (a core copying itself is a no-op at best), but Claude Code discovers only `.claude/skills/` at the project root, so a builder IN the core checkout could not invoke any of the nine ui-design skills written there. Decisions: **D1** `node scripts/gds/claude-materialize.js --module-skills-only [--dry-run] [--clean]` calls `materializeModuleSkillsIntoCore`: the same roster, gate and slug rule as step 1b, verbatim copies (no bongosify: the checkout runs `node scripts/gds/…`), a core-owned name kept, a disabled module's excluded; the guard untouched. **D2** untracked and self-ignored: a generated `.claude/skills/.module-skills.json` manifest and a `.gitignore` that ignores itself, the manifest and every materialised dir; each run removes what the last manifest named then lands the current set (byte-identical on repeat; a dropped declaration disappears); `--clean` stops after the removal. **D3** `gen-file-map.js` and `tests/file_map.mjs` skip materialised names by the manifest (a module skill is documented by its module's README, not by the notes sidecar). **D4** init from a checkout that carries the copies still lands them through step 1b, transformed, and never the dotfiles. **D5** pinned end to end in `tests/claude_materialize.mjs`. Rejected: dropping the guard; committing the copies; symlinks or junctions; notes for dirs not in the repository.](<redacted>.md) | materialiser / skills |
314
- | 0225 | [**Privacy suppresses cross-project surfaces only: a private builder still appears on their own project's roster** ([task 1003443](https://cloudbongos.com/builders#/task/1003443) · goal 1000075). The accepted privacy spec defined its two flags inconsistently for one case: `hide_stats` suppresses "everything derived from the CROSS-PROJECT rollup" (qualified, so an in-project leaderboard is out of scope) while `account_visibility: private` said "absent from search, LEADERBOARDS, sitemap" (unqualified, so it read as covering the in-project one too) — and PV2 demands "ONE shared predicate used by every public read: … leaderboards …" while `modules/economy/credits.js`'s `leaderboard()` is a bare `SELECT … FROM builders` with no join and no predicate. Read one way that is an unshipped PV2 violation in main; read the other it is correct, and nothing settled which. **Owner decision (2026-08-30): privacy suppresses CROSS-PROJECT surfaces only.** A `private` builder is absent from `/u/<handle>`, platform search, the sitemap, Explore and the recruiting reach, and still appears on the roster of a project they build on — a project's record of its own work is the project's business. Two consequences are the point of recording it: `credits.leaderboard()` is **correct as written** (adding a predicate there would be a regression, not a fix — exactly what a literal PV2 reading would have done), and PV2's "leaderboards" means the cross-project ones. The spec was amended rather than annotated: the D1 row and both `active · public` matrix rows now say *cross-project*, the two `active · private` rows drop "absent everywhere" (the file's most misreadable phrase) for "absent from every cross-project surface", PV2's ship-table row is qualified, and the boundary lands as the **third rule** under D2 beside "`hide_stats` beats everything". Rejected: making `private` hide a builder from projects they build on (it reads as more protective and breaks the ledger — a roster that omits contributors cannot show who built the thing, and credits would accrue to someone invisible on the surface explaining them); and leaving it as a dated spec note (it pins shipped code as deliberately predicate-free, which is an ADR's job).](<redacted>.md) | privacy / spec |
315
- | 0227 | [**A task network map is refused; the goal graph is the map, and search answers findability** ([task 1003282](https://cloudbongos.com/builders#/task/1003282) · goal 1000072, criterion `legibility-project-map`, which gates itself on this spike). Measured across a swept density range (p=2%–40% of tasks carrying a dependency): a node-link view of ~1,357 tasks is confetti, not structure, and densifying it draws forty goals badly while claiming to show a thousand tasks — a confident picture of nothing is the opposite of the legibility this goal serves. Decision: NO whole-corpus task map at any density; the readable surface is the ~40-node GOAL graph as the entry point with a per-goal DEPENDENCY FOREST as the drill-down, queried when a goal is opened rather than zoomed out of a global map. Every edge a map could draw already exists in the DB and none needs a new table — `task_dependencies` (020), `tasks.parent_task_id`, `tasks.goal_id`, `task_criteria`→`done_when_criteria`, `idea_inbox.promoted_to_task_id`, `idea_inbox.merged_into_idea_id` (`core_195`), `tasks.version_id` (membership, never an edge). **Findability (goal 1000027) answers the owner’s question more cheaply than a map**: the ask splits into “find what I was brainstorming”, which `/recall` (ADR 0060) plus the two lineage columns already answers end to end and which wants a text LINEAGE TRAIL — one thing and its ancestry — not a picture; and “see the shape of the portfolio”, the only half a graph earns, which is about ~40 goals and not about tasks. So the trail outranks the map and is sequenced first. Accessibility: the TABLE is the primary surface and the graph the enhancement (not an `aria-label` on an SVG blob, not a toggle hiding the accessible path), no sideways scroll on the page (the graph scrolls inside its own container), and category is text as well as colour since the ADR 0192 set is not colour-distinguishable at forty nodes. Ranked list: (1) measure the real density against the live DB — it GATES and may cancel the rest, (2) the lineage trail, (3) the goal graph, (4) the per-goal forest. Rejected: view A (every task); view D (densified); a prototype as the output (the criterion forbade it — `spike-task-map.js` is a measuring instrument under `docs/research/`, wired to no route); and building before measuring. Honest limit: the corpus is SYNTHETIC (the live DB was unreachable), so the verdict is robust across the sweep but the specific numbers are model output — which is why item 1 is a measurement against the real DB.](<redacted>.md) | GDS / legibility + hall |
316
- | 0228 | [**The reading room owes a reference its content rules, not a second archetype** ([task 1003473](https://cloudbongos.com/builders#/task/1003473) · goal 1000081). The archetype task 1003306 built for the primer — a linear read with a gate — now serves drachmae, a REFERENCE people open to answer one question about how they get paid. Decision: the same room with ONE body modifier, `room--reference`, switching on content rules in `room.css` §8 rather than a second sheet or a `.page-head--reference` voice: the linear-read instruments are off (no progress hairline, no section counters, no read time; `primer.css`, which holds only the acknowledge gate, is no longer loaded), the outline goes to h3 depth read from the rendered DOM and lights the entry and its part, every h2/h3/h4 is an always-visible address (`.room-anchor`) that copies its link, a deep link is landed by the page after the document, the live section and the fonts (a swap moved the browser's own landing 300px, and `:target` had resolved against nothing), the collapsed phone outline rides under the top bar naming the part, and the live bounty table is a section ahead of the closing links rather than an appendix after them. Found by rendering before touching anything and fixed in the shared reader: `md-reader.js` stopped at `###`, so the doc's six earning sources rendered as paragraphs with a literal `####`; the bold rule refused an inner `*`; a linked heading slugged its target's path into the address. Rejected: a second archetype sheet, hover-only anchors (a phone has no hover), an accordion outline (the sub-questions are what a reader scans for), placing the live table inside the prose (it would lose its outline entry), and teaching the kit stub to serve markdown (the ui-design module's). Pinned by `tests/hall_reference_world.mjs`, four of whose tests run the reader in a vm.](<redacted>.md) | hall / design |
317
- | 0229 | [**The title is the door into a task record, and the record exits to where it was opened from** ([task 1003468](https://cloudbongos.com/builders#/task/1003468) · goal 1000081). The owner, working the live hall: a to-do card "just gets selected", and a record opened from a goal "only offers the tasks page". Both were shipped behaviour: tasks 1759/1869 made multi-select the primary card interaction and left a 26px `--ink-faint` ⤢ glyph as the only way from a claimable card into the record, and `task.js` hardcoded `Open the work board` in three renders with `task.html`'s crumb a fourth. Decision: `.task__title` carries a real link (`.task__link`) on both surfaces that render the card — the convention `.gr-title` and the ledger rows already use — bought up to the 24px floor with the inline-block + padding + equal-negative-margin recipe so the card does not re-space; the card body still toggles selection and bulk claim is untouched; the glyph goes (a second door to the same room, one the owner never found). The link carries WHERE it was opened from as `?from=<path+query+hash>` (`OTBTaskDetail.href`), a query param because the origin must survive a reload and a copied link and because a referrer drops the `#/goal/N` fragment; `task.js` resolves the exit once — `?from=` → same-origin referrer → the work board — points the crumb before the fetch so the way back exists in every state, and labels it the way the sidebar does. A value is honoured only as a path on this host: one leading slash, no backslash, no whitespace, never `/api/`, never this same record. Rejected: opening on the card body (deletes bulk claim), keeping the glyph beside the link, in-memory origin, referrer-first, `history.back()`. Pinned by seven vm tests in `tests/hall_record_world.mjs` §10 and four in `tests/hall_board_world.mjs` §7.](<redacted>.md) | hall / design |
318
- | 0230 | [**A look skill is a composition grammar; the palette is the pack and the texture is a library entry** ([task 1003327](https://cloudbongos.com/builders#/task/1003327) · goal 1000083, criterion `uidesign-skills-in-house`). Four groups of the taste-skill rebuild spec ship a *look*, not a method, and upstream each one shipped a palette and a set of faces — which is the one thing the fifteen-token contract forbids (ADR 0196, 0219). Decisions: (1) a look skill ships **structure, rhythm, type roles, component anatomy, motion and bans, and no colours** — every colour a `var()` or `color-mix()` of the fifteen, the palette the instance's pack, the texture a named entry in `modules/ui-design/styles/`, and no skill adds a face. (2) The brutalist "one substrate per project" becomes an **archetype** deciding which mode the surface is composed *for*, while the other mode ships complete and measured; "never mixed in one interface" is the part that was about craft. (3) `gpt-taste`'s prompt-seeded **random** selection becomes a deterministic one derived from the route and bumped against sibling surfaces, written into a plan block before any markup with the hero arithmetic, the grid-density proof and the measured contrast pairs — a roll nobody can reproduce is not a decision. (4) `stitch-design-taste` **transcribes** the world instead of authoring one: the platform's own `DESIGN.md` frontmatter shape, the thirteen diffed from the pack rather than retyped, and its universal bans stated as defaults the instance overrules. (5) The upstream's seeded placeholder photography is **dropped, not softened** (ADR 0222); (6) motion is CSS inside the Kill Switch and what cannot live there is named *not available*; (7) the pre-flight is the kit and `tells.js` is its mechanical half, so a look skill adds no reviewer of its own. Rejected: shipping each look as a pack, a sixteenth token for a substrate, a fixed seed behind the same roll.](<redacted>.md) | ui-design / skills |
319
- | 0231 | [**The `/style` session: an owner authors a look inside the contract, and a module skill declares one of two origins** ([task 1003475](https://cloudbongos.com/builders#/task/1003475) · goal 1000083, criterion `uidesign-skills-in-house`). The library ships three looks to choose from; the owner's ask was to *make* one. A look is a small, strict object (thirteen colours, two font stacks, a `DESIGN.md`, one specimen), so what was missing was the loop between the ends. Decisions: (1) `/style` is a **bounded four-step session** - SEARCH, CREATE, PROVE, ADOPT - and nothing else. (2) SEARCH **renders before it asks**: the nearest two entries shown through the kit (`/` and the hall's light tier), then two or three questions about *feel*, the light-only fact of a pack stated up front, and adapt-or-draft declared in one sentence before a file is written. (3) CREATE stays inside the contract or refuses - never a fork of a sheet, **never a sixteenth token** (a radius, a shadow, a glass are a sheet's derived tier; a look that needs one is an ADR). (4) PROVE is **arithmetic, not taste**: the pack floor, the three token suites under the pack, `check-mock`, the renders, with a failure-to-fix table so the retune is a lookup, and the four green lines pasted as evidence. (5) ADOPT hands the owner one step and **never writes `config/branding.json`** - the host boundary is theirs (ADR 0062 §7) - but does file the catalogue rows under the claimed task. (6) A module skill declares **exactly one of two origins**, `## Rebuilt from` (ADR 0198 / 0220) or `## First party` (no upstream; the authoring task, this ADR, and why it exists), and the pins branch on which - a fabricated spec citation would destroy the provenance the section exists for. (7) The session's first output is its own proof: `blueprint`, the library's first cool look, cleared the pack floor and all three suites on the first measurement. Rejected: a fabricated Rebuilt-from, a session that repaints the live instance, filing the catalogue rows "later", a palette generated from a reference image.](<redacted>.md) | ui-design / skills |
320
- | 0226 | [**One stale-claim timer; reporting degrades where enforcement refuses** ([task 1003476](https://cloudbongos.com/builders#/task/1003476), goal 1000072, criterion `legibility-sort-and-staleness`). "How long may a claim go silent" had FOUR answers and none of them was the project's: the sweeper that actually releases a claim read `GDS_STALE_CLAIM_HOURS` (else 6h); `GET /builders/roster` and `GET /builders/:id/active-claims` each hardcoded 24h in byte-identical blocks; and the two `session_pulses` helpers defaulted to 30 minutes. The 6-vs-24 pair is the harm: the roster is the screen an Archon opens to decide whether to intervene, and for **eighteen hours** after the sweeper had released a claim, stamped its session log `[auto-released:stale]` and returned the task to `ready`, that screen still showed it healthy — while the roster's own comment claimed to "mirror" the sweeper, true of the definition and false of the number. Decision: ONE resolver, `src/stale-timer.js` (a KERNEL file, the `instance-config.js`/`branding.js` category — host config a module reads, never hardcodes), reached by modules through the doorway and by core routes and the CLI directly. Precedence: explicit override (`--threshold-hours`, `?stale_hours=` — a LENS, labelled `query`, never a reconfiguration) → `<PREFIX>_STALE_CLAIM_HOURS` via `instance-config.resolveEnv`, whose legacy prefix chain still honours the live `GDS_STALE_CLAIM_HOURS` so **nothing in a running deployment has to change** (and which is the channel ADR 0190's runner already patches into `web.env`) → **6**, the SWEEP's historical number and deliberately not the roster's 24, because the sweep is the surface that acts and unifying on 24 would have made every project three times more patient with a dead claim under cover of a refactor. Bound 1..720h — the range `/builders/roster` has published since V3.R14; one timer, one bound. Every surface that prints the timer prints its PROVENANCE (`stale_hours_source`, the sweep header, the auto-release note): a number without it is exactly what let 6 and 24 coexist unnoticed. **The fail posture is asymmetric and that is the decision**: the resolver reports an unusable value rather than throwing, then a surface that only READS degrades to the default and names the bad var (500-ing the roster an Archon opened to find the problem would hide the problem), while the SWEEP refuses to run at all (it releases other people's claims and stamps their logs; no reload undoes that) — a surface that only reads may guess and say so, a surface that acts may not guess. Both routes now share one `staleHoursFor(req, res)`; two copies of a literal is how the drift happened twice inside one file. Rejected: a new `config/stale.json` tier (the env route already IS the per-project channel); making 24 the shared default; throwing from the resolver (forces a second reader — the original bug); folding in the boards' 14/30/60-DAY task buckets (a different question — days, about a task, a browsing lens with no enforcement behind it); a DB row + settings UI (a reasonable next step, but the defect was four numbers, not a missing knob — a row added before the readers agreed would have been a fifth).](<redacted>.md) | GDS / claims + ops |
321
- | 0232 | [**Rot is derived, not swept; and `water` is the only new verb** ([task 1003279](https://cloudbongos.com/builders#/task/1003279) · goal 1000072, criterion `legibility-sort-and-staleness`). The inbox half [ADR 0226](<redacted>.md) split out and left open. Owner decision (2026-08-25): a goal or task past the timer raises a **needs-attention line** — not an automatic status change (it mutates real work with no human in the loop, and a wrong timer then silently freezes it) and not a nightly digest (a notification is not an action, and one you scroll past is indistinguishable from the silent decay it was meant to fix). The build note asked for an idempotent poller; the decision is that **there is no poller and no items table** — a line is a `SELECT` comparing the thing's rot clock to the timer, so "exactly one item per stale thing, not one per sweep" is true by arithmetic rather than by deduplication code that must be written correctly and then stay correct, and clearing falls out for free (an edit moves `updated_at`, a decision moves `status`, watering moves the mark). A stored item would have been a cache of a query whose inputs it could not observe. **The exclusions are the load-bearing part**: rot is work nobody picked up (`backlog`/`ready`, and `open` goals), and `active` is deliberately excluded because the claim-silence timer already reports it in hours — listing it here in days would put one rot in two inboxes under two numbers, the 6h-vs-24h split one tier up; `blocked`, `completed`/`confirmed` and the terminal states are excluded for the same reason, each naming the surface that owns it. The clock is **not a new definition of touched** — it is `task-lib.js`'s published `max(updated_at, shipped_at)` (task 1003278) plus a water mark, pinned by a test asserting the domain expression appears verbatim in migration `core_231`'s index expressions, because drift there is silent. The timer is a **second** timer and defends itself as a different question (days vs hours, DB vs env, asks a human vs releases a claim), stored in a new unseeded generic `project_settings` table — no row means "never configured", which is what lets a settings page tell that apart from "set to the default" — with ADR 0226's precedence and its reporting-degrades fail posture, and an unreachable DB explicitly not treated as a bad value. Three of the four verbs (**prioritise/prune/kill**) reuse `promote`/`demote`/`abandon`/`archive`, the task-1003066 precedent, so they add no permission; only `water` is new, gated on ownership OR `builder.roster.read` inside the handler (a creator holding no atom must still get through). Verbs are **served with each row** rather than hardcoded, since a goal legitimately gets two where a task gets four. Rejected: an items table, a sweep, a digest, auto-demotion, and a fourth verb on goals that would render a dead button.](<redacted>.md) | GDS / legibility + hall |
322
-
323
- | 0233 | [**A copy proposal is a task carrying a patch, and the applier is the only writer** ([task 1003114](https://cloudbongos.com/builders#/task/1003114) · goal 1000074, criterion `artist-edits-land-as-tasks`). Goal 1000074 asks for two things that pull against each other in its own text: the artist must change copy WITHOUT an engineer, and there must be NO LIVE CMS because every copy change is a claimed, graded, shipped task. R02 shipped the half that needs no reconciliation (the flag) and [ADR 0178](<redacted>.md) §3 pinned the absence of any column that could hold replacement text, naming this task as the one that would have to keep it true. Decisions: (1) the shape is **borrowed from the design half, not invented** — ADR 0081 already has a tool authoring and an `adapter.export()` landing the output as a committed diff under a claim inside a path guard, so the artist is the tool, the copy desk is where they author, and the proposal is the export. (2) A proposal is a **TASK carrying a fenced `copy-proposal` patch**, never a row: the ledger is where a proposal belongs anyway, nothing renders a task description as product copy, a reviewer can read the change without running anything, and `copy-desk` gains NO migration (the obvious-but-wrong R03 is a `copy_desk_proposals` table, which is the forbidden column with extra steps). The module gains `consumes: ["lifecycle"]` and calls `createTask` through the port — it can put a request INTO the ledger and nothing can pull copy OUT of it. (3) `scripts/gds/copy-apply.js` **re-derives the position** rather than trusting the patch, which required teaching R01s extractor to record a source SPAN and deliberately dropping it from the committed registry: an offset goes stale the moment anything above it changes, while the id is designed to survive exactly that movement, so the span is recomputed at the moment of the edit by the same code that decided the string was copy. (4) The **placeholder arithmetic** is what makes a patch mechanical rather than a note for an engineer — the applier puts the sources own `${expr}` back, k-th hole to k-th expression, so an artist may MOVE a value within a sentence but never add or drop one, checked at proposal time and again against the real source. (5) Every ambiguity is a **named refusal** (`target_gone`, `ambiguous_target`, `no_exact_span`, `placeholder_mismatch`, `path_outside_surface` — the design adapters path guard, derived from R01s own SURFACES), because a wrong edit is worse than no edit; after writing, the applier regenerates the registry and checks that what it wrote is what the inventory now reads. (6) Proposing is **open to every rank** (an artist is a craft, not a rank) and lands at `backlog`, so a proposal is a request that passes ordinary triage. Rejected: a proposals table, a column beside the flag, trusting the patchs line number, an escape-aware index map to buy spans for nine strings, auto-closing the flag a proposal answers.](<redacted>.md) | copy-desk / the artist loop |
324
- | 0234 | [**Idea routing: capture-time promotion, the landing matrix, and the homeless-only inbox** ([task 1003064](https://cloudbongos.com/builders#/task/1003064) · goal 1000071, "Idea routing"). Idea 1000771 named the failure: Discord `#bugs` created tasks DIRECTLY against a hardcoded, already-shipped version — no `captured_by`, no lineage, so the ideator-credit lane had nothing to pay against — while every other open idea sat waiting for a human triage pass that dropped its promotion into a version catch-all, because nothing in capture ever named a goal. Records what R01/R02/R05/R07/R13 shipped. `captureIdea` (`modules/ideas/inbox.js`) stays the ONE capture chokepoint — routing is one optional `goalId` argument, never a second creation door — and when the matrix routes, `routeIdeaToGoal` writes the idea row, the task (via the `lifecycle.createTask` port) and an `ideas_task_lineage` row (`origin='route'`) in ONE transaction, so a route can never mint attribution-free work the way `#bugs` did. `modules/ideas/routing.js`'s `decideLanding` is PURE and fail-closed: a bug from a Metic+ filer hard-lands `ready`, a sub-Metic bug or any other kind with a goal soft-lands `backlog`, a Full Idea or `criterion-proposal`/`decision` never routes, no goal named stays the pre-routing inbox path. `authorizeRoutedLanding` is the BINDING gate underneath the matrix's decision — a goal reaching protected scope raises the task's rank floor to Metic and REFUSES a sub-Metic filer outright, while membership is deliberately NOT required (reporting isn't authoring). `core_222`'s `suggested_goal_id` is an advisory hint only — the real home is `tasks.goal_id`, set by construction — and `/idea-triage` (`scripts/gds/triage.js`) now sends `goal_id` on promotion, so the inbox is homeless-only. Divergence recorded: R11 (Discord `#bugs`) hard-lands a linked Metic+ builder's bug immediately through the SAME shared matrix rather than the spec's "always soft-land, pending queue" design, because a local copy of the rank split would only drift from the one definition; and R12 (Discord `#ideas` hints) is NOT YET SHIPPED, so `#ideas` carries no goal hint today despite the interview decision that it should, hint-only, never route.](<redacted>.md) | ideas / lifecycle |
325
- | 0235 | [**A fix category is `kind='bug'` under a goal, not a new column** ([task 1003215](https://cloudbongos.com/builders#/task/1003215) · goal 1000071, "Idea routing" — the structural half of idea 1000828; task 1003214 shipped the advisory's first quiet case, task 1003157 extended it). A fix for something that should already work needs a home under an existing goal WITHOUT a new goal being born and WITHOUT a stored column restating what `tasks.kind` already says. Not a global "Bugs" goal either: a goal spanning every module makes the [ADR 0086](<redacted>.md) scope wall either everything or nothing, and a goal that can never reach `achieved` (a bug container never finishes) pollutes the exact signal [ADR 0183](<redacted>.md)'s auto-satisfy depends on. Not [ADR 0192 "a category orients and authorises nothing"](<redacted>.md) either, despite the structural resemblance: that decision forbids any gate from branching on a goal's category, while THIS decision is two decision surfaces deliberately branching on `kind='bug'` — `adviseGoal()` (`modules/lifecycle/goal-advisory.js`) goes silent for a bug under all three of its reasons (`no_goal`, `catch_all`, `no_criterion_link` — "a fix is the goal's 'fix category', not a delivery against one of its stated outcomes," its own doc comment) and `countTasksWithoutCriterionLink()` (`modules/lifecycle/db-deps-criteria.js`, surfaced on the hall's goal detail view) excludes `kind='bug'` from its count for the same reason. No hall/status change was needed for GROUPING bugs under a goal: every task card already renders its `kind` as a visible tag and the goal detail view's Filters already offer a `Kind` lens with `bug` as a value — verified against the live code, not built new. Rejected: a `tasks.category`/`fix_category` column or enum, a global cross-module "Bugs" goal, a bug-specific grouping widget beside the general-purpose kind facility that already works.](<redacted>.md) | ideas / lifecycle |
326
- | 0236 | [**Many ideas → one task: the lineage key is the pair** ([task 1003515](https://cloudbongos.com/builders#/task/1003515) · goal 1000071, "Idea routing" — promoted from idea 1000815; the table was built by [task 1002997](https://cloudbongos.com/builders#/task/1002997)). `ideas_task_lineage.task_id` was the PRIMARY KEY on the reasoning that "a task descends from at most one idea", and `linkIdeaTask` wrote `ON CONFLICT (task_id) DO UPDATE SET idea_id = EXCLUDED.idea_id` — so promoting a SECOND idea into a task that already had a link **silently replaced the first idea's lineage**: no error, no warning, no log line. Two ordinary triage promotes at one task were enough, and it happened on prod on 2026-08-21 (ideas 1000811 + 1000812 → task 1003150). Because this table is the rail [ADR 0172](<redacted>.md) stream 1 pays against, an overwritten row is an **ideator not paid** for work their idea produced, with nothing recording it — and it makes merging duplicate reports into one task a LOSSY move, which is the triage the inbox exists to encourage. The R01 grader named this shape and was told it was unreachable; it was unreachable on the ROUTING path examined and fully reachable on the ordinary PROMOTE path. **Decision: many ideas → one task is a real relation, so the key is the pair** — `ideas_006` re-adds `ideas_task_lineage_pkey` as `PRIMARY KEY (task_id, idea_id)` and both INSERT sites `DO UPDATE SET origin`, never `idea_id`. Refusing the second link would turn a legitimate merge into an error; logging the replacement keeps a lossy ledger. **Where one row is still required the EARLIEST link wins** (`ORDER BY created_at, idea_id`, stated in SQL rather than left to the planner): `sourceIdeaForTask`, `credits.awardIdeaTaskShipCredit`, and the nod queue's one-row-per-task predicate — one constant shared by the list and its count, so the pill can never disagree with the rows. Consequences: a task subsumed from N ideas **counts once toward each idea's** shipped-task credit (and cannot double-count within one — the pair is unique); the per-ship payout still names one idea, the earliest, because paying every co-source ideator is a decision about total worth, not a bug fix; the migration **backfills** from `idea_inbox.promoted_to_task_id`, the weaker pointer that survived each overwrite, labelling restored rows `origin='promote'`; and `updateIdea` still swallows a lineage failure but now reports it as a `LINEAGE GAP` through `api.logger` — a silent swallow on top of a silent overwrite was two layers of one mistake.](<redacted>.md) | ideas / economy |
327
- | 0238 | [**The join door is a provisioning SETTINGS key, and the hub column stays the cache** ([task 1002324](https://cloudbongos.com/builders#/task/1002324) · goal 1000046, criterion `proj-visibility-states` — R05). [ADR 0182](<redacted>.md) D5 says R05 "moves host — an instance surface, not a hub one", and on the tree both halves of that sentence pointed at nothing: `platform_identity_projects.visibility` (which R04 shipped) is written by **no route** — every write on `routes/projects.js` is the `project.curate` CURATION gate with no ownership check, `upsertProject` deliberately omits the column, and `owner_github_id` there is documented "advisory attribution (not an authority)" — while "the project's own hall settings" is a page that does not exist (`settings.html` is per-BUILDER; the hall's my-projects page was deleted by task 1003308). **Decision: the door is a key in `provisioning.SETTINGS_VOCAB`** (`visibility ∈ public|private|stealth`, default `public`), set through the existing own-scoped `PATCH /provisioning/instances/:id/settings` and rendered as one more `.mSec` section on the manage view an owner already uses. One line in the vocabulary buys the whole task from machinery already proven: `instanceAccessDenied` (own-scoped, Archon override, **404** not 403 — no existence leak), a validator that IS the vocabulary (so an off-list value 400s and a key can never drift out of the schema), a `provisioning_events` audit row, the `<PREFIX>_VISIBILITY` env the runner patches into `web.env`, and `src/branding.js` mapping it onto `project.visibility` in the public manifest. It is a **POLICY** key, not a platform-drawn one like the planet, for two reasons that outlive today: ADR 0182 D6 makes the door the CEILING `joinability` may only narrow and D1/D5 put admission authority on the instance — a door the instance cannot read cannot be a ceiling — and a key with a DEFAULT belongs in the defaulted vocabulary (`PLANET_VOCAB` keys are nullable because null means "the platform picks", and there is no such thing as the platform picking your join door). Nothing here writes the hub column: it stays ADR 0182 D5's cache, and R08 is the report that will feed it — so today an owner's pick is stored, validated, audited and delivered to their instance, and does not yet move the orb (that gap is R06/R08's, named so the next builder does not hunt a bug). The section deliberately ships **no "what your project runs" read-back line**, unlike its two siblings: a deployed project reports no door until it takes a release carrying the field, and a line that could only ever say "this project's server doesn't report this yet" is worse than none. Rejected: a new owner-scoped write route on the hub catalog (D5 forbids the hub authorising the change); a new instance-side settings page on its own (right long-run home, but today a control with no consumer while R08 is unshipped); a `visibility` COLUMN beside `description` (that one earned a column as half the publish gate and is read by SQL — a closed-vocabulary knob is what the jsonb store exists for); and `PLANET_VOCAB` to dodge the restart (a quieter save today, the ceiling rule lost tomorrow).](<redacted>.md) | provisioning / public-landing |
328
- | 0237 | [**A starter bundle is a preset over the always-on core, keyed by the declared project type** ([task 1002338](https://cloudbongos.com/builders#/task/1002338) · goal 1000046, criterion `proj-module-select`). "During setup a default set of modules is pre-selected" can only mean the OPT-IN modules, since every instance already boots with the same sixteen `default: true` core-domain ones — so a bundle is a short list of opt-in module keys layered on that floor, preselected and freely toggled off by R20's picker ([task 1002339](https://cloudbongos.com/builders#/task/1002339)). **The key is the DECLARED type, and the seeded vocabulary is stale:** the task text (2026-07-17) names *Game / SaaS-Tool / Community*, but task [1002772](https://cloudbongos.com/builders#/task/1002772)'s owner decision (2026-08-13) fixed the live closed set as `game · research · business · non-profit · not-sure` — two CHECK constraints, two `PROJECT_TYPES` constants and five wizard buttons — and "community" survives only as a `team_shape`. Two constraints ride the key: the arrow runs **type → modules, never back** (1002772 rejected inferring type from module flags, which flip all through setup), and the R15 interview must **not re-ask** what `type` declares, so R19 adds no `PROJECT_DETAIL_VOCAB` key and no migration. `not-sure` and *undeclared* resolve to the same modules but are deliberately different answers — `type` is nullable precisely so "nobody said" is never back-filled with a guess, and the resolver returns `type: null` with its own copy. **The declaration is pinned by a test, not a runtime catalog read:** a module may not import `src/module-loader/catalog.js` (the one-way rule, [ADR 0083](<redacted>.md)), so `modules/provisioning/starter-bundles.js` stays PURE like `onboard-plan.js` beside it and `tests/provisioning_starter_bundles.mjs` reads every `modules/*/module.json` off disk — failing the build if a bundle names a module that does not exist, if `ALWAYS_ON_CORE` drifts from the manifests' `default: true` set, if a bundle names an always-on module (a preselect that toggles nothing), or if it names one of the two hub-only modules. The bundles are `game → dev-box, discord`; `research → agents`; `business → agents, dev-box`; `non-profit → discord`; `not-sure → —`, and they are SHORT because today's opt-in catalog is three modules wide and a preselect an owner must audit is worse than none. Served whole at public `GET /provisioning/starter-bundles` (`{ core, bundles }`, same absent gate as `onboard-plan`) so the picker re-preselects without a refetch and R17's recommendations read the same shape. A bundle is **never a publish precondition** — [ADR 0182](<redacted>.md) D4 fixes that bar at name + description alone. Rejected: a runtime catalog read, a `bundle` key in the interview vocabulary, inferring the bundle from the description (the deferred AI layer, [task 1003500](https://cloudbongos.com/builders#/task/1003500)), and keeping the seeded Game/SaaS-Tool/Community triple.](<redacted>.md) | provisioning / project creation |
329
- | 0239 | [**A recommendation is a table that carries its own reason; governance is a preview and invites are a seam** ([task 1002336](https://cloudbongos.com/builders#/task/1002336) · goal 1000046, criteria `proj-module-select` / `proj-governance-reco` / `proj-invite-builders`). R19 ([ADR 0237](<redacted>.md)) declared the per-type starter bundle and said outright it is "a declaration and not a rule engine — R17/R22 own the layer that reasons"; this is that layer, and the goal pins it **rule-based**, with the AI/embeddings pass deferred ([task 1003500](https://cloudbongos.com/builders#/task/1003500), idea 1000552). **Two inputs, and the free-text description is not one of them:** the DECLARED `type` (a column) and `team_shape` (the one closed-answer key the R15 interview collects, [task 1002334](https://cloudbongos.com/builders#/task/1002334)) — inferring from prose is the deferred layer ADR 0237 §7 already rejected once. The engine is three frozen tables (`TEAM_SHAPE_RULES` add/remove ONE opt-in module; `GOVERNANCE_RULES` name a preset; `INVITE_RULES` emit a seek) and **every row carries its own `reason` sentence**, shipped with the effect rather than reconstructed by whichever surface renders it — a recommendation an owner cannot be told the reason for is one they cannot disagree with. `bundle.adjustments` is a **DELTA**: a rule whose condition matched but whose module the bundle already carried records NOTHING, because "we switched discord on because you said community" over a module a game would have had anyway is a claim the owner can check and find false. **Governance is a named preview, never a setting** — the governance module is planned and unbuilt (goal 1000030), so a preset is a name plus one sentence of what it feels like (`owner-only` / `trusted-team` / `open-contribution`) and every result carries `applied: false`, one flag instead of the same fact hardcoded in R25, R18, the wizard and the project page. **Invites are a SEAM**: `suggested` is always `[]` because ranking builders means querying the leaderboard in the sibling `economy` module, which the one-way rule ([ADR 0083](<redacted>.md)) forbids importing — the engine emits `seeking: { kind: 'top-builders', limit }` (or `null` = suggest nobody, a real answer) and R28 ([task 1002347](https://cloudbongos.com/builders#/task/1002347)) turns it into names. **Skipping is an answer:** every table states a default and each is the NARROWEST option (widening later costs one setting; a role granted to strangers is not recovered by setting it back), an off-vocabulary `team_shape` reads as no answer exactly as `normalizeProjectDetail` drops it, and the route answers 200-with-defaults rather than 400 — refusing would make the supported path ([ADR 0182](<redacted>.md) D4) an error. `tailored: boolean` is the machine-readable half of `proj-detail-interview`'s details-vs-skip split that R18 ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) proves. Served at public `GET /provisioning/recommendations?type=&team_shape=` (same absent gate as `starter-bundles`; no database, pinned by a `pool`-throws stub), returning ADR 0237 §5's bundle shape plus `adjustments` so a recommendation and R20's preselect can never name different modules. Rejected: reading the description, filling `suggested` from `GET /public/leaderboard`, a weighted scoring function, a reason per matched condition, and a 400 on an unknown shape.](<redacted>.md) | provisioning / project creation |
330
- | 0240 | [**A skipped module picker stores nothing, and the bundle is resolved on read** ([task 1002339](https://cloudbongos.com/builders#/task/1002339) · goal 1000046, criterion `proj-module-select`). [ADR 0237](<redacted>.md) declared the starter bundles and closed with "where a chosen selection lands is R20's decision". It lands in a new **nullable** `modules jsonb` column on `provisioning_instances` (`provisioning_014`), read through `starter-bundles.effectiveModules(row)` and **three-valued**: `NULL` = nobody answered (the picker was skipped, or the caller does not ask) and the declared type's bundle is resolved AT READ TIME; `[]` = the owner switched them all off, a real answer; `["dev-box",…]` = what they picked. `NOT NULL DEFAULT '[]'` was rejected because it collapses the first two forever — the `PROJECT_DETAIL_VOCAB` rule ("absent stays absent, skipped stays skipped") applied to a list. **Skipping is safe because the preselect and the fallback are the same function**, `bundleForProjectType`, called twice — not two lists kept in agreement — so `bongos onboard` and any API caller inherit the bundle by omitting the field, and the canonical sequence in `onboard-plan.js` grows **no new step** (the picker refines `type`, an answer already in it). NOT `detail` (its vocabulary is closed + scalar by construction, and [ADR 0237](<redacted>.md) §2 ruled the interview must not ask this) and NOT `settings` (a setting is a policy the RUNNING instance obeys — web.env, a settings-apply intent, a restart). The picker's catalog copy (`OPTIONAL_MODULES`) is **declared beside the bundles**, because `module.json` descriptions are written for builders, `GET /modules` needs a session the signed-out wizard has not got, and `GET /public/modules` serves booleans — pinned the ADR 0237 way, so a new `default: false` module cannot ship without owner-facing copy. `GET /provisioning/starter-bundles` gains `optional` so the preselect and the catalog arrive in **one** read. The route **refuses** an unknown / always-on / hub-only key (`400 bad_modules`) rather than dropping it silently (`badProjectDetail`'s rule), and a re-request reports `modules_ignored`. The column is a DECLARATION, not yet an input to the control-plane runner. Rejected: storing the preselect on a skip (it writes down an answer nobody gave), a new `onboardPlan` step, and rendering the always-on core as sixteen disabled toggles (a disabled control reads as a choice the owner failed to make).](<redacted>.md) | provisioning / project creation |
331
- | 0241 | [**The artist gate holds the DEPLOY, not the ship, and it reads a state rather than a judgement** ([task 1003116](https://cloudbongos.com/builders#/task/1003116) · goal 1000074, criterion `artist-blocking-is-per-project` — R05, the DECISION task the goal placed before R04 so no code would be wired to an unsettled gate). Goal 1000074 carries two rules on a collision course: the owner's "blocking strictness is PER PROJECT, not global", and [ADR 0162](<redacted>.md), which retired review gates on `main` after a code-owner requirement stranded two graded-PASS ships in a day. **0162 is usually cited by a paraphrase — "a verdict is advisory, never self-executing" — and the paraphrase is wrong as a blanket claim:** 0162's own table of compensating controls marks three of four as *fails closed*, the subagent grader among them. What it actually retired was a gate GitHub could only express as ACCOUNTS — identity-pinned against [ADR 0016](<redacted>.md), and stranding with no escape hatch. So the surviving test is: **a gate is admissible when it is evaluated automatically from readable state, consistently for every builder, with a named escape hatch; inadmissible when a particular person must act and it stalls when they do not.** Decision: (1) the gate reads a **STATE** — "is there an unresolved artist review task?" — never an opinion, so the artist's judgement is what they write INTO the task and stays advisory in exactly 0162's sense, while what the system reads is ordinary GDS status; (2) strictness is a project setting `artist_gate ∈ off | advisory | strict` and at `strict` it holds the **DEPLOY, not the ship** (owner ruling) — the builder's work is graded, merged, credited and landed on CI alone exactly as today, so 0162's strand class cannot recur, and the gate attaches to the boundary the artist's ABI mandate is actually about (users see the deployed instance, not `main`; [ADR 0137](<redacted>.md) already splits land/release/deploy); (3) the escape hatch is the **project OWNER only** — not the builder, not any Metic+, not the artist — via the existing `POST /tasks/:id/override-request`, because a person can be both artist and builder and could pass their own work; a self-release is permitted but never quiet (required rationale, audit-logged as a self-override) so a solo instance cannot deadlock; (4) the default is **`strict`** (owner ruling, against the author's `advisory` recommendation), which is safe only because §2 holds the deploy — the worst case is a pin that waits, and this instance is included once R04 ships; a project with no artist must still be able to deploy, and the refusal must name what holds it and which control releases it; (5) tightening is **not retroactive** — re-gating history would strand the next deploy behind reviews for work nobody remembers and make the setting frightening to raise; R02's flag queue is the right tool for already-shipped copy. Rejected: blocking the ship (recreates 0162's incident exactly, with credits unpaid — [ADR 0120](<redacted>.md)); advisory at every level (configures nothing); any Metic+ as the hatch (makes `strict` indistinguishable from `advisory`); making the artist's VERDICT the gate (the one thing 0162 genuinely forbids); retroactive tightening; a global constant (ruled out by the owner's constraint by name).](<redacted>.md) | goal 1000074 / the artist's loop |
332
- | 0242 | [**A cross-role cascade is a declaration table on the event that already exists** ([task 1003549](https://cloudbongos.com/builders#/task/1003549) · goal 1000074, criterion `artist-review-task-on-ship` — the split R04 invited in its own brief and [ADR 0241](<redacted>.md)'s dependency). R04 needs "every ship with copy or visuals GENERATES one", and its brief said the infrastructure did not exist, naming the spark queue as the prototype. Reading both surfaces shows the prototype is the wrong half: the spark queue ([task 1002921](https://cloudbongos.com/builders#/task/1002921)) and the copy-desk flag queue ([task 1003113](https://cloudbongos.com/builders#/task/1003113)) are both **PULL** — filtered listings of rows another role already created — and nothing anywhere generated work on a state transition, which is the **PUSH** R04 asks for. (`cascade-dispatch.js` is model-cost routing; named only because its name has misled more than one reader.) **Decision: no new bus.** The kernel seam registry's own header uses this exact scenario as its worked example, `task.shipped` is already emitted post-commit by both ship paths and already has two consumers (discord's broadcast, sessions' deferred reward) — so a cascade is ONE MORE LISTENER on it, registered in `routes/lifecycle.js` beside the port because that mount is the one boot hook tests also run. A cascade is a frozen entry `{ key, discipline, when(task), title(task), body(task) }` — **a declaration table, not a rules engine**: no condition language, no stored rules, no per-project config, no priority ordering; adding one appends an entry and the runner does not change (the [ADR 0237](<redacted>.md)/[0239](<redacted>.md) shape, for their reason — the generalisations worth building are the ones two or three real entries have shown they share). **`CASCADES` ships EMPTY and R04 writes the first rule**, asserted so the emptiness is a decision on the record; the mechanism is proved against the tests' own declarations rather than against its first consumer's shape. Four load-bearing properties, all pinned: (1) **it can never fail the ship it reacts to** — post-commit event, isolated listeners, and a runner that catches everything and returns a report, because ADR 0241 settled that an artist may hold a DEPLOY and never a builder's SHIP and a generator that could fail a ship would smuggle that gate back in; verified against a real failure (no database reachable → one log line, resolves); (2) **idempotent on machinery that already exists** — `source: 'cascade'` + `source_ref: cascade-<key>-<taskId>` through `createTask`'s `(source, source_ref)` read, the same one a copy proposal uses ([ADR 0233](<redacted>.md)), so a reconciler re-sweep or a double emit is a no-op; (3) **it says where it came from** — inherits `goal_id`/`version_id`, carries the origin id in the ref and the prose, but NOT `parent_task_id`, which models a SUBTASK, and cascaded work is not part of the ship that triggered it; (4) **files at `backlog`, never `ready`** — generated work is a request, and auto-filing claimable work would let a rule nobody re-read mint claims. A rule key is PERMANENT (half the source_ref; renaming re-files everything it ever generated), pinned by a duplicate-key check. The mechanism is discipline-agnostic — an ideator or engineer cascade is the same entry with a different `discipline`, which is the generalisation that made this its own task. Accepted limit: cascades are declared in code, not configured per project. Rejected: extending the spark queue (a read-side filter that generates nothing), a new event or poller, a rules engine or per-project config, filing at `ready`, `parent_task_id` as the link, and running the cascade inside the ship transaction (which would make a generation failure a ship failure — the exact coupling ADR 0241 rules out).](<redacted>.md) | goal 1000074 / the artist's loop |
333
- | 0243 | [**A bundle adjustment belongs to the bundle, not to the advice about it** ([task 1002341](https://cloudbongos.com/builders#/task/1002341) · goal 1000046, criterion `proj-module-select`). R17 ([ADR 0239](<redacted>.md)) built a rule engine and R20 ([ADR 0240](<redacted>.md)) built a picker, and nothing connected them: the picker preselected the type's RAW preset while `GET /provisioning/recommendations` separately applied `TEAM_SHAPE_RULES` to that same preset and returned a better answer no surface rendered. Joining them the obvious way — let the picker apply the rules — breaks the property R20 was built on ([ADR 0240](<redacted>.md) §2: the preselect and the fallback are "one function, called twice"), because an owner who answers the interview, watches the panel preselect `discord`, and then SKIPS the picker would get a project without it. **Decision: `TEAM_SHAPE_RULES` moves from `recommendations.js` down to `starter-bundles.js`** (re-exported, so the public surface is unchanged), `effectiveModules` resolves through the new `bundleForAnswers(type, teamShape)`, and the picker reads the same table the fallback does. The distinction is not a preference: a bundle adjustment is not advice ABOUT the bundle — it IS the bundle once the owner has answered, and it is the only one of R17's three tables that is ever APPLIED (governance is a preview carrying `applied: false`, invites are a seam for R28), so it is the only one that has to live where the resolution happens. `effectiveModules` reads `detail.team_shape` straight off the row rather than through `normalizeProjectDetail` — importing it would point the bundle layer UP at the module that requires it ([ADR 0083](<redacted>.md)) to buy an answer it already has, since an off-vocabulary shape fires no rule and adjusts nothing. `GET /provisioning/starter-bundles` gains `byTeamShape`: every bundle re-resolved for every ruled answer, with the `adjustments` that got it there — twenty small rows, the whole preselect table in ONE read, for the reason `optional` rides the same payload (the owner flips type and team-shape on two EARLIER panels, and a refetch could land after they moved on or paint a key the panel has no row for). `PATCH /provisioning/instances/:id/detail` returns `modules: effectiveModules(updated)` beside its `publish` verdict and for the same reason — changing `team_shape` MOVES the modules of a `source: 'bundle'` project, so the effect comes back with the cause. The panel renders one reason line per adjustment that ACTUALLY fired, and only while the owner has not answered for themselves (a reason handed back for a toggle they flipped reads as the panel arguing with them). An adjusted bundle returns in CATALOG order, never append order, or the same set of modules would read as a different list depending on whether the picker was answered or skipped; a payload without `byTeamShape` degrades to the R20 answer, which is never wrong, only untailored. No new storage — the adjustment is resolved, never written. Rejected: leaving the rules up in `recommendations.js` and having the picker call `/recommendations` (two lists kept in agreement by hand, plus a round trip per keystroke); importing `normalizeProjectDetail`; storing the adjusted bundle on a skip (it writes down an answer nobody gave); moving governance and invites down too (they are never applied, so there is nothing for them to stay in agreement with); and a reason line per matched condition rather than per actual change.](<redacted>.md) | provisioning / project creation |
334
- | 0245 | [**The way back into an optional step is that step’s own answer route** ([task 1002342](https://cloudbongos.com/builders#/task/1002342) · goal 1000046, criterion `proj-module-select`). R20 ([ADR 0240](<redacted>.md)) made the create wizard’s module picker skippable and left a project’s module set write-once — the route file said so out loud (“there is no mutation route yet”). R23 closes it WITHOUT a new endpoint: `modules` joins `description` and `detail` on `PATCH /provisioning/instances/:id/detail`, because the wizard’s two optional steps are the same kind of thing — asked optionally at creation, stored on the platform row, reaching no running instance — and that route already carried the ownership gate, the `api:self`/`api:archon:<id>` actor rule, the 404-no-existence-leak shape, and (since [ADR 0243](<redacted>.md)) an `effectiveModules` answer it could report but not set. Deliberately NOT the settings channel: a setting is a policy the running instance obeys and would queue a restart to deliver a value the instance does not read. Only an ARRAY is a change — an omitted or null key leaves the column alone — so the three-valued contract’s `NULL` (“never asked”) stays unreachable by a later edit, and `modules` REPLACES where `detail` merges, because a merging write would make removal impossible. The surface is the manage view’s extras band (`/projects?view=manage&id=N`), where a toggle IS the save and the band repaints from the platform’s answer rather than from what was sent.](<redacted>.md) | provisioning / project creation |
335
- | 0244 | [**A catalog search is a view over the rows, never an edit to the selection** ([task 1002340](https://cloudbongos.com/builders#/task/1002340) · goal 1000046, criterion `proj-module-select`). The creation picker ([ADR 0240](<redacted>.md)) paints one toggle per optional module, and the catalog is meant to grow — so R21 adds a keyword search over it. A search over a list of links is trivial; a search over a list of TOGGLES is not, because the list is also an answer. Two failure modes follow from letting a row be hidden: the filter **edits** the answer (narrow the rendered array and the read-back array together, and typing `discord` then pressing Next files one module instead of three — silently, with every screen looking right), or the filter **lies** about it (a switched-on module off screen reads as switched off, which is [ADR 0237](<redacted>.md)’s "a preselect they have to audit" in its harsher form — one they cannot see at all). **Decision: the search is a VIEW and `state.modules` is the answer; the view may not write to it.** `moduleQuery` is a plain variable, never part of the draft, so `saveDraft` is not called from the input handler and a search can never turn an untouched picker into an answered one (ADR 0240’s `null` survives); `visibleModules()` derives the shown rows per render while `pickedModules()`, `modulesForCreate()` and `extrasRow()` keep reading the whole catalog; and the panel STATES what the filter is hiding — one line under the field counting what is **on**, not what is visible. Matching is `moduleMatches(mod, terms)`, a pure function: lower-cased **label + summary + key** as one haystack, every whitespace-separated term an AND’d substring. AND because adding a word means the list is still too long; substring because a filter that shows nothing until you finish typing reads as broken; the key because that is what a returning owner names a module by. No ranking (reordering toggles under a typing hand is worse than not), and deliberately **not semantic** — by-meaning search is deferred with the rest of the creation AI layer (idea 1000552). Rejected: filtering the read-back array (failure mode 1), CSS-hiding non-matches (leaves invisible `aria-pressed` toggles in the a11y tree), pinning switched-on rows above the query (a filter that lies the other way), persisting the query in the draft, and debouncing a pass over already-loaded rows.](<redacted>.md) |
336
- | 0247 | [**The join door is ONE composed answer, and dark matter has no live verdict to compose with yet** ([task 1002331](https://cloudbongos.com/builders#/task/1002331) · goal 1000046, criterion `proj-join-policy`). [ADR 0182](<redacted>.md) D6 wrote the rule — visibility is the ceiling, join policy may only narrow it — and nothing implemented it: the public `POST /access-requests` read `joinability` alone, so an owner could set their project private or stealth on the map and the application endpoint behaved exactly as before. R12 adds the third knob (`join_grant` = view | apply | full, one line of settings vocabulary, default `full` so nothing existing changes) and composes all three in one pure kernel file, `src/bongos/project-door.js`, published through the doorway so no module can re-open a door the others closed. Three things worth carrying: the door vocabulary is a list ordered widest→strictest, so “the stricter wins” is the data structure rather than a branch; the refusal never names WHICH knob closed the door, because a stealth project that could be identified by its 403 gives back exactly what R06’s server-side redaction protects; and `locked` (dark matter) is specified, tested and deliberately UNWIRED — the instance cannot derive publishability (no description in its pack, and a pushed verdict goes stale the moment the owner writes one), while the hub’s cached copy says every live project is dark matter today, so enforcing off it would refuse every join on the platform.](<redacted>.md) | onboarding / provisioning / project creation |
337
- | 0248 | [**A hub project’s owner is the provisioning row, carried on a port and bound to both halves of the catalog projection** ([task 1003580](https://cloudbongos.com/builders#/task/1003580) · goal 1000046, criterion `proj-invite-builders`). [ADR 0246](<redacted>.md) made a wizard-made project resolvable by `POST /projects/invite` and named the remainder: the route is gated `project.curate` (floor metic) with no own-scope arm, so the project’s own owner — a `xenos` on the hub — got 403 on their own project, blocking BV1.R27–R29 and the C6 proof. The arm admits a `project.curate` holder **OR** the owner, where owner means `provisioning_instances.owner_builder_id` — never the hub’s `owner_github_id` columns, which [ADR 0238](<redacted>.md) fixes as attribution. Three things worth carrying: the fact travels on a NEW port `provisioning.projectOwnedBy` answering a **boolean**, so the comparison stays on the plane that holds the row and no consumer handles an ownership id it could mis-compare; the port is asked about **both halves of the ADR 0246 projection** (slug AND origin), because a hand-seeded catalog row’s name is otherwise a squattable claim on that project’s membership directory; and ownership is asked BEFORE the target is resolved, so the refusal stays the untouched `requirePermission` 403 and cannot become an existence oracle over stealth projects ([ADR 0182](<redacted>.md) D2a). The atom stays spelled in the route’s own middleware list ([ADR 0210](<redacted>.md)’s rule), so the route still classifies `metic+archon`.](<redacted>.md) | onboarding / provisioning / project creation |
338
-
339
- | 0249 | [**The invite step is the done panel’s first act, not a rail step — and the same widget is the way back** ([task 1002346](https://cloudbongos.com/builders#/task/1002346) · goal 1000046, criterion `proj-invite-builders` / C6). BV1.R27 asks for a skippable invite step *in creation*, but an invite cannot be one of the wizard’s numbered panels: `POST /projects/invite` takes `{ client_id, github_login }` where `client_id` **is the slug**, and per [ADR 0246](<redacted>.md) the catalog row that slug resolves is projected **at create** — so before the Create step there is nothing to invite anyone to, and a pre-Create panel could only queue names and replay them, inventing a second unaudited invite path whose failures land after the owner has left. **Decision: the step is the FIRST act of the DONE panel**, keyed on the created record (not `instanceId`, which `startProgress` sets only *after* `renderDone`), so it also returns on a resumed watch; the rail is NOT renumbered (a captured learning on this file: a renumbering couples in four places, and a step that can only run after the terminal step is not a step in the rail). The SAME widget is mounted again on the project’s own page beside *Its extras* — the [ADR 0245](<redacted>.md) argument, one implementation and two mounts, so the refusal vocabulary cannot drift. The widget answers neither “is anyone signed in” nor “is this their project”: each call site hands over a slug or nothing (the created record; the own-scoped manage read that 404s for everyone else), because a page that re-derived ownership would be a second, weaker copy of a server rule. **The 403 is quoted carefully and that is the point** — [ADR 0248](<redacted>.md) paid two equalised reads per request to make the refusal identical for a real, stealth, absent or malformed target ([ADR 0182](<redacted>.md) D2a), and “you lack permission on **this project**” would assert in copy the existence the route refuses to confirm, so the page says only “this account can’t invite people here” and a test pins that wording. `account_not_found` and `unknown_project` are said in the route’s own words (the latter as a moment, not a dead end — the row is projected at create); anything else falls through to the server’s own message. The pending line is drawn from the 201 row’s `membership_kind` and the typed login — **never the `github_id`** it carries, which names someone who has accepted nothing yet. Creation is never blocked: the project is filed before the step is shown, so every refusal leaves it created and the step re-tryable, and Skip sends nothing. Rejected: a rail step before or after Create; the manage page alone (R27 asks for it *in creation*); reading the real membership directory back (no own-scoped route exists, and the session receipt is honest about being one).](<redacted>.md) | provisioning / project creation |
340
- | 0250 | [**Strict versioning — the version boundary is the scope gate** ([task 1003589](https://cloudbongos.com/builders#/task/1003589) · goal 1000086). The work hierarchy ([ADR 0086](<redacted>.md)) has a bottom and no top: tasks close, criteria close, goals close, and a version never does. Measured live — **14 goals reading `achieved`/`archived` while holding 35 unfinished tasks**, 523 open tasks across 37 open goals, and **two versions `building` on one project**; every number was worse than the planning session had recorded two days earlier, which is the argument: the drift is the steady state, not a backlog. The cause is four mechanical gaps, not a social one — `POST /goals` fetches the version row and never reads its `status`; goal achievement is computed from criteria alone and never consults `tasks` (both the `db-goals.js` writer and the `done-when.js` cascade); `POST /goals/:id/archive` checks nothing at all; and no route ships a version, so version status has only ever moved as hand-run SQL on the droplet. **Decision, five rules:** the goal set closes when a version starts `building` (new goals go on the `planning` version — where the next version is scoped in advance) with a named 409 an agent can act on; a goal reaches `achieved` only when its criteria are satisfied **and** no task in it is non-terminal, where terminal is `shipped`|`abandoned` per [task 1003588](https://cloudbongos.com/builders#/task/1003588)'s one vocabulary and the mid-ship `completed`/`confirmed` hold it open; archiving a goal that holds open tasks refuses until the caller dispositions each (abandon with the reason, or move to a named surviving goal); `goal_id` becomes required and the per-version catch-all fallback is deleted; and exactly one version builds per project, auto-closing when its last non-maintenance goal achieves, with the single `planning` version promoting itself so the project is never without a live train. **The escape hatches are deliberate and visible**: an Archon may admit one *named* goal to a building version with a reason, and the overrides are counted on the version (the standard failure of a hard scope gate is a fake hotfix version cut to hold what did not fit — an untraced override is just a slower version of no rule); plus one auto-created maintenance goal per version, exempt from the auto-close count, so bugs never need an override to be filed. **Nothing here can refuse a ship, a claim, or a task** — every rule acts on a *closing* move. Rejected: counting `completed`/`confirmed` as finished (they can fail a grade or strand — the goals **board** buckets them as done, but that is a progress ring, and `task-status.js` keeps the two questions apart); a nightly reconciler that reopens wrongly-closed goals (detection is what you build when prevention is unavailable, and it is available here); a human review gate before a goal may close (the queue [ADR 0183](<redacted>.md) dismantled — the check is a fact the table already holds); concurrent building versions scoped by module (two live trains means every "what is in this version" question has two answers); retiring versions entirely (the version is the unit the owner sets scope in and `limitations/<version>-shipped.md` archives). **Amends [ADR 0157](<redacted>.md) for exactly one key:** `version.create` returns to the Archon floor. 0157's "authoring delegates, disposition does not" was right about what a version *was* — but once the boundary carries the scope gate, cutting a new version is no longer authoring, it is the way *around* the gate, so create joins close as a disposition over scope.](<redacted>.md) | lifecycle / versioning / scope |
341
- | 0251 | [**A suggestion of who to invite is a recruiting surface, and owes the recruiting opt-out** ([task 1002347](https://cloudbongos.com/builders#/task/1002347) · goal 1000046, criterion `proj-invite-builders` / C6). BV1.R28 fills the seam [ADR 0239](<redacted>.md) §6 left open: the wizard’s pure rule engine emits a **seek** (`solo` → `null`, `small-team` → 3, `community` → 5) and never a list, because ranking builders is a directory read a pure file may not run and `provisioning` may not import a sibling. Two readers could answer “who are the top builders” and they differ by **one SQL predicate**: `economy`’s `leaderboard()` joins `rollupVisibleSql` and deliberately honours **no** recruiting opt-out, while `platform-identity`’s `listScoutingRollup()` joins `scoutingListableSql`, which carries it. The leaderboard is cheaper in every mechanical sense — already a public route, no port, callable straight from the browser. **Decision: use the recruiting directory anyway**, because `account-visibility.js` says the split in its own words (`hide_stats` suppresses *numbers* everywhere; the recruiting opt-out “removes an account from the RECRUITING SURFACE only”, community spec D3), and a list of people the platform proposes you recruit **is** that surface — sourcing it from the leaderboard would route around the one control built for this exact case and name every builder who asked not to be recruited. The rule for later readers: **the cheapness of a public read is not an argument about who consented to appear in it.** The lookup ships as `listInviteSuggestions` on the existing `platform-identity.hub` port, reached with `resolveOptional` and NOT declared in `consumes` (the `catalog-bridge.js` precedent — platform-identity is hub-only, so its absence is “no suggestions”, never a 500); `github_id` **does not cross the port** (explicit whitelist — login, display name, avatar, disciplines, two counts — so a new rollup column cannot leak by default), and the owner-exclusion is applied inside, over-read by the exclusion count so filtering never returns short. The route `GET /provisioning/instances/:id/invite-suggestions` is own-scoped by the **existing** `instanceAccessDenied` (same 404 as the manage read) and deliberately does NOT reuse `/scouting`’s gate ([ADR 0210](<redacted>.md)): a brand-new owner has no hub-witnessed owner row at the moment the done panel paints, so the correct-looking gate would refuse the exact caller the feature exists for — and reaching the route IS the ownership answer ([ADR 0249](<redacted>.md) §4). On the surface: a **second door in** (`setInviteSuggestions`) beside `setInviteSlug`, so the widget still answers neither “signed in” nor “theirs”; the reason is **always** said including when the answer is nobody (`seeking: null` costs no query — rendering a stated answer as an empty list would make it look like a failed lookup); a picked name fills the field and takes the **same write path** as a typed one, so the refusal vocabulary cannot fork; a filed invite stops being a suggestion; and every failure is silent, because a suggestion is an extra and a just-created project must not look broken over one. Rejected: the browser reading the economy leaderboard (the whole decision — consent, not architecture); reusing `/scouting` (right data, gate that refuses this caller); filling `invites.suggested` inside the engine (I/O in a pure file + a sibling import); a `provisioning → economy` port (legal, still the wrong directory — the boundary was never the objection).](<redacted>.md) | provisioning / platform-identity / privacy |
342
- | 0252 | [**The self-hosted visibility report is a PULL, and auto-appear buys a retraction lever** ([task 1002321](https://cloudbongos.com/builders#/task/1002321) · goal 1000046, criterion `proj-visibility-states` / C1, BV1.R02 — a spike whose output is this decision; gates [task 1002327](https://cloudbongos.com/builders#/task/1002327) / R08). **The premise failed first:** a self-hosted instance has no channel to the hub and no credential to open one — `registerClient` has exactly one caller in the tree (the operator's own `provision-net.js`), gated `hosting_shape !== 'co-tenant'`, and no route registers a client. So *federated* ⊆ *operator-provisioned co-tenant* and *self-hosted* holds nothing, deliberately: [ADR 0145](<redacted>.md) says do NOT extend federation to `standalone`, and [ADR 0144](<redacted>.md)'s Known Limitation rests on "every secret-holder is an operator-provisioned, trusted instance … the proper fix must land before federation opens to untrusted third-party clients." [ADR 0182](<redacted>.md) D5's "accepts a report from the authenticated `sso_client`" described a client that, for this population, does not exist. **Decision 1 — the channel is a PULL of the instance's already-public manifest** (`GET <origin>/api/gds/instance` → `branding.project.visibility`, a value every core already serves and whose author anticipated this consumer). It buys five things a push must build: no credential (0144's tripwire untouched — the whole reason), **no replay surface** (a pull reads current state, so the `jti` machinery D5 handed R02 is moot — replay is answered by not having a message), no wire change on another instance's upgrade clock ([ADR 0205](<redacted>.md)'s and [ADR 0201](<redacted>.md) D2.2's rejected-required-field problem simply absent), the hub still authorising nothing (0182 D5 satisfied, [ADR 0238](<redacted>.md)'s rejected `PATCH /projects/:id/visibility` still rejected), and a fetch that already exists behind a real SSRF guard (`project-stats.js` — DNS-resolve-all, `redirect: 'error'`, TTL + negative caches). The pull carries `visibility` and NOTHING else, so the property the map has today — a project owner controls no free-text field that reaches the feed — is not spent. **Decision 2 (the Archon's) — auto-appear:** no human stands between a public project and the map; `featured` keeps only the ranking job [ADR 0182](<redacted>.md) D3 left it. Conditional on Decision 4. **Decision 3 (the Archon's) — trust follows provenance:** both populations auto-appear, and the asymmetry is in the GUARDS, not a gate — a control-plane row's `visibility` is *assigned* by the bridge from an own-scoped audited route (authoritative), a self-hosted row's is *pulled* from a manifest the hub does not control (a cached claim with a recency stamp, able to move its own row and nothing else). An instance is authoritative about its own door and about nothing else. **Absent is a THIRD state and must not purge:** `null` (never read) ≠ `false` (read, negative). 0182 D5's fail-closed default holds only for a self-hosted row that has NEVER answered; a row that answered before and is unreachable now keeps its last state with a recency stamp ([ADR 0199](<redacted>.md), [ADR 0226](<redacted>.md)), because [ADR 0247](<redacted>.md) D4 found the live shape of that trap — every catalog row at `description: null`, so naive fail-closed would have emptied the map at once. Narrowing is free; widening is what costs. **Decision 4 — auto-appear buys a retraction lever, in the same task** ([ADR 0047](<redacted>.md): the straight-through path and its abuse guard are one task, treated as binding): `platform_identity_projects.status` is `(active,hidden)` and **no route writes it** — takedown is hand SQL today, and a one-way door onto a public surface is not a door. R08 ships `PATCH /projects/:id/status` (curate floor, audited), a per-IP limit on the unmetered CORS-open feed read ([ADR 0209](<redacted>.md)), and above all **`hidden` STICKY against the pull** — a retracted project that keeps reporting `public` stays hidden, or the next sweep silently undoes every takedown and the lever is decorative. Also named: `hall_origin` reaches the feed from the remote's own discovery index checked only for *differing* from the project host — the opposite of containment — and gets `shipUrlWithinClientOrigin`'s downward-only rule, filed against R08. Rejected: a pushed credentialed report (the literal R08 — needs the credentials 0144/0145 forbid, plus replay, rate and a wire-compatible field, none of which the pull has); `PATCH /projects/:id/visibility` (0238 already); a one-time admission and ongoing curation (both offered to the Archon, both declined); trusting the pull to move `status`; enforcing "unreported ⇒ dark matter" across all rows; letting the pull carry name/tagline/art. The later ANALYTICS feed is deliberately NOT decided here — it carries per-person data, exactly what [ADR 0205](<redacted>.md) proved cannot ride an unwitnessed self-report, and it needs the 0144 credential work first.](<redacted>.md) | platform-identity / provisioning / federation trust |
343
- | 0256 | [**The visibility ceiling binds the SIGN-IN gate too, and the knob reader moves to the composer** ([task 1003579](https://cloudbongos.com/builders#/task/1003579) · goal 1000046, criterion `proj-join-policy`). Closes the *KNOWN LIMIT* [ADR 0247](<redacted>.md) wrote down: it composed `visibility × joinGrant × joinability` into one door and enforced it at the public application endpoint, leaving `openEnrollmentEnabled()` reading the `joinability` knob alone — so a project set `private` (or `stealth`, or `joinGrant: view`) while `joinability` stayed `open` **queued applications for review and still let any first-time GitHub account sign itself in at xenos**. Half of [ADR 0182](<redacted>.md) D6’s table — its whole *Self-join* column — was unenforced wherever the owner had explicitly opened enrolment. **Decision 1:** `openEnrollmentEnabled()` becomes `effectiveJoinDoor(deps) === 'open'`, the same SR-19 move [ADR 0194](<redacted>.md) D1 made one level down, so the four sign-in call sites, `firstTimeAdmissionBlocked`, `webAdmissionStatus` and the manifest all follow untouched — and **nothing changes for any existing project**, because `branding.js` normalises an absent visibility to `public` and an absent grant to `full`, the pair that narrows nothing. The ceiling closes **self-enrolment only**: an invited first-timer, an existing builder and the configured `firstAdmin` still get in, or a private project locks out its own founder. **Decision 2:** the manifest’s `admission.policy` now reports the GATE while `brand.project` keeps reporting the SETTING — they may differ, because they answer different questions, and `invite-only` is already what every `apply` project reports, so narrowing into it names no state ([ADR 0247](<redacted>.md) D3). **Decision 3 — the mechanical one:** `joinabilityMode()` moves from `src/bongos/auth-admission.js` into `src/bongos/project-door.js` (re-exported, so no import path and no doorway export changes). The door resolved its own joinability input with a lazy `require('./auth-admission')`; the moment the gate requires the door back the two files point at each other, and the `require_cycle_count` ratchet counts edges from **source text**, so laziness is no escape. Rejected: duplicating the composition in the gate (the drift SR-19 exists to prevent), and making `joinabilityMode` a required argument of `effectiveJoinDoor` (turns the published no-argument call into a footgun that silently answers `invite_only`). The reader was only in the admission file because the gate was its first caller; the composer owns its inputs. **Decision 4:** the gate passes no `publishable` verdict, so `locked` still cannot fire from it — [ADR 0247](<redacted>.md) D4’s finding stands, and a gate reading the hub’s cached column would refuse every first sign-in on the platform. Also rejected: refusal copy that names the private state (the read-out D3 forbids, and a case-specific sentence makes the refusals distinguishable by another route); a `webAdmissionStatus` special case (it reads the same function, and its anti-drift sweep already pins the agreement). Proof: `tests/admission_ceiling_gate.mjs` boots a real instance on `private` + `open` through the production env channel, then sweeps all 75 knob combinations asserting the gate **is** the door and that exactly one combination — `public`/`full`/`open` — self-enrols.](<redacted>.md) | admission / project visibility / kernel layering |
344
- | 0253 | [**The takedown lever has two writers, and provenance has to be a column** ([task 1002327](https://cloudbongos.com/builders#/task/1002327) · goal 1000046, criterion `proj-visibility-states` / C1, BV1.R08 — the build). Amends [ADR 0252](<redacted>.md) §4/§5: three premises that did not survive contact with the tree. The decisions stand; what changes is what has to be BUILT. **(1) `hidden` is sticky against TWO writers, and 0252 names the wrong one.** 0252 §5 says "no route writes [`status`]" and defends the takedown against the PULL. But `project-catalog.js:upsertOwnedProject` writes `status = EXCLUDED.status` on every conflict, sourced from `catalog-bridge.js:catalogStatusForInstance` — which returns `active` for any instance not torn down — and [ADR 0246](<redacted>.md) re-takes that projection at EVERY provisioning write. So the live failure is: curator hides a project → its owner saves any setting → it is back on the map. Stickiness therefore belongs to the ROW (`status_pinned`, set by the curated PATCH, honoured by the projection), not to one writer's manners; the pull separately writes no `status` at all, asserted against the statement text. `retireOwnedProject` deliberately does NOT honour the pin — it is a safety retraction for an origin the project no longer answers on ([ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2), and narrowing is always free. **(2) Provenance must be a stored column.** 0252 §4 tabulates control-plane vs self-hosted rows as though the catalog knew which was which; it has no `hosting_shape` (that lives on `provisioning_instances`, in a module platform-identity may not import — [ADR 0083](<redacted>.md)) and `owner_github_id` separates nothing (set by the bridge, set by `registerClient`, null on hand-seeded rows). `visibility_source` (`control-plane`|`pull`|NULL) records it on the row, re-asserted in the UPDATE's own WHERE so a mid-sweep projection cannot lose the race — making "the two never write the same row" an invariant the SQL enforces. Rejected a `provisioning.instanceShape` port: a cross-module read in a background loop, for a value that never changes per row. **(3) The tri-state cannot live in `visibility`** (`NOT NULL DEFAULT 'public'`), so the recency stamp IS the null-carrier: `visibility_reported_at IS NULL` is the never-read state, and a failed read writes nothing — which is what makes "keep the last known state" fall out rather than be implemented. The fail-closed feed predicate needed a BACKFILL for [ADR 0247](<redacted>.md) D4's exact reason: both columns are new, so unbackfilled it would have emptied the public map on deploy. Migration 020 stamps every existing row `control-plane`, which is safe because it is TRUE — no instance has ever been asked. Two smaller corrections: the manifest key is **`brand`**, not `branding` (0252 §2 and the task body both misname it; reading only the documented spelling makes every pull a silent no-verdict), and the pull is a **POLLER, not a passenger on the feed's fan-out** — the feed excludes exactly the rows that need pulling (a never-answered row is dark matter, so it is never enriched, so it never answers), stealth rows are excluded from enrichment by design, and a read-path pull would make a DB write a side effect of an anonymous CORS-open read. Honest limit recorded: 0247 D4's `description: null` problem is NOT fixed here — 0252 §7 refuses to let the pull carry name/tagline/description, so the publish gate stays unmet for self-hosted rows. The pull moves the door; it does not publish the project.](<redacted>.md) | platform-identity / provisioning / abuse guard |
345
- | 0255 | [**A public list's ordering is part of its payload, so an anonymous row must be ordered by nothing** ([task 1003561](https://cloudbongos.com/builders#/task/1003561) · goal 1000046, which absorbed [task 1003560](https://cloudbongos.com/builders#/task/1003560)). The public projects feed redacts a stealth row down to an allow-list of `state` + `orb` and carries **no stable per-project key on purpose** ([ADR 0182](<redacted>.md) D2a) — then sorted `ORDER BY featured DESC, updated_at DESC` and handed the observer one anyway. **Ordering is a covert column:** a position is a comparison result, and enough comparisons reconstruct the key. Three consequences, the third load-bearing — `updated_at` was published despite being deliberately withheld from the projection (it lies as a public "last updated"); a black hole sat BETWEEN NAMED NEIGHBOURS whose order is visible, bracketing its timestamp between two identifiable rows; and it **MOVED when its hidden project was touched**, which is an activity oracle on a public, CORS-open, unauthenticated route, and stable enough between touches to follow one black hole across snapshots. **Decision: a row may be ordered only by what it publishes.** An identified row publishes `created_at` + `origin`, so it sorts by those under `featured` (D3's remaining ranking job) — an order that is a PURE FUNCTION OF THE WIRE PAYLOAD, reproducible by a reader holding only the response. An anonymous row publishes nothing, so it is ordered by nothing about itself: the black holes collate into ONE TRAILING BLOCK permuted by a keyed hash. Four sort terms; the first partitions the blocks so no later term compares across them, and **terms 2-4 are GUARDED** — the "anonymous rows go last" half without the "and are blanked" half is not a fix, since the trailing block would simply re-sort by `created_at`, which narrows *which* project it is for anyone who watched the catalog fill. The identified set is DERIVED from `ORB_BY_STATE` (never re-typed, so SQL and `feedShapeFor()` cannot drift) and is an ALLOW-list, so an unknown fourth enum value sorts anonymous as well as drawing anonymous. The permutation salt is **secret** (origins are guessable domain names — a predictable salt lets an observer hash candidates and match ranks) and **rotates on a 24h epoch** (a permanently stable permutation still makes POSITION a durable handle); random per process, since nothing persists or reads it back. **Pagination is the same decision, not a neighbouring one** — task 1003560's premise was already stale (`<redacted> = 500` shipped with the feed; its own comment called the cap "the cheap half" and pagination "its own task"), and the surviving half edits this exact statement, so it merged in: `?limit=&offset=` per [ADR 0119](0119-api-pagination-contract.md), **no cursor** because the obvious encoding is the last row's timestamp — the very channel this closes — and the **default page IS the maximum (500)** so a static orbs site in ANOTHER REPO is byte-for-byte unchanged, with `page` added beside `projects` rather than wrapping it. Paging cannot aim more fan-out than one request already could: `?limit=` only shrinks, and `?offset=` is bounded by `project-stats.js`'s 10-min per-origin cache, not the request count ([ADR 0252](<redacted>.md) §5.3's per-IP limiter unchanged). Rejected: sorting everything by `created_at` (positions a black hole by a date its row refuses to carry); alphabetical `origin` order for the block (an oracle against a candidate set); `ORDER BY random()` (breaks paging — a row could appear twice or not at all); coarse `updated_at` buckets (a sort key that must be blurred should not be there). **Honest limits, stated rather than implied fixed:** existence and count stay public by design; **the identified set still CHANGES when a project goes stealth**, a LARGER channel than this one and untouched here; and position is stable within one epoch.](<redacted>.md) | platform-identity / privacy boundary / public API |
346
- | 0254 | [**A relayed link is contained downward, or it is not published** ([task 1003670](https://cloudbongos.com/builders#/task/1003670) · goal 1000046, criterion `proj-visibility-states` / C1) — the gap [ADR 0252](<redacted>.md) §5 named and filed. **The defect:** `hall_origin` reaches the public projects feed from the REMOTE instance’s own discovery index (`GET <origin>/api/bongos` → `index.hall`, `project-stats.js`) and the hub renders it as an `<a href>` under that project’s name — checked only for **differing** from the project’s site host, which is containment turned inside out: it admits every host on the internet and refuses the one that is certainly safe. `{"hall": "https://evil.example/builders"}` was published on the hub’s own domain, attached to the victim. The rule answered a DIFFERENT question correctly ([task 1003257](https://cloudbongos.com/builders#/task/1003257), owner decision (a): *a door only where it opens somewhere else*); nobody had asked it the security one. It stopped being survivable when [ADR 0252](<redacted>.md) §3 replaced “whom the operator provisioned” with “whoever points a domain at the hub”. **Decision:** the same **downward-only** rule a pushed ship link already obeys — a relayed URL must point AT OR BELOW the origin that reported it (own host or a **dot-anchored** subdomain; `http(s)` only; no embedded credentials; **refused whole at the length cap, never truncated**, because slicing a URL changes where it points). Containment runs FIRST and compares **hostnames** (a port is not a trust boundary); the unchanged “different door” test runs second and compares **hosts with the port** (a hall on another port really does open elsewhere). Net: a hall link survives only as a strict subdomain — or another port — of the project’s own origin. **One home:** `shipUrlWithinClientOrigin` (a private helper in `platform-identity.js`) is lifted to `origin-containment.js:urlWithinOrigin` and required by BOTH callers — the defect WAS two surfaces answering one question differently, and leaving the answer where only one could reach it is what let them differ. Its own file for the reason its siblings give: `platform-identity.js` is AT the 1500-line ratchet (the lift shrinks it by 11), and deliberately NOT re-exported there — a re-export is a third name for one answer. **Downward only, and the cost stated:** the mirror rule looks symmetric and is not (on a shared apex it lets `foo.github.io` publish links on bare `github.io`, and vouch for every neighbour), so a project whose hall is a SIBLING of its site (site `www.example.com`, hall `builders.example.com`) loses the glyph — the hall stays reachable, the hub stops vouching for the address. [ADR 0253](<redacted>.md) §2’s asymmetry again: **narrowing is free; widening is what costs**. One guard added on the way in: a hostless origin (`file:`, `data:` — a stored value is not a validated one) is refused outright, because the old suffix test would have matched a legal trailing-dot FQDN like `evil.example.` against the bare `.` it leaves behind. **Rejected:** mirroring the check in the browser ([ADR 0182](<redacted>.md) D2a — the page is not the gate, and a second copy of a containment rule is the drift this ends); a per-project allow-list of hall hosts; copying the rule into `project-stats.js` (the cheapest diff, and it re-creates the cause); truncating at the cap (fine for an `<img src>` display bound, wrong for a containment decision); folding both tests into one hostname comparison (it would silently drop a legitimate same-host, different-port hall).](<redacted>.md) | platform-identity / public feed / link containment |
347
-
348
- | 0246 | [**Catalog membership is a projection of the provisioning row, not a side effect of federation** ([task 1003576](https://cloudbongos.com/builders#/task/1003576) · goal 1000046, criterion `proj-detail-interview` / C3). The BV1.R18 proof ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) demonstrated C3 against live and found its last clause — “then it publishes” — structurally false: the owner’s verdict reads `provisioning_instances` while the public map reads `platform_identity_projects`, and the ONLY bridge was `federateInstance → registerClient → upsertProject`, which `shouldFederate` refuses for anything but `co-tenant` (the wizard hard-codes `standalone`) and which runs ONCE, at provision time, through a COALESCE. So a wizard-made project was never catalogued at all and a co-tenant one that answered later never updated — `publishable: true`, `{"projects": []}`, forever. The same missing row is [task 1003580](https://cloudbongos.com/builders#/task/1003580)’s GAP 1: `POST /projects/invite` resolves against `platform_identity_sso_clients`, whose only writer is that same `registerClient`. Underneath is one conflation — federation mints CREDENTIALS and was incidentally the only thing that minted MEMBERSHIP, while [ADR 0182](<redacted>.md) D4 already fixed the map’s gate at IDENTITY, which has nothing to do with hosting topology. **Decision: the catalog row is a PROJECTION of the provisioning row, re-derived at every write that can change its inputs** — two halves, neither sufficient alone. (1) THE ROW SHAPE: every control-plane project gets both rows whatever its hosting shape, and the client row carries NO credential (`secret_hash = ''`, `redirect_uris = '{}'`), so **the shape is the enforcement** rather than a flag a caller could forget — `redirectUriAllowed` refuses `/sso/authorize`, `clientSecretValid` refuses `/sso/token`, `/sso/membership/check-in` and `/sso/activity/rollup`, and the one leg that checked NEITHER (`/sso/device/start`, which resolves a client from a user-typed origin) grew an explicit `clientIsCredentialed` guard. (2) THE TIMING: re-taken on create, on `PATCH …/detail`, on `PATCH …/settings` when the door changes, and on `POST …/domain` (which re-keys the row, retiring the abandoned origin — [ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2’s leak, in the catalog instead of DNS). **NO MIGRATION** — `secret_hash` is `NOT NULL` and `''` satisfies it while hashing to nothing. `description` and `visibility` are ASSIGNED, not COALESCEd (clearing a description must UN-publish); `featured`/`tagline`/`art_url`/`template` are curation and stay untouched, so [ADR 0182](<redacted>.md) D3 holds. It grants nothing — the row is attribution per [ADR 0238](<redacted>.md), ownership stays `provisioning_instances.owner_builder_id` — and because it writes rows keyed on owner-supplied values (a slug, a BYO domain) each write carries its **claim rule in the ON CONFLICT action’s own WHERE**: the client is refreshed only `WHERE secret_hash = ''` (a real client’s origin, which binds the CLI mint audience, can never be rewritten) and the project only `WHERE owner_github_id = EXCLUDED.owner_github_id` (`NULL = anything` is not true, so the hand-seeded rows cannot be claimed). Writing `visibility` does not contradict [ADR 0182](<redacted>.md) D5: the hub authorizes nothing — the authority already ran on the provisioning plane ([ADR 0238](<redacted>.md)’s own-scoped, audited PATCH), and a project the control plane does NOT own has no provisioning row, so D5’s instance report (R08) stays the only channel for those. `tearing_down`/`torn_down` project `hidden`; a `no_address` instance is not catalogued at all. Travels on the kernel port `project.catalog`, the `project.publishGate` seam in the other direction ([ADR 0083](<redacted>.md) forbids the import). Rejected: extending `shouldFederate` to every shape (hands a real secret to instances whose web.env the platform does not write, and keeps the once-at-standup bug); bridging only at the publish moment (leaves the invite with nothing to resolve); registering only at standup (the bug itself); a `catalog_only` boolean column (needs a migration and is weaker than the shape); letting the bridge write `featured`; and backfilling the three existing catalog rows from code.](<redacted>.md) | provisioning / project creation |
349
-
350
- > ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
351
- | 0257 | [**Auth resolves before the hall mounts anything, and a widget's boot read may never navigate** ([task 1003673](https://cloudbongos.com/builders#/task/1003673) · goal 1000063 — *The front door*). An invite-only instance could not admit its FIRST builder, and the symptom lied about where the fault was: an owner saw an empty Access-requests queue and no approve button, because **signing in does not file a request** — only the landing's *Request access* form does, and that form was unreachable. A signed-out visitor to `/builders` was bounced to GitHub, refused as a first-timer ("request access first"), and pointed back at `/builders` to be bounced again. Reproduced against `main`, not just an old pin. `builders.js` was already careful — `/me` is read `softAuth` and a 401 there renders the landing — but `DOMContentLoaded` called `mountHallWidgets()` FIRST, synchronously, and `goals.js`'s mount-time read of `/goals` goes through a `getJSON` with no `softAuth`, which answers 401 by NAVIGATING. The widget's read raced `renderLanding()` and won. **Decision: the front door settles which page this is before anything else runs** — `boot()` reads `/me` (soft), then either renders the landing and stops (nothing mounts, nothing else fetches) or mounts the hall, handing the resolved payload to `loadAll()` so the page still asks once. Widgets are member surfaces (`renderLanding` hid them all after the fact anyway), so not mounting them while signed out removes the CLASS rather than the one instance of it that was found; **a widget's own boot read may never navigate** is the belt beside it (`goals.js` reads soft and renders nothing without a session). A 401 during boot is not an instruction to go and sign in — only a user action is. `tests/hall_landing_boot.mjs` executes the real `builders.js` in a DOM stub and was confirmed to FAIL against the pre-fix file. Rejected: patching `goals.js` alone (it was merely first); making the 401 redirect soft everywhere (an expired session mid-visit SHOULD be sent to sign in — the line is boot vs user action); open enrollment as the cure (removes the gate [ADR 0050](<redacted>.md) chose deliberately instead of repairing the door).](<redacted>.md) | hall-ui / admission / front door |
352
- | 0258 | [**The public CLI is a generated client-only package, and its file list is proven by running it** ([task 1003679](https://cloudbongos.com/builders#/task/1003679) · goal 1000054 — *A newcomer can build without the UI*). The core ships private as `@bongos/core` ([ADR 0108](<redacted>.md)), so a newcomer with no credential can install NOTHING and the web hall is the only way in — the owner's words: "how am I supposed to easily take on tasks as a new builder? I should be able to do everything without the UI." Open task 1002025 proposed publishing the core, which ships the SERVER and bypasses [ADR 0099](<redacted>.md)'s redaction pipeline (dormant until ~2026-11). **Decision: generate a separate public `@cloudbongos/cli` from the core and do not publish the core.** Four load-bearing parts. (1) SCOPED NAME, because npm shares one namespace between org names and unscoped packages — the bare `cloudbongos` is unpublishable *precisely because the owner owns that org*, which npm reports as "invalid" and reads like the name is taken; and a package name cannot be created on the npm website at all (it exists on first publish, which is why "Add Existing Package" answered `Forbidden`). (2) THE FILE LIST IS DECLARED AND PROVEN BY BEHAVIOUR, never computed — a static require-closure CANNOT answer this, because `src/module-api.js` is the doorway a module may only import ([ADR 0083](<redacted>.md)) and it *names* every kernel capability, so a static walk sees 33 `src/` files from `start.js` alone where runtime resolves **one** (`api-prefix.js`, which imports nothing) — the same trap as task 1003677's unsatisfiable done-when. So the test packs the tarball, installs it into an empty dir with NO repo, and runs every verb: it may fail for want of a session, never for a missing file. That caught two real holes on its first strengthened run — `clients/bongos-client/index.mjs`, reached by a dynamic `import()` no `require()` walk can see, and a hand-listed `files` array that dropped `clients/` and produced a tarball which installed cleanly then died on first use; both are now build-time refusals, and the `files` array is derived from the manifest. (3) `claim`/`ship`/`dev`/`serve`/`module`/`upgrade`/`onboard`/`doctor`/`exec`/`package-core` are ABSENT and the CLI says WHY plus the next step (`bongos shell` → a cloud box with the full CLI) — a bare `unknown command` teaches nothing, which is the exact failure being fixed. The supported journey is `login` → `start` → `shell`. (4) A REDACTION GATE fails the build closed on non-loopback IPv4 (RFC 5737 doc ranges exempt), token/key shapes, and every domain + owner login readable from the instance's own `config/branding.json` — needles come from host config so the core carries no instance identity ([ADR 0062 §7](<redacted>.md)); `cloudbongos.com` is allowlisted as public by design. No `repository` field while the core repo is private (it would 404 for every user and publish the owner's login for nothing). 31 files, one dependency (`undici`), version independent of the core's ([ADR 0161](<redacted>.md)). `@bongos/client` is VENDORED, not depended on — a public package depending on a private one is uninstallable. **The owner runs `npm publish`; a builder must not.** Task 1002025 is superseded. Rejected: publishing the core; deriving from a static closure (structurally impossible past the doorway); waiting for the mirror (dormant, and this is a build product not a source release); an unscoped name; depending on `@bongos/client`; a degraded `claim`; shipping all of `src/` to make lazy getters safe.](<redacted>.md) | cli / distribution / public surface |
353
- | 0259 | [**A project’s departure from the public list is public, and the copy says so** ([task 1003672](https://cloudbongos.com/builders#/task/1003672) · goal 1000046 — *Project creation*). Decides the leak [ADR 0255](<redacted>.md) §6.2 deliberately filed rather than implied fixed. The public projects feed publishes a named list plus a count of anonymous rows, so when a project flips to stealth its named row LEAVES the identified block while the count RISES BY ONE — *“Mercury is gone, and there is one more black hole”* names it, and the moment, from a single diff of two snapshots. **Everything [ADR 0182](<redacted>.md) D2a does is defeated by it**, because the identity was never carried ON the anonymous row (`STEALTH_ANONYMOUS_KEEPS` is an allow-list precisely so a column added next year is anonymous by default) — it is carried by the ABSENCE of the named row that used to sit beside it. **Larger than the channel 0255 closed**, which needed repeated sampling and correlation; this one is exact and needs two reads. No `ORDER BY` change touches it: it is inherent in publishing a named list at all, so every fix is a product decision with a visible cost and the owner’s call, not a builder’s. **Decision: accept it, and make the copy say so.** Two reasons it is the right end of the trade and not merely the cheapest — (1) it is ALREADY the honest reading of the existing promise, since D2a made existence and count public *by design*, and a count that is public and truthful is a count whose CHANGES are public; (2) both alternatives damage the control they defend. A **delay window** means the owner clicks the privacy setting and is not private yet *without knowing it*, converting an information leak into a false belief — strictly worse, and the wrong direction for a privacy control. **Decoys** spend the count’s truthfulness (it is truthful today) on a guarantee that is still only probabilistic, and a decoy scheme needs some way to tell padding from real rows — the exact stable per-project key D2a forbids. The promise stealth actually keeps is about the project’s CONTENTS, not the TRANSITION: an anonymous row stays anonymous (no name, tagline, art, origin, or key surviving to the next snapshot); what is not private is stepping off a published list readers may keep copies of. So the copy — which read *“its name, and everything else about it, stay yours”*, every clause true and the paragraph as a whole overselling, because a reader takes it to cover the switch — now names the boundary in BOTH places an owner meets the setting (the choice’s blurb and the what-changes fold), in the plain register 0182’s interview fixed for this card. **A privacy control that oversells is worse than one that admits a limit**, the same standard 0255 §6 set for itself. **No behaviour changes** in `projects-feed.js` — the feed, the redaction allow-list and the keyed rotating permutation are decided FINISHED, not insufficient. `tests/projects_hub.mjs` pins the sentence in both surfaces (the copy IS the fix, so a future edit tightening the blurb must not quietly drop it) and re-applies the no-space-metaphor ban to it. Carries 0255 §6.3’s smaller residual so both live in one place: within one 24h epoch the permutation is stable, so same-day snapshots tell black holes apart (not name them) — accepted on the same reasoning, since the fix is a per-request permutation, which breaks pagination exactly as `ORDER BY random()` was rejected for. Guards recorded for any future attempt: no stable per-project key on an anonymous row, and no anonymous ordering an observer can predict or correlate. Rejected: the delay; decoys; dropping the named list (protects a privacy nobody asked for at the cost of the feature); and saying nothing, which is the failure mode this closes.](<redacted>.md) | platform-identity / privacy boundary / copy honesty |
354
- | 0260 | [**An application IS the consent, and the hub’s own echo is the gate** ([task 1002972](https://cloudbongos.com/builders#/task/1002972) · goal 1000045, criterion C4). Privacy spec **D7** asks that a reviewer see what the applicant’s OWN profile rules would already show, evaluated LIVE at review time. The rule is ONE port composing the two EXISTING views — `getPublicProfileExtras` for a public account, `recruiter-sliver`’s own `sliverShapeFor` for a private one — because a third five-key lookalike **is** the new disclosure class D7 forbids. **The one deliberate difference from the D3 sliver:** `getRecruiterSliver` floors on recruiting REACH, and reusing it verbatim would be wrong in the direction that looks safe — `recruiter_discoverable` defaults to FALSE for a private account, so a private builder who deliberately applied would show their reviewer NOTHING and C4 would be satisfied by an empty box. D7 settles it: *“applying is an explicit act”*. The swap relaxes REACH only; `accountActiveSql` (active **and** terms accepted) and `hide_stats` still floor it, and the proof asserts both directions on the same account so the tempting refactor cannot pass quietly. **The federated route’s gate is the whole security story** ([ADR 0205](<redacted>.md) / security report 1000027): client credentials name a PROJECT and never a person, so `POST /sso/applicant-profile` authenticated alone is a bulk disclosure oracle over every account on the platform, private ones included. `applicationBacksProfileRead` demands a HUB-WRITTEN echo for exactly (this account, this client) inside the same 30-day window — a row only the hub’s own join relay creates — refusing **404, never 403**, and BEFORE any account is read (asserted on the statement log, because a gate that refuses after reading has already done the disclosure work). LIVE means nothing is stored at either end; the queue keys on the VOUCH and never on `github_login` (the one PUBLIC unauthenticated write makes an unvouched login an impersonation surface); and the hub client secret stays in core behind a narrow doorway port. **The path is complete but DORMANT until R09 ([task 1002285](https://cloudbongos.com/builders#/task/1002285)) ships the vouch writer** — nothing writes `applicant_github_id` today, and that dependency was missing from 1002972’s graph. Rejected: routing the port through `getRecruiterSliver`, a snapshot at apply time, resolving by `github_login`, and putting `loadIdpConfig` on the doorway.](<redacted>.md) | platform identity / privacy |
355
- | 0261 | [**A preselect always carries a reason; the bundle’s summary is the floor** ([task 1003684](https://cloudbongos.com/builders#/task/1003684) · goal 1000046 — *Project creation*). Resolves a collision between two rules that were each right alone. [ADR 0243](<redacted>.md) made `adjustments` a **delta** — a team-shape rule that fires without moving anything claims nothing, because a sentence explaining a change that did not happen is a claim the owner cannot check. [ADR 0237](<redacted>.md) says a preselect the owner cannot see a reason for is one they must audit, which is worse than no preselect at all. The two collide whenever a type’s bundle **already contains** what a rule would add: `game` is `[dev-box, discord]` and the `small-team` rule adds `dev-box`, so the rule fires, moves nothing, and correctly reports `adjustments: []`. Confirmed on live core 1.19.580 — `byTeamShape[small-team]` = `{"bundle":["dev-box","discord"],"adjustments":[]}`. The owner then reached step 4 and saw **two extras switched on with no reason beside them**, `#modWhy` an empty hidden div: exactly what 0237 exists to prevent, produced by 0243 behaving correctly. Filed as a dropped adjustment; it was not — the engine is right, and the panel simply had ONE voice and fell silent when the rule had nothing to say. **Decision: the why-line has two voices and the type’s is the floor.** A real adjustment still speaks for itself and is never displaced; when none moved, the type’s own curated `summary` explains the preselect; when the owner has answered the picker themselves the panel stays silent (unchanged — a reason handed back for a toggle they just flipped reads as the panel arguing with them); a bundle with no summary still says nothing, because a floor is not an invention. **No new copy, endpoint or field** — the summary was authored for this job in `starter-bundles.js`, served on every row of `GET /provisioning/starter-bundles`, and rendered NOWHERE in the product until now. 0243 is not weakened: the delta stays a delta and no rule is credited with a change it did not make. The pin `tests/projects_hub_module_picker.mjs` carried a fixture with **no `summary` field**, which made four “no adjustment, no sentence” cases pass for the wrong reason; the fixture now carries the real summaries and those cases assert the new contract, including explicitly that no RULE sentence is invented for a change that did not happen. Rejected: making the rule claim a no-op change (re-introduces the unverifiable claim 0243 removed); widening the bundles so no rule is ever redundant (contorts curated presets for a rendering concern, and the redundancy returns on the next edit); and leaving it silent, declined by the owner once the trade-off was put to them directly.](<redacted>.md) | provisioning / starter bundles / owner-facing copy |
356
- | 0262 | [**A bug never lands in the inbox: the system names the home the reporter didn't** ([task 1003691](https://cloudbongos.com/builders#/task/1003691) · goal 1000086 — *Strict versioning*). `routing.js` returned early on a goal-less filing, so its own `kind='bug'` hard-land branch was UNREACHABLE without a goal — `kind` only ever chose a routed task's STATUS, never whether it routed — and a reported defect sat in `idea_inbox` awaiting the triage pass criterion C1 exists to remove. The premise that [ADR 0250](<redacted>.md) D4 and [ADR 0235](<redacted>.md) contradict each other is FALSE: 0235's exemption is an advisory silence in `goal-advisory.adviseGoal`, and its actual decision already requires a real `goal_id`. So nothing is amended; this is the explicit decision 0235 §6 said would be needed. **D1** a homeless bug gets the version's MAINTENANCE goal (R18's design), find-or-created under an advisory lock inside the route's own transaction — not the catch-all R11 is deleting, and adding no `allowCatchAll` caller. **D2** the gate is the VECTOR (`api`, `discord-bugs`), not the kind: `#ideas` passes no kind, so the classifier guesses one from keywords, and gating on kind alone would silently overturn [ADR 0234](<redacted>.md)'s owner-interview decision. Rejected: goal-less bug tasks (breaks D4 a day after it landed); rejecting the filing (Discord cannot retry, so the report is lost); the catch-all (repopulates the bucket being drained); building R18 whole first.](<redacted>.md) | work intake / idea routing / the version-goal invariant |
357
- | 0263 | [**How a version closes: auto, early, roll-forward, and the maintenance exemption** ([task 1003593](https://cloudbongos.com/builders#/task/1003593) · goal 1000086 — *Strict versioning*). [ADR 0250](<redacted>.md) D5 decided a version *can* close; this is the design pass that makes its five build tasks mechanical, so they do not each invent their own refusal codes and payload shapes and meet only in production. **The asymmetry that shrinks the feature:** auto-close fires when the last non-maintenance goal achieves — so by construction every non-maintenance goal is already closed and the set needing a disposition is EMPTY. All the machinery (the disposition map, the refusal that returns the goals, successor lineage) belongs to EARLY close alone; R16 is small and R12 carries the weight. **Auto-close hangs off BOTH achievement writers** (`db-goals.achieveGoalIfComplete` and the `done-when` cascade — the deliberate duplicate D2 already governs, a require cycle being the reason they are not one helper), on the caller `exec` so it sees the ship's uncommitted flip, and **may never fail a ship**: wrapped and swallowed like `closeCompletedWorkForShip`, idempotent under `WHERE status='building'`, so a failure leaves a version un-closed (self-healing on the next reconciler sweep) and never half-closed. **Early close** is the R10 archive two-step verbatim in spirit — refuse with `version_holds_open_goals` **returning the goals**, then proceed with a per-goal `roll_forward`/`abandon` map and a required `reason`; six named codes, because the callers are agents. **Two briefs are corrected by measurement:** the lineage column is `succeeded_by_goal_id` (migration 160, on the OLD row pointing forward) — not the `succeeds_goal_id` both 0250 and R17's brief name, so R17 needs NO migration; and R18's `is_maintenance` flag is load-bearing rather than cosmetic, because answering the close count with a `title NOT LIKE` would let a hand-titled goal silently exempt itself from the gate that decides when a VERSION closes. Promotion runs inside the close txn and BEFORE roll-forward (`ensureMaintenanceGoal` returns null for a non-`building` version, so a carry-over run first silently carries nothing); zero planning versions is not an error; R19's one migration should carry the `building` AND `planning` partial unique indexes so the promotion race resolves like a 23505 rather than a prayer. Rejected: auto-creating the successor version inside a close (the "fake hotfix version" escape hatch 0250 built the override counter to prevent), writing `limitations/<version>-shipped.md` from the route (file I/O in a ship transaction — it stays R23's CLI work), and a version reopen.](<redacted>.md) | version lifecycle / scope closure |
358
- | 0264 | [**The ten working areas: one goal per area, held until 5,000 builders** ([task 1003696](https://cloudbongos.com/builders#/task/1003696) · goal 1000086 — *Strict versioning*, criterion `sv-cutover-proven-live`). The owner and the full team ran a manual goal review on 2026-09-07 and cut **38 open goals down to 10**, one per working area, each with a named owner and a described 5,000-builder end state: project creation (Rini), account/community (Rini), human project management (Scott), core distribution (Lars/Will), module distribution & economy (Will), the four-role experience incl. agents (Masterqua/Rini), government (Will/Masterqua), the credit economy (Scott), platform analytics (Lars), security (everyone bar Nils and Scott). **Why the rules were not enough on their own:** [ADR 0250](<redacted>.md) D1 closes the goal set of a BUILDING version — which freezes 38 goals exactly as effectively as it would freeze 10. Enforcement cannot shrink a scope that is already too large, so the rules and the cut are two halves of one move; the horizon is a SCALE (5,000 builders), not a date, because that is the question that decides when this set stops being the right set. **The finding that reorders the goal:** none of the cut can be executed today. R10 ([task 1003597](https://cloudbongos.com/builders#/task/1003597)) made `POST /goals/:id/archive` refuse a goal holding unfinished tasks and demand a disposition — correct, and exactly D3 — but the vector that SUPPLIES a disposition is R14 ([task 1003601](https://cloudbongos.com/builders#/task/1003601)), unshipped. Every one of the 12 deletes and every fold goes through that door, so **R14 is the keystone of the whole cut**, not one feature among thirteen, and is built before the rest of the version-close chain rather than in rank order. It looks like a bug and is not: the refusal is the design, landed one task ahead of its own remedy. "Delete" means `archived`, never a row removal (`goals.status` is open|achieved|archived, migration 160; reversible via `/reopen`) — preserving the rows keeps shipped-task attribution and credit history a delete would strand. Three open goals were NOT covered by the review and are surfaced as open questions rather than guessed: 1000003 (the catch-all — already R25's target), 1000065 (assumed to be area 2's community half), and 1000088 (repo cleanup — maps to no area, needs an owner decision). Deliberately does not decide: the areas' priority order, criteria for the six goals R29 must create (inventing criteria for someone else's area is how a goal acquires scope its owner never agreed to), or any task rehoming (R25/R26).](<redacted>.md) | scope / goal set / owner decision |
359
- | 0265 | [**Work with no goal survives a version close, and the order the ten-area cut actually needs** ([task 1003713](https://cloudbongos.com/builders#/task/1003713) · goal 1000086 — *Strict versioning*, criteria `sv-every-task-has-a-goal` + `sv-cutover-proven-live`). `closeVersion`'s post-apply invariant counts GOALS, and every task write inside the close is scoped `WHERE goal_id = $1` — so a task belonging to no goal is reached by no disposition, counted by no re-count, and ships stranded on a version that is over. Not hypothetical: six such tasks already sat on shipped GDS-V3/GDS-V4, and 47 rows on BONGOS-V1 carried a null `goal_id` (R11 required the column going forward but never backfilled, because every catch-all lookup matches the exact title `<VERSION> — general` and BONGOS-V1's is titled 'Work with no goal yet'). Decision: a second invariant in the same transaction refuses a close leaving ANY non-terminal task on the version, failing `CLOSE_LEAVES_STRANDED_TASKS` with the ids; exempting only the maintenance goal (its carry-forward runs in the later `onClosed` hook) — through the SAME `maintenanceGoalExemptSql` the goal count uses, never a second copy — and `__smoke__` fixtures. Consequence recorded here too: because `PATCH /tasks/:id {goal_id}` refuses a cross-version move (409 `goal_version_mismatch`) and `roll_forward` CREATES a successor rather than merging into one, [ADR 0264](<redacted>.md)'s cut must run home-then-close-then-**fold on the planning version**, where task and destination finally share a version. Rejected: a `fold_into` disposition (puts scope decisions inside a shipping transaction), creating the six new areas on the building version via R07 admissions (spends ten overrides to route around the rule they price), and a backfill instead of a gate (fixes today's rows, not the forty-eighth).](<redacted>.md) | lifecycle / version boundary |
360
- | 0266 | [**The Board Room is its own surface, reachable by whoever may vote** ([task 1003734](https://cloudbongos.com/builders#/task/1003734) · goal 1000111 — *Working area 7, Government*, criterion `wa7-government`; owner decision 2026-09-08). The hall's only **upward-pointing** gate ([ADR 0175](<redacted>.md)) shipped as a hash tab inside `/government`, and two things about that did not survive inspection. **The nav never followed the page:** R16 correctly widened the PAGE gate from `government.manage` (archon) to `page.view.government` (metic+) when the page became three rooms — its own comment says *"the shell follows the widest legitimate audience"* — but `shell.js` still declares that item as label Permissions with gate archon, so a Metic who SITS on the board has no nav link at all and the only word in the nav is Permissions. Watch, Harbor, Gate and Sessions each own a nav item; the voting room was reachable only by knowing a URL fragment. **The surface was narrower than the franchise:** `board.vote.cast` floors at XENOS deliberately (`board.js:149` — the floor is low *so that widening the board works*) while the shell demands metic+, so the moment a constitution widens membership below Metic — the exact act [ADR 0175](<redacted>.md) §6 exists to make a CONFIG change — those members may vote by API and cannot load the page they would vote on, reintroducing the code-path change §6 removed. Decision: **its own page at `/board-room`, gated on the same atom the vote route checks.** The floor stays coarse — membership is still checked in-handler per item against that item's own snapshotted constitution — so a builder who clears the floor but sits on no board sees the room and no ballot, which is honest (§9 made the ballot open on purpose). **The migration is client-side because a fragment never reaches the server:** `#board-room` cannot be caught by any server route, so `government.js` redirects on load, preserving `?item=N` — recorded as a decision because adding a regex to `serve-internal.js` looks like the obvious fix and silently never fires. Three writers move (Discord `board-broadcast.js`, the `board_votes` need href, docs), and the redirect stays a release regardless because announcements already in channel history carry the old link forever. A **waiting-vote count** goes on the nav item: the data already exists (`boardVotesNeed` carries a live count pre-filtered to what the vote route would accept, served on `GET /me`), but `shell.js` has NO badge/bell/dot mechanism at all, so one is built once, generically, zero-is-silent, visible to exactly whoever the item is. Found while scoping and filed separately ([task 1003738](https://cloudbongos.com/builders#/task/1003738)): the hall renders needs as a SINGLE slot and `computeNeeds` sorts only by state with a stable sort, so registry order decides inside the `action_needed` bucket and `boardVotesNeed` sits fourth behind `artKeyNeed` — any builder with an unresolved image key never sees the board notice, which was live for this decision's own owner at the moment they asked for the bell. Rejected: a nav item merely deep-linking to the existing tab (cheaper, keeps the hash links free, but leaves the voting room inside a page whose gate answers a different question); a bell in the global header (a second inbox, needing its own read/unread and dismissal semantics); and a SOUND (`alert.wav`/`chime.wav` already ship, so it would be easy — it was not asked for).](<redacted>.md) | government / the board / hall nav |
361
- | 0267 | [**Unanimity, and the revise-and-re-sit loop** ([task 1003733](https://cloudbongos.com/builders#/task/1003733) · goal 1000111 — *Government*, owner decision 2026-09-08). A Full Idea is ratified by **unanimous** agreement from round one; a sitting that does not carry RETURNS to its author, who revises and re-sits it, unbounded, until the board is unanimous. `unanimous` joins `PASS_RULES` as a fourth answer on the axis these rules actually differ on — **what silence means**: it is two conditions, not one, because "nobody objected" is true of an empty room. Everyone who spoke must have said yes AND somebody must have spoken, so an unvoted sitting RETURNS, which is exactly what makes the clock the owner asked for safe under it ([ADR 0191](<redacted>.md) §4: a deadline may only be given to a rule whose expiry means return). **There is deliberately no membership denominator** — that is the whole difference from `majority`, and it is the owner's "silence does not block" as code: an absent member is not in the reckoning, so one yes out of four carries. The stated cost is that an attentive minority can ratify on a distracted board; the alternative is one person on holiday stopping the pipeline. Trigger changes follow from the rule: a single yes does NOT close it (a later member must still be able to object — the sitting runs its clock, full turnout closes early), while a reasoned objection closes it AT ONCE (no vote-changing in v1 fixes the outcome, and the author needs the feedback to revise). ADR 0191 §3's author rule rides both unchanged. **`BOARD_DEFAULTS` is NOT moved**: a fresh instance stays the day-one monarchy, because under any non-author-yes rule a solo founder can never ratify their own ideas (0191 §5) — adoption is a board amendment, and no env var can change a constitution. The revise-and-re-sit loop already existed (`POST /inbox/:id/resubmit` re-grades and re-fires the window hook); what the board owes it is now pinned by test — a return CLOSES the item so the next window may open, and pays nothing. Named limits: a zero-vote return carries no human objection (the author gets the mechanical weakest-section flag), and this changes nothing live until the separate defect where a ratified amendment does not survive to the next read is fixed.](<redacted>.md) | government / board room |
362
- | 0268 | [**The constitution comes from two roots, and a decision that did not take must say so** ([task 1003739](https://cloudbongos.com/builders#/task/1003739) · goal 1000111 — *Government*). On 2026-08-25 cloudbongos.com's board RATIFIED an amendment (rank:metic+ · consent · a 1440-minute sitting) and the constitution never changed — for two weeks `GET /government/constitution` answered the day-one monarchy with the passed amendment sitting in its own `history` array directly beneath the contradicting `board` block. Every sitting since was decided under a rule the board had voted to replace, and it silently un-shipped [ADR 0191](<redacted>.md) (majority) and would have un-shipped [ADR 0267](<redacted>.md) the same way. **Cause:** `modules/government/config.js` resolved BOTH its neutral starter and its instance pack from one `path.resolve(__dirname, '..', '..')`, under a comment saying the roots coincided *today* — true in a single checkout, false on a STANDALONE instance ([ADR 0108](<redacted>.md) §1), where the server runs `node_modules/@cloudbongos/core` with `WorkingDirectory=<instance repo>`. The neutral path stayed right (it really is core content); the INSTANCE path resolved inside the core package, so the host's own `config/government.json` was never read and `applyBoardAmendment` wrote the ratified amendment into `node_modules`, where the nightly core upgrade ([ADR 0161](<redacted>.md) cuts a release per merge) erased it within a day. **Nothing threw and nothing could have** — both paths exist, both are writable, and an absent instance pack is a legal state, so the read fell through to the neutral monarchy exactly as designed. `src/branding.js` had the pattern right three files away. **Fix:** neutral from `resolveCoreRoot()`, instance from `resolveInstanceRoot()`, reached through `src/module-api` — which had exposed NEITHER resolver, and that absence is precisely why the module re-derived the wrong one ([ADR 0083](<redacted>.md) forbids requiring a core internal). Resolved at load, not per call, because `resolveInstanceRoot()` falls back to `process.cwd()` and a lazy resolve would let a `chdir` move the constitution; `applyBoardAmendment` now mkdirs, since the write targets a directory the core does not own. Single-checkout behaviour is byte-identical, asserted rather than claimed. **The detector, which is the durable half:** `constitutionView` carries a `divergence` block comparing the newest PASSED amendment (sanitized as an apply would write it) field-by-field against what is in force, rendered above the hall's dials in words rather than config keys. It REPORTS and never HEALS — the instance pack is also the file a human editing the constitution touches, so a silent re-apply would revert a legitimate hand edit with no way to tell the two apart. A RETURNED amendment is never compared (that is what *returned* means). Does NOT put the 2026-08-25 amendment back in force — that is a board act. Sibling sweep RUN, not promised: one real hit filed as [task 1003745](https://cloudbongos.com/builders#/task/1003745) (quarantine.js's repo-containment refusal is blind to the instance repo).](<redacted>.md) | government / instance roots |
363
- | 0269 | [**The CLI session store is host-keyed at a fixed anchor; the per-brand file stays the active pointer** ([task 1003741](https://cloudbongos.com/builders#/task/1003741) · goal 1000090 — *Working area 4, Bongos Core distribution*). The CLI resolved its session path from the BRANDING PACK, read out of whatever checkout the process stood in — one slot, that moved. Standalone (the public CLI's whole situation) no checkout means no brand, so every instance shared `~/.config/cloudbongos/gds-session.json` and signing into a second DESTROYED the first; in-repo it landed in `~/.config/<slug>/` where the standalone CLI could never find it (`npx … api GET /me` from a bare dir returned the cloudbongos builder while a valid hermeslines session sat on the same machine). Not theoretical — the owner's config dir carries a hand-made `gds-session.<redacted>.bak.json` and four more of the same shape. **Decision: one file per instance keyed by HOST at a FIXED anchor (`~/.config/<FALLBACK_DIR>/instances/<host>.json`), with the per-brand `gds-session.json` left exactly where it is as the ACTIVE pointer.** Four load-bearing parts. (1) The anchor must never read `configHome()`/`configDirName()`/the pack — a test asserts the function body names none, since routing it back through the brand silently reinstates the split. (2) The active pointer does NOT move: every existing reader (in-repo skills, the card hook, the dev box, `readSessionToken`) looks there, and the store is purely additive. (3) The OUTGOING session is archived BEFORE the incoming one lands — a session written before the store existed is not in it, so without this the first login after upgrading still loses it, the bug surviving its own fix. (4) A stored token is VERIFIED against `/api/gds/me` before being reinstated; unverified falls through to the real device flow, `--force` signs in as somebody else. `login` also names the other instances and how to switch, since sessions are now kept rather than overwritten. Files 0600 in a 0700 dir; an `api_base` naming no host is not stored rather than stored under a guess, so nothing can write outside the store. The dir is `instances/` NOT `sessions/` — `sessions` is a live module key and [ADR 0083](<redacted>.md) §Decision #4 forbids kernel machinery naming one; the fitness gate caught it. Rejected: moving the active pointer (breaks every reader for no visible gain); one file holding a map (changes a format hooks and the dev box parse today); keying under `configHome()` (the bug); restoring without verifying (hands over a dead session that looks live); a new `bongos switch` verb (login is the command already reached for, and the public CLI's surface is deliberately small).](<redacted>.md) | cli / auth / distribution |
364
- | 0270 | [**The rank floor derives from facts, not from a prediction** ([task 1003663](https://cloudbongos.com/builders#/task/1003663) · goal 1000111 — *Working area 7, Government*). [ADR 0084](<redacted>.md) derives every task’s claim-time `requires_rank` floor from `touches[]` + `security_sensitive`; [ADR 0049](<redacted>.md) then retired PREDICTIVE `touches[]`, which is now backfilled from the real committed diff at SHIP — after the claim the floor exists to gate. So on every create vector that omits it the matcher saw an empty array, and because the floor is computed ONCE at creation and never recomputed, it returned `xenos` forever. Nothing threw: an empty `touches[]` is a legal state. **Measured before the fix: 88 tasks declared `needs_migration` and 68 of them (77%) sat at `xenos`** — including [task 1003372](https://cloudbongos.com/builders#/task/1003372), a READY task to hash the `builder_sessions` token store at rest, claimable by any newcomer; goal 1000111 alone held 20 such rows. Two mechanisms hid it: the authoring routes DISAGREE (`POST /goals/:id/tasks` REQUIRES `touches[]` as its scope-wall blast radius, so the floor derives there, while its sibling `POST /tasks` — the route `/goal-create` actually files seed tasks through — treats it as optional), and `goal-create/SKILL.md` told authors in ONE line to *omit `touches[]`* and *never hand-set `requires_rank`*: omit the only input, never set the output. **Decision:** the floor derives from facts a task already carries at create time, never from a prediction — `deriveRequiredRank` gains `opts` with two touches-free inputs, `needs_migration` (the direct successor to the `migrations/` entry `POST /tasks` used to fold into `touches[]`) and `scopeProtected` (the owning goal’s `scope_modules` wall, ALWAYS available since `goal_id` is required, read through the same `scopeIncludesProtected` primitive the pre-push hook, grader and main-audit use, so it cannot drift). Derived at the LOWEST create layer in `createTask` after `goalIdEffective` resolves — the ADR 0096 argument — so every vector inherits it and patching the two routes separately (which is how they came to disagree) is not repeated. `updateTaskRequiresRank` re-derives with the same four inputs, else an Archon setting a migration task to `xenos` would silently undo the create-time floor; raise-only throughout; `archon` still never auto-assigned; a strict `=== true` so a stray truthy value cannot wall off the open queue. One-time backfill `core_236` raises still-CLAIMABLE rows only (the floor is a claim-time gate; rewriting shipped rows changes no live decision) and never lowers. **Accepted cost:** a protected goal wall is blunt — any protected module in the wall floors the whole goal, so a `hall-ui` task in a mixed goal floors at Metic too; same semantics the ideas vector has enforced since task 1003066, fails safe, and the remedy is to narrow the wall. Zero claimable `newcomer_friendly` tasks exist today, and no carve-out was added for them on purpose. Rejected: re-requiring predictive `touches[]` (re-litigates ADR 0049); recomputing at ship (too late by construction); deriving from `module_key` (null on most tasks, including the reporter).](<redacted>.md) | permissions / task lifecycle |
365
- | 0271 | [**The ideator surface is two halves: Bongos owns the prose, the harness owns the chrome** ([task 1003506](https://cloudbongos.com/builders#/task/1003506) · goal 1000095 — *Working area 6, Governor / Builder / Artist / Ideator experience*, criterion `wa6-kernel-and-packs`; owner idea 1000732, 2026-08-16). The ask — *no need for an ideator to see all the text and technical work, or the inner Bongos logic* — is two problems in one sentence. **The PROSE half ships here**, through three seams that already existed: a `machinery` field on `interaction-prefs` (per-PERSON, default `show`, riding the live Conductor injection rail — advisory 8, which means task [#1002514](https://cloudbongos.com/builders#/task/1002514)’s claim that `contractFor()` has zero consumers is STALE); the `ideator` directive in `discipline-modes.json` (per-ROLE, the file whose own docs say *to give a discipline its own experience later, add an entry here*); and a new surface section in `/ideate`, which the directive names as the only home of the experience. Default `show` means no existing builder changes. **The CHROME half is the harness’s and is deferred to a spike, not declared impossible** — this record was nearly written on the wrong assumption that config cannot reach it. Two real levers exist: **A**, shipped client defaults (`/focus` renders *only your last prompt, a one-line tool-call summary, and the final response*; `alwaysThinkingEnabled:false` stops thinking being generated; `showTurnDuration`/`spinnerTipsEnabled`/`statusLine` trim chrome) — but `/focus` is fullscreen-only (research preview), `viewMode`’s committability is UNVERIFIED, and thinking suppression is per-model; and **B**, an app-owned surface over `claude -p --output-format stream-json`, whose documented streaming-UI pattern (working indicator on a `tool_use` block, render on `result`) IS the ask, is model-independent, and is the recorded bias. **Bypass is REJECTED** despite the idea proposing it, on three grounds ending in a technical one: the least technical role is the worst audience for removing confirmations; the prompt was never the gate ([ADR 0016](<redacted>.md) — rank is checked server-side, and sub-Metic protected-path globs already narrow an ideator); and a project **cannot** ship it — `defaultMode: bypassPermissions` in `.claude/settings.json` silently does not take effect and starts the session in Manual. Rejected also: a hall ROOM on the Studio’s precedent (goal 1000074’s *a place, not a form*) — right for an artist, wrong here, because the ideator’s complaint is about the Claude session’s surface, which the hall does not render; and waiting on per-session declared role, whose dependency is softer than filed since `discipline` + the claim-time fork already carry the craft axis. When declaration lands the contract MOVES into the Ideator pack with nothing undone.](<redacted>.md) | roles / ideator experience / session surface |
366
- | 0273 | [**The hall nav can gate on a permission, and the Board Room leaves Government** ([task 1003744](https://cloudbongos.com/builders#/task/1003744) · goal 1000111 — *Working area 7, Government*, criterion `wa7-government`; owner decision 2026-09-09). `shell.js` `gateAllows()` resolved six gate keys and every one was a RANK, so a nav item whose page is gated on an ATOM had to borrow the nearest rank floor and hope the two agreed. [ADR 0266](<redacted>.md) ended that: the Board Room page asks `board.vote.cast`, which floors at XENOS deliberately (`board.js` — the floor is low *so that widening the board works*), while its nav item read the Government group's metic floor; the carve filed the gap rather than guessing at it. Latent today because the constitution seats `rank:archon`, live the moment an instance widens membership below Metic — the exact act [ADR 0175](<redacted>.md) §6 exists to make a CONFIG change — at which point those members reach the room by URL and by the `board_votes` deep link with no sidebar entry. **Decision: a `perm:<atom>` gate key, answered by the SERVER.** `NAV_PERMISSION_ATOMS` in `routes/me.js` is a frozen allowlist of the atoms a nav item may gate on; `/me` resolves each through the `government` port and returns `permissions: { atom: boolean }`; `gateAllows` reads that map and never re-derives it (the social-mode precedent — a page that decided for itself could disagree with the rest of the hall); the Board Room item declares the SAME key `requireBoardVotePage` checks, so the two cannot drift. An ALLOWLIST rather than the full resolved set, because the full set is the shape of the instance's governance and a cosmetic sidebar has no business shipping it on every page load. `permissions: null` (government off, port missing, resolver threw) means *cannot say*, not *holds nothing*, and HIDES the link — `default: false` and OFF-is-the-fallback, applied. Cosmetic throughout ([ADR 0016](<redacted>.md)): the page gate and each route's `requirePermission` are the wall. **The placement was the owner's call, and the reason this task existed:** gating honestly while leaving the item in the Government group would put a WIDER item under a narrower head, so a newcomer board member would see a group labelled "Government" holding one link — more authority than they hold. Declined; the item MOVES to the unlabeled project group after Tasks, because a sitting is work the project does, not a monitoring surface. Government's audience, head and floor are unchanged and it is rank surface throughout again. Also rejected: widening the Government head to show whenever any item inside is visible, and a single-item "Board" group. `tests/hall_nav.mjs` gained the MISSING HALF of its group invariant — it checked only that a head is never wider than its items, never that an item is wider than its own head (an orphan link under a hidden label, exactly the declined arrangement) — and `GATE_WIDTH` learned `government`, which had been `undefined` so every comparison against that group passed vacuously. The harness `/me` fixture carries `permissions`, or the item would be permanently invisible in `scripts/hall-preview`. **Not done:** no other item re-gated — `people` still reads `government` for `project.curate`.](<redacted>.md) | hall nav / permissions / government |
367
- | 0272 | [**A craft is an identity, not a toolchain: `artist` becomes core** ([task 1003719](https://cloudbongos.com/builders#/task/1003719) · goal 1000111 — *Government*). Reported from Discord on 2026-08-15 as "cant save preferences of role types in account page"; reproduced live 2026-09-09: `GET /me` returns `preferred_disciplines: ["artist","ideator"]` and `PATCH /me/disciplines` with that SAME array returns `422 invalid_discipline {bad:["artist"]}` — the API handing a builder a value and refusing to accept it back. **Cause:** `updateBuilderDisciplines` validates against `enabledDisciplines()`, and R59 (task 1198) put `artist` in the module half on the reasoning that the craft existed to make the game pixel art, so it rode `art-pipeline`. **That gate had stopped existing** — art-pipeline was never converted to a discovered module, there is no `modules/art-pipeline/module.json`, and so NOTHING declared `contributes.disciplines: ["artist"]`. Measured on a FULL repo checkout with the pipeline present on disk: `["engineer","ideator","ui"]`. The craft was offered by no instance in any configuration, and is unreachable by construction on a published core anyway, since [ADR 0098](<redacted>.md) excludes `modules/art-pipeline/` for a hard copyright reason (ripped reference maps). Meanwhile it stayed first-class everywhere else: the `builders.preferred_disciplines` CHECK (086/176), `tasks.discipline`, and the help-request craft roster all accept it — so a TASK could be an artist task and a PERSON could not be an artist. **The tests were green because they skipped:** the artist assertion sat behind `if (!EFFECTIVE["art-pipeline"]) return;`, always false, so the only real leg never ran. **Decision:** `artist` joins `CORE_DISCIPLINES` → `["engineer","artist","ideator"]` (migration 176 enum order); modules still contribute their own (ui-design → ui). A craft names WHO A BUILDER IS, not which toolchain ships — an instance can have artists without a pixel-art pipeline, and binding the identity to one optional module was the category error. The new invariant, pinned with no skip guard over the widest module set: anything the DB CHECK will store, some instance shape must offer. Rejected: enabling art-pipeline (impossible — absent from the package, and enabling an absent module throws at boot); writing the missing manifest (works only in a full checkout); an instance-declared discipline list (leaves the core CHECK describing a craft core cannot offer, and does not fix the vanilla case); stripping unofferable crafts from stored rows (deletes a builder stated identity to make a validator mistake look consistent). No migration — the rows were always CHECK-valid; only the validator disagreed.](<redacted>.md) | crafts / instance composition |
368
- | 0274 | [**One kernel, three role packs: the always-loaded instructions stop being an engineer's** ([task 1002990](https://cloudbongos.com/builders#/task/1002990) · goal 1000095 — *Working area 6, Governor / Builder / Artist / Ideator experience*, criterion `wa6-kernel-and-packs`). The root `CLAUDE.md` loads in full every session for every craft, and it was an engineer's document: 250 lines / 32,422 chars of claim-before-working, worktree-per-claim, `DEPS_NOT_SHIPPED`, `touches[]` and `/merge-mode`. An ideator and an artist each paid the full token cost of a pipeline they were not going to run and read it as instructions addressed to them — and it CONTRADICTED the same goal's `wa6-drift-stops-the-session`, which says ideation *"needs no claim"*, while the kernel's hard rule said every change is backed by one. The craft-specific half lived in three SKILLS (`/dev`, `/ideate`, `/paint`), the wrong carrier twice over: a skill's description is resident in every session's listing whatever craft is running (27,022 chars against a ~8,000 budget, three of them over-length), and a skill must be INVOKED — a session that never types the command works from the kernel alone, which is the engineer's document again. **Decision: a role-neutral kernel plus one pack per craft under `docs/packs/`, bound by a registry CI checks.** The kernel keeps what is true for every craft (the lifecycle, the money, the trust boundary, the identity split, the charter, navigation) and drops to 216 lines / 22,572 chars, clearing the character target it had been over; the per-line test gains a second half — *is it true for every craft?* The Engineer pack inherits today's methodology; the Ideator pack absorbs `/ideate` and writes down the two grades the criterion names (a quick idea stays ONE LINE, then Claude ASKS whether to develop it into a Full Idea — five questions drawn out of the conversation, in the ideator's own words, scored on completeness); the Artist pack absorbs `/paint`, generalised off the pixel-art pipeline per [ADR 0272](<redacted>.md) — show-first, as little interpretation as possible, and ARRIVE PRECALCULATED, because a session that opens with intake questions is a form. The three skills are deleted. `scripts/gds/discipline-modes.json` becomes the ROLE REGISTRY: a core craft names a `pack` (a file to read), a module-contributed discipline may still name a `skill` (`ui` → `/design`, unchanged as ADR 0272 requires), and `claim.js` prints whichever with the right verb. **`scripts/gds/role-pack-guard.js` HARD-FAILS CI** on a pack path that resolves to nothing, an entry with neither pointer, fewer than three packs, or a pack over budget (16,000 hard / 13,000 target) — hard precisely BECAUSE `claim.js` fails open: a broken path degrades to silence, the role is hollow, and no error says so, so CI is the only gate ("CI budgets each pack"). `docs/packs/` over `.claude/packs/` because `docs/` is already owned by the `methodology` scope key — a new top-level path resolves to NO key, and the scope map's own comment records the cost (a missing key is an empty wall, and goal-scope-check then denies every member-authored task); it is registered explicitly in the default-deny publish allowlist, in lockstep with the registry, or a released core would print an unresolvable pack path on every claim. `ship-card.js` moved with the claim (it would have shipped an "Open the work" button firing a deleted command — the 1003487 divergence in reverse) and `tests/idea_ideate_full.mjs` moved with its subject, its thirteen Full-Idea assertions passing against the pack unchanged. Governor is deliberately DEFERRED — three packs, not four, until the owner decides; the guard floors at three. Rejected: keeping the skills beside the packs (two homes drift, and the listing cost stays); packs AS skills (the exact failure being fixed); duplicating the safety rules into all three packs (the charter forbids it — three copies drift and the kernel becomes the one nobody trusts); leaving the guard advisory like the doc-comment check (that one gates a judgement, this one gates a resolvable path). **Not done:** the kernel is 216 lines against a ~200 target — still a warning, as it was at 250; the line count is the context-budget cluster's work (tasks 1003620/1003687/1003688), which the goal's audit sequenced behind this one.](<redacted>.md) | methodology / roles / context budget |
369
- | 0275 | [**A role's written responsibility has one source, and the pack's copy is generated** ([task 1003732](https://cloudbongos.com/builders#/task/1003732) · goal 1000095 — *Working area 6*, criterion `wa6-written-responsibilities`; statements fixed by the area owner 2026-09-08). The criterion is one sentence — *"one source, shown on the profile, injected into the pack, referenced by grading"* — and before this task the three statements existed only in the criterion record, nowhere in the tree. The interesting half is why ONE source and not three good copies: if each consumer holds its own, they drift, and the drift has a specific unfair shape — the project shows a builder one standard on their profile, teaches their session a second, and grades the shipped work against a third, so someone is judged by a rule they were never shown. **Decision: a frozen map at `src/role-responsibilities.js` (verbatim owner text, imports nothing), read by three consumers, with the one copy that CAN drift generated and CI-gated.** The hall reads it through `clientModules()` — already injected into every served page as `window.__MODULES__` and already carrying the discipline roster the statements are keyed by, so the profile renders with no new route, no fetch and no copy. The grader reads `responsibilityFor()` through the module doorway and states the standard in the prompt as the project's, not its own. The PACKS get a generated block: markdown cannot require a JS module, so their copy is the only one that can go stale — and it is the worst to lose, being what a session is actually instructed by. `gen-role-responsibilities.js` writes it from the ROLE REGISTRY (so a renamed pack, or Governor when the owner decides, needs no edit), and `role-pack-guard.js` fails CI on drift. Three consequences worth the record. (1) `fitness.js` needed an EXEMPTION for the owner's own prose: the ideator's statement opens "Make good ideas" and `ideas` is a module key, so the no-module-keys scan flagged an English word — paraphrasing to satisfy a scanner is precisely what the task forbids, so the reason is written down and the file's import DIRECTION is still guarded. (2) Registering the freshness check inside `checkGeneratedArtifactsFresh` pushed `fitness.js` one line past its 1,500-line ratchet; rather than raise the baseline (which the check forbids in its own message) the assertion MOVED into `role-pack-guard.js`, which already owns the packs — one check now answers "are the packs correct". (3) The generator matches each pack's own line ending, because `.md` checks out CRLF on Windows and LF in CI, and a generator that always wrote `\n` would make the gate red on one checkout and green on the other for a file nobody touched. A craft with no statement (`ui`, module-contributed per [ADR 0272](<redacted>.md); Governor, deferred per [ADR 0274](<redacted>.md)) renders NOTHING everywhere — no empty quote, no grader line — because a grader told a role has no written standard would supply its own. The profile's avatar was repinned to the top row (the head grid is `align-items:center`, fine for three short lines, wrong once the block made the column tall); verified in both themes in the hall preview. `tests/role_responsibilities.mjs` asserts the criterion's actual content by sweeping `src/`, `scripts/`, `modules/`, `tests/` and `docs/packs/` for any second copy. Rejected: three hand-written paragraphs (the drift would be invisible); a dedicated route for the hall (a fetch and a failure mode for static text already on a rail); storing the statements in the DB (not live state — a row puts the project's written standard outside code review); letting the grader describe a craft from its name (the invented standard this removes); raising `oversized_file_count` to let `fitness.js` grow.](<redacted>.md) | roles / grading / generated artifacts |
370
- | 0276 | [**The skill-listing budget cannot hold 64 skills and every trigger, so the number is ratcheted and the choice goes to the owner** ([task 1003620](https://cloudbongos.com/builders#/task/1003620) · goal 1000095 — *Working area 6*, criterion `wa6-role-experience`). Every SKILL.md description is resident in EVERY session for every craft, and `skill-lint` budgets the whole listing at 8,000 chars. It has been trimmed twice ([#1003548], [#1003587]) and grown back both times, because the 400-char per-description aim was a WARNING THAT ACCUMULATES. This task rewrote 49 of the 50 `.claude/skills/` descriptions — 17 warnings to 0, listing 25,624 → 19,316 chars, about 1,570 tokens returned to every session — with every trigger phrase preserved and CHECKED MECHANICALLY (a checker diffed the quoted phrases against HEAD and caught five real losses, all restored; four dropped strings were UI labels, not triggers). **8,000 was not reached, and the arithmetic says it cannot be:** across 64 skills the names (880) plus the mandatory quoted triggers (~5,370) are an irreducible ~6,250, leaving ~27 chars per skill to say what the skill DOES. Reaching the budget means trigger-only descriptions — trading truncation for the loss of the semantic signal routing leans on when the user’s words do not literally match a trigger. **Decision: ratchet `skill_listing_chars` at 19,316** in `fitness-ratchets.js` (fails open if the linter cannot load), so it cannot grow while the real question is open; the baseline is deliberately ABOVE skill-lint’s budget and is not a claim the budget is met. **Open and owner-gated:** delist skills (64 is a menu), scale the budget with the roster, or accept trigger-only text — recommendation is delist then rescale, filed as a blocker. `fitness.js`’s over-budget warning STAYS, as the visible trace of that question. Rejected: raising `LISTING_BUDGET_CHARS` to silence the warning (deletes the signal, not the debt); re-cutting the 14 module ui-design descriptions a week after [#1003587] wrote them, for ~2,000 chars toward a target still 9,000 away.](<redacted>.md) | skills / context budget / routing |
371
- | 0277 | [**A box is "in use" only while a human is attached, and the claim expires** ([task 1003507](https://cloudbongos.com/builders#/task/1003507) · goal 1000095 — *Working area 6*, criterion `wa6-role-experience`). `sweep-idle` skipped any box with `claude_active = true` at ANY age, and `claude_active` is a LATCH, not a level: only a heartbeat ping writes it, and `infra/box-heartbeat.sh` exits WITHOUT pinging when it sees nothing — so silence, the very signal the sweep exists to act on, could never clear it. Reproduced against the shipped selector: skipped at `idleMinutes` of 10, 120, 1440 and **5,256,000** (ten years). Not reaped late; never. The second leg is that `load > 0.2` kept `last_activity_at` bumping every 5 minutes anyway (a devcontainer plus an idle `claude` clears that floor on its own), so EITHER leg alone kept a box alive — which is why bounding only the veto looks like a fix and is not: the incident box's heartbeat was FRESH. Measured: `example-owner`'s box up since 2026-08-12, tmux `otb` UNATTACHED since 2026-08-15 18:01 UTC, `claude` burning 16 min of CPU across 25h of wall clock, **$20.21** of mostly-unattended compute. A THIRD defect, found while testing this and confirmed on clean `origin/main`, made the sweep inert regardless: `loadDeps()` read `_deps` before anything declared it, so it threw `ReferenceError` on first call and every command resolving a DigitalOcean client went with it — including `sweep-idle --apply`, which builds that client before the park loop. **The idle sweep could not park any box at all**, hidden because the suite's only `apply: true` test relied on the veto emptying the idle set before `makeDo()` was reached: one bug shielded by the other. **Decision: a box may not stay active longer than `BOX_UNATTENDED_MAX_HOURS` (12) without evidence a HUMAN was attached.** The heartbeat already computed that signal and folded it into one boolean; it now reports `attached` separately (login session, inbound SSH, open ttyd, or an ATTACHED tmux client via `#{session_attached}` — the signal that separates this box from a working one) and the server stamps `last_attached_at`. `claude_active` keeps its veto but it EXPIRES, bounded by a `claude_active_since` edge stamp. `pgrep -x claude` and the load floor remain reasons the box PINGS, never evidence anyone is THERE — a running process is not a person, and that distinction is the whole decision. The two `NULL` defaults deliberately DISAGREE: `last_attached_at` NULL means "no data" and keeps a pre-1003507 box on the old behaviour (core_238 pointedly does NOT backfill it — a backfilled `now()` starts a clock nothing can advance and parks every un-upgraded box one cap later, and box source sync is not prompt: idea 1000745 records 257 commits behind for three days), while `claude_active_since` NULL is REFUSED because an unknown latch age is the forever-latch itself, and is backfilled so the state is unreachable after deploy. 12h because parking is reversible since task 1002726 (snapshot kept), so a false positive costs one wake against $20.21 for no cap. Both clocks clear at park/wake/deprovision, or a woken box inherits an expired latch and is parked instantly — the fix reintroducing the bug from the far side. Rejected: tracking attachment INSTEAD of bounding the latch (the silent box keeps `true` forever, so the reported hole survives); bounding the latch alone (built first, and the verification probe caught it — the fresh heartbeat meant lifting the veto changed nothing); deleting the load floor (it is what makes an autonomous run count); measuring from `active_since` (that is uptime — parks a box worked on for days); raising `BOX_IDLE_MINUTES` (no threshold reaches an unbounded veto).](<redacted>.md) | dev box / cost / idle sweep |
372
- | 0278 | [**A gated project still takes applications, and the exemption is scoped to the verb** ([task 1003525](https://cloudbongos.com/builders#/task/1003525) · the apply write itself in [task 1003624](https://cloudbongos.com/builders#/task/1003624) · goal 1000106 — *Working area 1, Project creation*; owner decision 2026-09-11). A project's owner sets who may SEE it (`platformVisibility`, [ADR 0192](<redacted>.md)) and who may JOIN it (`joinability`, [ADR 0194](<redacted>.md)) independently — and set to their middle values, members-only AND apply-to-join, the project took no applications at all: the member door refused every cookie-less request with `401` before the public `POST <api>/access-requests` could answer, because that write was not on the exempt list. The two settings composed into **"nobody can apply"**, which nobody chose. It survived because nothing LIED about it — the hub's join box relayed the project's own `401` honestly as `members_only`, and the hall's landing, where the apply form lives, is itself behind the door; the composition was simply unreachable. Found by the R14 proof ([task 1002333](https://cloudbongos.com/builders#/task/1002333)). ADR 0192 §3 had fixed the exempt list at "the door, the manifest, the probes and the downloads — and nothing wider" and left widening it as an owner call, which is what this is. **Decision: yes — and BOTH halves are exempted, each scoped to one path and one verb.** `POST <api>/access-requests` (it grants nothing — an application is a row in a queue the owner still reviews, [ADR 0201](<redacted>.md), already public on every non-gated project) and `GET <api>/access-requests/status` (without it the answer is half an answer: `bongos login` cannot re-poll the device flow after a `not_approved` — the `device_code` is spent — so an applicant would file a request and then wait on an approval they can never observe). `EXEMPT` entries may now be `{ re, methods }` beside the bare `RegExp`s, and `isExempt(path, method)` takes the verb as an OPTIONAL second argument that **fails closed** for a scoped entry when none is given, so the one-argument static callers cannot accidentally widen. **The verb is load-bearing, not tidiness:** the bare `GET` on `<api>/access-requests` is the OWNER'S QUEUE (`requireBuilder` + `access_request.review`), the surface listing would-be builders by name with their vouch state — a path-only exemption would have silently taken the member door off the front of it, leaving one layer where there were two, and the queue's own `requirePermission` still holding is exactly what makes that loss easy to miss. **It opens no oracle the gate was closing:** the status route's boolean twin `GET <api>/auth/web/admission-status` is ALREADY reachable on a gated project inside the `auth/*` subtree §3 exempts whole (§3 records that cost in as many words), and the two share ONE per-IP budget on purpose ([ADR 0209](<redacted>.md)) so neither can be alternated against the other. What it DOES add, stated as the honest cost: applicant detail — `pending`/`dismissed`/`none` over the twin's bare `admitted`. Whether that answer should collapse is ADR 0209's still-open owner question and is deliberately NOT decided here. No hub change: `joinRelayOutcome` maps the RELAYED status, so it carries the project's real answer the moment the `401` stops. Rejected: "gated means gated" — hide *Apply to join* and say so in the manage blurb (coherent, and the call went the other way); exempting the path without the verb; exempting the write alone; collapsing the status response while the route happened to be open (that is how a deferred decision gets made by accident).](<redacted>.md) | project visibility / join door / member door |
373
- | 0279 | [**An upgrade is proven by the served version, not by a health check** ([task 1002884](https://cloudbongos.com/builders#/task/1002884) · goal 1000090 — *Working area 4, Bongos Core distribution*; from idea 1000682). On 2026-08-11 the auto-upgrade sweep printed `✓ upgrade complete — core 1.19.13 → 1.19.56`, wrote a success row to `core_upgrades` and exited 0 while live kept serving **1.19.13**. Three shipped checks formed a closed loop that could not see the failure they existed to catch: the `systemctl restart` failed with `Interactive authentication required` (a `User=` unit, no TTY) and `upgrade.js` treated it as a WARNING and fell through; `readInstalledCoreVersion()` then confirmed the version on **disk**, where `npm install` had correctly put it; and `pollHealth()` got a 200 from the **still-running old process**, because a health check confirms a port is served, never *what* serves it. A lying tool is worse than a broken one — nothing goes looking. The damage outlived the incident: `subscribe.js` had already routed the unattended lane around `bongos upgrade` in favour of `go-live.js`, citing this false-pass in a comment. **Decision: a bump is confirmed by asking the running process what version it is.** (1) A failed restart enters the same auto-rollback path as a failed install/migrate/health and exits non-zero — rolling back rather than merely erroring keeps disk and process consistent, since disk-ahead-of-process is the state that made the incident invisible. (2) The served version is read back from `/version` (`coreVersion`, since 1.17.2, prelaunch-gate exempt), defaulting to the `--health-url` origin and overridable via `--version-url` — derived rather than opt-in because every existing call site passes only `--health-url`, and a check you must opt into is off exactly where it is needed. (3) A **mismatch** fails (proof of failure → roll back); an **unreadable** endpoint only warns (absence of proof — refusing every such bump would regress harder than the false-pass), except under an explicit `--version-url`, which asks for proof and therefore gets a failure. That strict mode is what the unattended subscription lane now passes, its roster already carrying the URL. **Escalation is `sudo -n`, not a hand-placed polkit rule:** `restartService()` retries a failed restart through `sudo -n` when not root — the idiom `dev.js`/`dev-lib.js` already use and consistent with `provision.js`, `upgrade.js` having been the one place that restarted without escalating. The live `/etc/polkit-1/rules.d/<redacted>.rules` mitigation is superseded: a rebuilt box inherits code, not hand-placed `/etc` files (polkit route kept in [`docs/recipes/instance-service-restart.md`](../recipes/instance-service-restart.md)). `--no-health-check` stays the single escape hatch and now waives the read-back too. Rejected: comparing `startedAt` (cannot distinguish a restart onto the same old core from one onto the new); making an unreadable endpoint fatal by default; requiring `--version-url` everywhere (absent from every current call site — the same "off where it matters" failure in a new costume); keeping polkit as the answer.](<redacted>.md) | core distribution / upgrade verification |
374
- | 0280 | [**The unattended lane may drive a co-tenant, because the roster entry IS the per-instance decision** ([task 1003843](https://cloudbongos.com/builders#/task/1003843) · goal 1000106; from [task 1003521](https://cloudbongos.com/builders#/task/1003521), which built co-tenant mode into `go-live.js` and deliberately left this caller alone). `subscribe.js` keeps a SECOND COPY of go-live's target contract, and it drifted: `GO_LIVE_REQUIRED` still listed `deployTimer` after 1003521 made it optional, so `goLiveTargetFor()` returned `missing: ['deployTimer']` for every provisioned co-tenant, and `rawTopologyFor()`'s fixed field list dropped `pinMode` before the target was built — the same stale-normalizer trap that function's own comment was written to fix, one field later. **1003521 deferred this as a policy judgement call; reading the fallback shows the premise was wrong.** `mapped.ok === false` never meant "skip this instance" — it meant "deploy it the WEAKER way": a bare `bongos upgrade`, no backup, no deploy-timer guard, and a pin COMMITTED AND PUSHED into the co-tenant's own repo, which is exactly what `pinMode` exists to prevent. The lane was already auto-deploying co-tenants; the only open question was whether it did so well. A THIRD defect sat behind the other two and would have turned the silent downgrade into a hard failure had only the first been fixed: the normalizer yields `deployTimer: null`, an explicit null SURVIVES `JSON.stringify` into `--target-json`, and go-live's `validateTarget` runs `isSafeToken(null)` on it and rejects the whole target. A FOURTH sat PAST the go-live call and is what separates "reachable" from "safe": after any successful upgrade the lane runs its own pin net, `commitPin()`, which commits the pin files and PUSHES them to `origin` — a net that predates `pinMode`. Under `local-commit` go-live leaves the tree clean so it no-ops by luck; under **`leave-dirty`** the pin is in the working tree BY DESIGN, so the net committed it and pushed it into the customer's own repo — the lane honouring `pinMode` right up to the moment it undid it. **Decision: the lane may drive a co-tenant and needs no new gate, because listing an instance in `config/update-subscriptions.json` already IS the explicit per-instance operator decision** — the roster ships empty, nothing is touched until an operator lists it, and the routine is autonomy-gated (`requiresAutonomy: true`, default OFF) on top of that; a second consent gate asks the same person the same question twice. So: `GO_LIVE_REQUIRED` now MIRRORS go-live's `REQUIRED_FIELDS`, with a test reading BOTH source files and failing on drift; `deployTimer` moves to the optional-field loop so a co-tenant OMITS the key rather than carrying a null; `rawTopologyFor` carries `pinMode`; `resolveGoLive` gains a `needsCoTenant` probe leg; and `goLiveEngineFor()` makes the decision ONCE for both the dry run and the apply path, which had re-derived it separately. The pin net now obeys the same `pinMode` go-live was given — `commitPin()` takes `push` (default true, so the platform instance is unchanged), `local-commit` commits without pushing, `leave-dirty` skips the net, the rollback tidy follows the same rule, and the "not durable" advice stops telling an operator to `git push` a repo we do not own; its refusal to touch a worktree carrying NON-pin changes is unchanged in every mode. **The probe is CONDITIONAL on purpose** — demanding `pinMode` of every target would strip the go-live path from any instance between the `--target-json` release and 1003521's, trading a live regression for a hypothetical one. Rejected: a second consent gate (an `allowCoTenant` flag); deferring to the unspecced fleet control plane ([task 1001948](https://cloudbongos.com/builders#/task/1001948)) — which does not leave co-tenants alone, it leaves them pushing pins into customer repos; dropping `deployTimer` from `GO_LIVE_REQUIRED` and nothing else (the fix as proposed — trades a silent downgrade for a hard rejection); probing unconditionally; reaching co-tenant mode WITHOUT teaching the durability net about it (nominal support that breaks its own promise seconds later); and teaching `loadSubscriptions()` about `pinMode` instead, since that normalizer ships in the control plane's vendored core that nobody bumps — the exact trap `rawTopologyFor` routes around.](<redacted>.md) | core distribution / unattended deploy |
375
- | 0281 | [**An instance’s identity is its own unix account and its own password-authed Postgres role** ([task 1003369](https://cloudbongos.com/builders#/task/1003369) · goal 1000094, audit ref `B3` of the 2026-08-29 security audit). [ADR 0111](<redacted>.md) made standup automatic and [ADR 0128](<redacted>.md) settled how the RUNNER escalates; neither settled who the INSTANCE is once it runs, and the answer was that everybody was the same person. `serviceUnit` hardcoded `User=lars`, and **co-tenant is the default shape** — so on a shared box every instance’s process ran under one uid, and `pool.js` connected by **passwordless peer auth**. Two consequences, neither a bug in any route: a unix user may read `/proc/<pid>/environ` of its own processes, so every instance could read every sibling’s `GITHUB_APP_PRIVATE_KEY`, hub client secret and OAuth secret; and peer auth answers “which unix user are you”, so any code as `lars` could open a pool against **any** database on the box by naming it — including the control plane’s own `builders` and `builder_sessions`. The `0600` `web.env` and the per-instance DB were already correct: sound walls with one door, and every instance had the key. **Decision: each instance gets its own unix account and its own password-authed PG role, both derived from its slug, both created by the provision run.** The account is `bongos-<slug>`, hash-folded past utmp’s 32 chars — prefixed because a project may legally be called `postgres`, and hash-suffixed because two slugs sharing a 16-char prefix would otherwise fold onto ONE account, which is the shared identity being removed. Its home is `/var/lib/<user>` and deliberately **not** under `/home`, since the unit sets `ProtectHome=read-only` and the server writes `builder-secret.key` inside its config home on first boot — a `/home` account would make that `EROFS`. The unit gains `ProtectSystem=strict` / `ProtectHome=read-only` / `PrivateTmp=true` with `ReadWritePaths` as the whole allow-list; **the checkout the service runs is deliberately not writable by it**. The role is `LOGIN` + password, owns its own DB, and is NOT superuser/CREATEDB/CREATEROLE — with **`REVOKE CONNECT … FROM PUBLIC` as the half that actually isolates**, because PG grants `CONNECT` to `PUBLIC` by default and per-role passwords alone leave a sibling one peer-auth hop away. Grants run in a SECOND statement **after** the migrate, since the privileged migrate runs as `postgres` and owns every table it creates; `ALTER DEFAULT PRIVILEGES` aims the same grant forward. The password is minted per run and never stored — the same run sets it on the role and rewrites the `0600` env file, so **every provision rotates it** — and `pool.js` reads `PGUSER`/`PGPASSWORD` **explicitly** rather than via `pg`’s env defaults, because the whole defect was a connection whose identity nothing in the code stated. **Absence is the old behavior:** no password ⇒ no credential lines and no pool keys, so an instance provisioned before this keeps its peer auth and the rollout cannot brick a box. Two consequences worth the record. (1) **The generated SQL contains no double quote, and that is load-bearing** — every literal is dollar-quoted and every identifier goes through `format(%I)`, because these commands are embedded in the dedicated droplet’s cloud-init as `- [ bash, -lc, "…" ]`, a YAML DOUBLE-QUOTED scalar: a plain `"role"` identifier reads fine in `psql` and silently truncates the line. A test asserts the absence rather than leaving it to habit. (2) **A partial, stated honestly** — the account is added to the app-user group so it can read the checkout it runs from (without it the unit fails at exec with `EACCES`), so FILE-level co-tenant separation is not what this delivers; the process and database boundaries are. Rejected: a `DATABASE_URL` in `web.env` (needs no pool change, but trades a stated, assertable identity for a socket-path parse that is `pg-connection-string`’s business); per-role passwords WITHOUT the `PUBLIC` revoke (reads like isolation, and peer auth walks around it); `ProtectSystem=full`, or `strict` with the working directory writable (makes the code the service executes writable by the service, in a change whose subject is containment); keeping `User=lars` and isolating with a second mechanism (the shared uid IS the `/proc` vector); storing the password in `provisioning_instances` (a live credential in plaintext, to buy stability the rotation does not need).](<redacted>.md) | tenant isolation / provisioning / DB auth |
313
+ | 0224 | [**Module-owned skills reach the core checkout as untracked, self-ignored copies with a manifest: the self-copy guard stays, the dev path is a flag, and the file map skips what the module documents** ([task 1003330](https://cloudbongos.com/builders#/task/1003330) · goal 1000083). `materializeClaude`'s self-copy guard is right (a core copying itself is a no-op at best), but Claude Code discovers only `.claude/skills/` at the project root, so a builder IN the core checkout could not invoke any of the nine ui-design skills written there. Decisions: **D1** `node scripts/gds/claude-materialize.js --module-skills-only [--dry-run] [--clean]` calls `materializeModuleSkillsIntoCore`: the same roster, gate and slug rule as step 1b, verbatim copies (no bongosify: the checkout runs `node scripts/gds/…`), a core-owned name kept, a disabled module's excluded; the guard untouched. **D2** untracked and self-ignored: a generated `.claude/skills/.module-skills.json` manifest and a `.gitignore` that ignores itself, the manifest and every materialised dir; each run removes what the last manifest named then lands the current set (byte-identical on repeat; a dropped declaration disappears); `--clean` stops after the removal. **D3** `gen-file-map.js` and `tests/file_map.mjs` skip materialised names by the manifest (a module skill is documented by its module's README, not by the notes sidecar). **D4** init from a checkout that carries the copies still lands them through step 1b, transformed, and never the dotfiles. **D5** pinned end to end in `tests/claude_materialize.mjs`. Rejected: dropping the guard; committing the copies; symlinks or junctions; notes for dirs not in the repository.](0224-module-skills-reach-the-core-checkout-as-untracked-self-ignored-copies.md) | materialiser / skills |
314
+ | 0225 | [**Privacy suppresses cross-project surfaces only: a private builder still appears on their own project's roster** ([task 1003443](https://cloudbongos.com/builders#/task/1003443) · goal 1000075). The accepted privacy spec defined its two flags inconsistently for one case: `hide_stats` suppresses "everything derived from the CROSS-PROJECT rollup" (qualified, so an in-project leaderboard is out of scope) while `account_visibility: private` said "absent from search, LEADERBOARDS, sitemap" (unqualified, so it read as covering the in-project one too) — and PV2 demands "ONE shared predicate used by every public read: … leaderboards …" while `modules/economy/credits.js`'s `leaderboard()` is a bare `SELECT … FROM builders` with no join and no predicate. Read one way that is an unshipped PV2 violation in main; read the other it is correct, and nothing settled which. **Owner decision (2026-08-30): privacy suppresses CROSS-PROJECT surfaces only.** A `private` builder is absent from `/u/<handle>`, platform search, the sitemap, Explore and the recruiting reach, and still appears on the roster of a project they build on — a project's record of its own work is the project's business. Two consequences are the point of recording it: `credits.leaderboard()` is **correct as written** (adding a predicate there would be a regression, not a fix — exactly what a literal PV2 reading would have done), and PV2's "leaderboards" means the cross-project ones. The spec was amended rather than annotated: the D1 row and both `active · public` matrix rows now say *cross-project*, the two `active · private` rows drop "absent everywhere" (the file's most misreadable phrase) for "absent from every cross-project surface", PV2's ship-table row is qualified, and the boundary lands as the **third rule** under D2 beside "`hide_stats` beats everything". Rejected: making `private` hide a builder from projects they build on (it reads as more protective and breaks the ledger — a roster that omits contributors cannot show who built the thing, and credits would accrue to someone invisible on the surface explaining them); and leaving it as a dated spec note (it pins shipped code as deliberately predicate-free, which is an ADR's job).](0225-privacy-suppresses-cross-project-surfaces-only.md) | privacy / spec |
315
+ | 0227 | [**A task network map is refused; the goal graph is the map, and search answers findability** ([task 1003282](https://cloudbongos.com/builders#/task/1003282) · goal 1000072, criterion `legibility-project-map`, which gates itself on this spike). Measured across a swept density range (p=2%–40% of tasks carrying a dependency): a node-link view of ~1,357 tasks is confetti, not structure, and densifying it draws forty goals badly while claiming to show a thousand tasks — a confident picture of nothing is the opposite of the legibility this goal serves. Decision: NO whole-corpus task map at any density; the readable surface is the ~40-node GOAL graph as the entry point with a per-goal DEPENDENCY FOREST as the drill-down, queried when a goal is opened rather than zoomed out of a global map. Every edge a map could draw already exists in the DB and none needs a new table — `task_dependencies` (020), `tasks.parent_task_id`, `tasks.goal_id`, `task_criteria`→`done_when_criteria`, `idea_inbox.promoted_to_task_id`, `idea_inbox.merged_into_idea_id` (`core_195`), `tasks.version_id` (membership, never an edge). **Findability (goal 1000027) answers the owner’s question more cheaply than a map**: the ask splits into “find what I was brainstorming”, which `/recall` (ADR 0060) plus the two lineage columns already answers end to end and which wants a text LINEAGE TRAIL — one thing and its ancestry — not a picture; and “see the shape of the portfolio”, the only half a graph earns, which is about ~40 goals and not about tasks. So the trail outranks the map and is sequenced first. Accessibility: the TABLE is the primary surface and the graph the enhancement (not an `aria-label` on an SVG blob, not a toggle hiding the accessible path), no sideways scroll on the page (the graph scrolls inside its own container), and category is text as well as colour since the ADR 0192 set is not colour-distinguishable at forty nodes. Ranked list: (1) measure the real density against the live DB — it GATES and may cancel the rest, (2) the lineage trail, (3) the goal graph, (4) the per-goal forest. Rejected: view A (every task); view D (densified); a prototype as the output (the criterion forbade it — `spike-task-map.js` is a measuring instrument under `docs/research/`, wired to no route); and building before measuring. Honest limit: the corpus is SYNTHETIC (the live DB was unreachable), so the verdict is robust across the sweep but the specific numbers are model output — which is why item 1 is a measurement against the real DB.](0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md) | GDS / legibility + hall |
316
+ | 0228 | [**The reading room owes a reference its content rules, not a second archetype** ([task 1003473](https://cloudbongos.com/builders#/task/1003473) · goal 1000081). The archetype task 1003306 built for the primer — a linear read with a gate — now serves drachmae, a REFERENCE people open to answer one question about how they get paid. Decision: the same room with ONE body modifier, `room--reference`, switching on content rules in `room.css` §8 rather than a second sheet or a `.page-head--reference` voice: the linear-read instruments are off (no progress hairline, no section counters, no read time; `primer.css`, which holds only the acknowledge gate, is no longer loaded), the outline goes to h3 depth read from the rendered DOM and lights the entry and its part, every h2/h3/h4 is an always-visible address (`.room-anchor`) that copies its link, a deep link is landed by the page after the document, the live section and the fonts (a swap moved the browser's own landing 300px, and `:target` had resolved against nothing), the collapsed phone outline rides under the top bar naming the part, and the live bounty table is a section ahead of the closing links rather than an appendix after them. Found by rendering before touching anything and fixed in the shared reader: `md-reader.js` stopped at `###`, so the doc's six earning sources rendered as paragraphs with a literal `####`; the bold rule refused an inner `*`; a linked heading slugged its target's path into the address. Rejected: a second archetype sheet, hover-only anchors (a phone has no hover), an accordion outline (the sub-questions are what a reader scans for), placing the live table inside the prose (it would lose its outline entry), and teaching the kit stub to serve markdown (the ui-design module's). Pinned by `tests/hall_reference_world.mjs`, four of whose tests run the reader in a vm.](0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md) | hall / design |
317
+ | 0229 | [**The title is the door into a task record, and the record exits to where it was opened from** ([task 1003468](https://cloudbongos.com/builders#/task/1003468) · goal 1000081). The owner, working the live hall: a to-do card "just gets selected", and a record opened from a goal "only offers the tasks page". Both were shipped behaviour: tasks 1759/1869 made multi-select the primary card interaction and left a 26px `--ink-faint` ⤢ glyph as the only way from a claimable card into the record, and `task.js` hardcoded `Open the work board` in three renders with `task.html`'s crumb a fourth. Decision: `.task__title` carries a real link (`.task__link`) on both surfaces that render the card — the convention `.gr-title` and the ledger rows already use — bought up to the 24px floor with the inline-block + padding + equal-negative-margin recipe so the card does not re-space; the card body still toggles selection and bulk claim is untouched; the glyph goes (a second door to the same room, one the owner never found). The link carries WHERE it was opened from as `?from=<path+query+hash>` (`OTBTaskDetail.href`), a query param because the origin must survive a reload and a copied link and because a referrer drops the `#/goal/N` fragment; `task.js` resolves the exit once — `?from=` → same-origin referrer → the work board — points the crumb before the fetch so the way back exists in every state, and labels it the way the sidebar does. A value is honoured only as a path on this host: one leading slash, no backslash, no whitespace, never `/api/`, never this same record. Rejected: opening on the card body (deletes bulk claim), keeping the glyph beside the link, in-memory origin, referrer-first, `history.back()`. Pinned by seven vm tests in `tests/hall_record_world.mjs` §10 and four in `tests/hall_board_world.mjs` §7.](0229-the-title-is-the-door-and-the-record-exits-to-where-it-was-opened-from.md) | hall / design |
318
+ | 0230 | [**A look skill is a composition grammar; the palette is the pack and the texture is a library entry** ([task 1003327](https://cloudbongos.com/builders#/task/1003327) · goal 1000083, criterion `uidesign-skills-in-house`). Four groups of the taste-skill rebuild spec ship a *look*, not a method, and upstream each one shipped a palette and a set of faces — which is the one thing the fifteen-token contract forbids (ADR 0196, 0219). Decisions: (1) a look skill ships **structure, rhythm, type roles, component anatomy, motion and bans, and no colours** — every colour a `var()` or `color-mix()` of the fifteen, the palette the instance's pack, the texture a named entry in `modules/ui-design/styles/`, and no skill adds a face. (2) The brutalist "one substrate per project" becomes an **archetype** deciding which mode the surface is composed *for*, while the other mode ships complete and measured; "never mixed in one interface" is the part that was about craft. (3) `gpt-taste`'s prompt-seeded **random** selection becomes a deterministic one derived from the route and bumped against sibling surfaces, written into a plan block before any markup with the hero arithmetic, the grid-density proof and the measured contrast pairs — a roll nobody can reproduce is not a decision. (4) `stitch-design-taste` **transcribes** the world instead of authoring one: the platform's own `DESIGN.md` frontmatter shape, the thirteen diffed from the pack rather than retyped, and its universal bans stated as defaults the instance overrules. (5) The upstream's seeded placeholder photography is **dropped, not softened** (ADR 0222); (6) motion is CSS inside the Kill Switch and what cannot live there is named *not available*; (7) the pre-flight is the kit and `tells.js` is its mechanical half, so a look skill adds no reviewer of its own. Rejected: shipping each look as a pack, a sixteenth token for a substrate, a fixed seed behind the same roll.](0230-a-look-skill-is-a-composition-grammar-the-palette-is-the-pack.md) | ui-design / skills |
319
+ | 0231 | [**The `/style` session: an owner authors a look inside the contract, and a module skill declares one of two origins** ([task 1003475](https://cloudbongos.com/builders#/task/1003475) · goal 1000083, criterion `uidesign-skills-in-house`). The library ships three looks to choose from; the owner's ask was to *make* one. A look is a small, strict object (thirteen colours, two font stacks, a `DESIGN.md`, one specimen), so what was missing was the loop between the ends. Decisions: (1) `/style` is a **bounded four-step session** - SEARCH, CREATE, PROVE, ADOPT - and nothing else. (2) SEARCH **renders before it asks**: the nearest two entries shown through the kit (`/` and the hall's light tier), then two or three questions about *feel*, the light-only fact of a pack stated up front, and adapt-or-draft declared in one sentence before a file is written. (3) CREATE stays inside the contract or refuses - never a fork of a sheet, **never a sixteenth token** (a radius, a shadow, a glass are a sheet's derived tier; a look that needs one is an ADR). (4) PROVE is **arithmetic, not taste**: the pack floor, the three token suites under the pack, `check-mock`, the renders, with a failure-to-fix table so the retune is a lookup, and the four green lines pasted as evidence. (5) ADOPT hands the owner one step and **never writes `config/branding.json`** - the host boundary is theirs (ADR 0062 §7) - but does file the catalogue rows under the claimed task. (6) A module skill declares **exactly one of two origins**, `## Rebuilt from` (ADR 0198 / 0220) or `## First party` (no upstream; the authoring task, this ADR, and why it exists), and the pins branch on which - a fabricated spec citation would destroy the provenance the section exists for. (7) The session's first output is its own proof: `blueprint`, the library's first cool look, cleared the pack floor and all three suites on the first measurement. Rejected: a fabricated Rebuilt-from, a session that repaints the live instance, filing the catalogue rows "later", a palette generated from a reference image.](0231-the-style-session-an-owner-authors-a-look-inside-the-contract.md) | ui-design / skills |
320
+ | 0226 | [**One stale-claim timer; reporting degrades where enforcement refuses** ([task 1003476](https://cloudbongos.com/builders#/task/1003476), goal 1000072, criterion `legibility-sort-and-staleness`). "How long may a claim go silent" had FOUR answers and none of them was the project's: the sweeper that actually releases a claim read `GDS_STALE_CLAIM_HOURS` (else 6h); `GET /builders/roster` and `GET /builders/:id/active-claims` each hardcoded 24h in byte-identical blocks; and the two `session_pulses` helpers defaulted to 30 minutes. The 6-vs-24 pair is the harm: the roster is the screen an Archon opens to decide whether to intervene, and for **eighteen hours** after the sweeper had released a claim, stamped its session log `[auto-released:stale]` and returned the task to `ready`, that screen still showed it healthy — while the roster's own comment claimed to "mirror" the sweeper, true of the definition and false of the number. Decision: ONE resolver, `src/stale-timer.js` (a KERNEL file, the `instance-config.js`/`branding.js` category — host config a module reads, never hardcodes), reached by modules through the doorway and by core routes and the CLI directly. Precedence: explicit override (`--threshold-hours`, `?stale_hours=` — a LENS, labelled `query`, never a reconfiguration) → `<PREFIX>_STALE_CLAIM_HOURS` via `instance-config.resolveEnv`, whose legacy prefix chain still honours the live `GDS_STALE_CLAIM_HOURS` so **nothing in a running deployment has to change** (and which is the channel ADR 0190's runner already patches into `web.env`) → **6**, the SWEEP's historical number and deliberately not the roster's 24, because the sweep is the surface that acts and unifying on 24 would have made every project three times more patient with a dead claim under cover of a refactor. Bound 1..720h — the range `/builders/roster` has published since V3.R14; one timer, one bound. Every surface that prints the timer prints its PROVENANCE (`stale_hours_source`, the sweep header, the auto-release note): a number without it is exactly what let 6 and 24 coexist unnoticed. **The fail posture is asymmetric and that is the decision**: the resolver reports an unusable value rather than throwing, then a surface that only READS degrades to the default and names the bad var (500-ing the roster an Archon opened to find the problem would hide the problem), while the SWEEP refuses to run at all (it releases other people's claims and stamps their logs; no reload undoes that) — a surface that only reads may guess and say so, a surface that acts may not guess. Both routes now share one `staleHoursFor(req, res)`; two copies of a literal is how the drift happened twice inside one file. Rejected: a new `config/stale.json` tier (the env route already IS the per-project channel); making 24 the shared default; throwing from the resolver (forces a second reader — the original bug); folding in the boards' 14/30/60-DAY task buckets (a different question — days, about a task, a browsing lens with no enforcement behind it); a DB row + settings UI (a reasonable next step, but the defect was four numbers, not a missing knob — a row added before the readers agreed would have been a fifth).](0226-one-stale-timer-reporting-degrades-enforcement-refuses.md) | GDS / claims + ops |
321
+ | 0232 | [**Rot is derived, not swept; and `water` is the only new verb** ([task 1003279](https://cloudbongos.com/builders#/task/1003279) · goal 1000072, criterion `legibility-sort-and-staleness`). The inbox half [ADR 0226](0226-one-stale-timer-reporting-degrades-enforcement-refuses.md) split out and left open. Owner decision (2026-08-25): a goal or task past the timer raises a **needs-attention line** — not an automatic status change (it mutates real work with no human in the loop, and a wrong timer then silently freezes it) and not a nightly digest (a notification is not an action, and one you scroll past is indistinguishable from the silent decay it was meant to fix). The build note asked for an idempotent poller; the decision is that **there is no poller and no items table** — a line is a `SELECT` comparing the thing's rot clock to the timer, so "exactly one item per stale thing, not one per sweep" is true by arithmetic rather than by deduplication code that must be written correctly and then stay correct, and clearing falls out for free (an edit moves `updated_at`, a decision moves `status`, watering moves the mark). A stored item would have been a cache of a query whose inputs it could not observe. **The exclusions are the load-bearing part**: rot is work nobody picked up (`backlog`/`ready`, and `open` goals), and `active` is deliberately excluded because the claim-silence timer already reports it in hours — listing it here in days would put one rot in two inboxes under two numbers, the 6h-vs-24h split one tier up; `blocked`, `completed`/`confirmed` and the terminal states are excluded for the same reason, each naming the surface that owns it. The clock is **not a new definition of touched** — it is `task-lib.js`'s published `max(updated_at, shipped_at)` (task 1003278) plus a water mark, pinned by a test asserting the domain expression appears verbatim in migration `core_231`'s index expressions, because drift there is silent. The timer is a **second** timer and defends itself as a different question (days vs hours, DB vs env, asks a human vs releases a claim), stored in a new unseeded generic `project_settings` table — no row means "never configured", which is what lets a settings page tell that apart from "set to the default" — with ADR 0226's precedence and its reporting-degrades fail posture, and an unreachable DB explicitly not treated as a bad value. Three of the four verbs (**prioritise/prune/kill**) reuse `promote`/`demote`/`abandon`/`archive`, the task-1003066 precedent, so they add no permission; only `water` is new, gated on ownership OR `builder.roster.read` inside the handler (a creator holding no atom must still get through). Verbs are **served with each row** rather than hardcoded, since a goal legitimately gets two where a task gets four. Rejected: an items table, a sweep, a digest, auto-demotion, and a fourth verb on goals that would render a dead button.](0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md) | GDS / legibility + hall |
322
+
323
+ | 0233 | [**A copy proposal is a task carrying a patch, and the applier is the only writer** ([task 1003114](https://cloudbongos.com/builders#/task/1003114) · goal 1000074, criterion `artist-edits-land-as-tasks`). Goal 1000074 asks for two things that pull against each other in its own text: the artist must change copy WITHOUT an engineer, and there must be NO LIVE CMS because every copy change is a claimed, graded, shipped task. R02 shipped the half that needs no reconciliation (the flag) and [ADR 0178](0178-the-copy-desk-flag-and-queue.md) §3 pinned the absence of any column that could hold replacement text, naming this task as the one that would have to keep it true. Decisions: (1) the shape is **borrowed from the design half, not invented** — ADR 0081 already has a tool authoring and an `adapter.export()` landing the output as a committed diff under a claim inside a path guard, so the artist is the tool, the copy desk is where they author, and the proposal is the export. (2) A proposal is a **TASK carrying a fenced `copy-proposal` patch**, never a row: the ledger is where a proposal belongs anyway, nothing renders a task description as product copy, a reviewer can read the change without running anything, and `copy-desk` gains NO migration (the obvious-but-wrong R03 is a `copy_desk_proposals` table, which is the forbidden column with extra steps). The module gains `consumes: ["lifecycle"]` and calls `createTask` through the port — it can put a request INTO the ledger and nothing can pull copy OUT of it. (3) `scripts/gds/copy-apply.js` **re-derives the position** rather than trusting the patch, which required teaching R01s extractor to record a source SPAN and deliberately dropping it from the committed registry: an offset goes stale the moment anything above it changes, while the id is designed to survive exactly that movement, so the span is recomputed at the moment of the edit by the same code that decided the string was copy. (4) The **placeholder arithmetic** is what makes a patch mechanical rather than a note for an engineer — the applier puts the sources own `${expr}` back, k-th hole to k-th expression, so an artist may MOVE a value within a sentence but never add or drop one, checked at proposal time and again against the real source. (5) Every ambiguity is a **named refusal** (`target_gone`, `ambiguous_target`, `no_exact_span`, `placeholder_mismatch`, `path_outside_surface` — the design adapters path guard, derived from R01s own SURFACES), because a wrong edit is worse than no edit; after writing, the applier regenerates the registry and checks that what it wrote is what the inventory now reads. (6) Proposing is **open to every rank** (an artist is a craft, not a rank) and lands at `backlog`, so a proposal is a request that passes ordinary triage. Rejected: a proposals table, a column beside the flag, trusting the patchs line number, an escape-aware index map to buy spans for nine strings, auto-closing the flag a proposal answers.](0233-a-copy-proposal-is-a-task-carrying-a-patch.md) | copy-desk / the artist loop |
324
+ | 0234 | [**Idea routing: capture-time promotion, the landing matrix, and the homeless-only inbox** ([task 1003064](https://cloudbongos.com/builders#/task/1003064) · goal 1000071, "Idea routing"). Idea 1000771 named the failure: Discord `#bugs` created tasks DIRECTLY against a hardcoded, already-shipped version — no `captured_by`, no lineage, so the ideator-credit lane had nothing to pay against — while every other open idea sat waiting for a human triage pass that dropped its promotion into a version catch-all, because nothing in capture ever named a goal. Records what R01/R02/R05/R07/R13 shipped. `captureIdea` (`modules/ideas/inbox.js`) stays the ONE capture chokepoint — routing is one optional `goalId` argument, never a second creation door — and when the matrix routes, `routeIdeaToGoal` writes the idea row, the task (via the `lifecycle.createTask` port) and an `ideas_task_lineage` row (`origin='route'`) in ONE transaction, so a route can never mint attribution-free work the way `#bugs` did. `modules/ideas/routing.js`'s `decideLanding` is PURE and fail-closed: a bug from a Metic+ filer hard-lands `ready`, a sub-Metic bug or any other kind with a goal soft-lands `backlog`, a Full Idea or `criterion-proposal`/`decision` never routes, no goal named stays the pre-routing inbox path. `authorizeRoutedLanding` is the BINDING gate underneath the matrix's decision — a goal reaching protected scope raises the task's rank floor to Metic and REFUSES a sub-Metic filer outright, while membership is deliberately NOT required (reporting isn't authoring). `core_222`'s `suggested_goal_id` is an advisory hint only — the real home is `tasks.goal_id`, set by construction — and `/idea-triage` (`scripts/gds/triage.js`) now sends `goal_id` on promotion, so the inbox is homeless-only. Divergence recorded: R11 (Discord `#bugs`) hard-lands a linked Metic+ builder's bug immediately through the SAME shared matrix rather than the spec's "always soft-land, pending queue" design, because a local copy of the rank split would only drift from the one definition; and R12 (Discord `#ideas` hints) is NOT YET SHIPPED, so `#ideas` carries no goal hint today despite the interview decision that it should, hint-only, never route.](0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md) | ideas / lifecycle |
325
+ | 0235 | [**A fix category is `kind='bug'` under a goal, not a new column** ([task 1003215](https://cloudbongos.com/builders#/task/1003215) · goal 1000071, "Idea routing" — the structural half of idea 1000828; task 1003214 shipped the advisory's first quiet case, task 1003157 extended it). A fix for something that should already work needs a home under an existing goal WITHOUT a new goal being born and WITHOUT a stored column restating what `tasks.kind` already says. Not a global "Bugs" goal either: a goal spanning every module makes the [ADR 0086](0086-goal-scoped-work-hierarchy.md) scope wall either everything or nothing, and a goal that can never reach `achieved` (a bug container never finishes) pollutes the exact signal [ADR 0183](0183-criteria-close-themselves.md)'s auto-satisfy depends on. Not [ADR 0192 "a category orients and authorises nothing"](0192-a-category-orients-and-authorises-nothing.md) either, despite the structural resemblance: that decision forbids any gate from branching on a goal's category, while THIS decision is two decision surfaces deliberately branching on `kind='bug'` — `adviseGoal()` (`modules/lifecycle/goal-advisory.js`) goes silent for a bug under all three of its reasons (`no_goal`, `catch_all`, `no_criterion_link` — "a fix is the goal's 'fix category', not a delivery against one of its stated outcomes," its own doc comment) and `countTasksWithoutCriterionLink()` (`modules/lifecycle/db-deps-criteria.js`, surfaced on the hall's goal detail view) excludes `kind='bug'` from its count for the same reason. No hall/status change was needed for GROUPING bugs under a goal: every task card already renders its `kind` as a visible tag and the goal detail view's Filters already offer a `Kind` lens with `bug` as a value — verified against the live code, not built new. Rejected: a `tasks.category`/`fix_category` column or enum, a global cross-module "Bugs" goal, a bug-specific grouping widget beside the general-purpose kind facility that already works.](0235-a-fix-category-is-kindbug-under-a-goal-no-column-required.md) | ideas / lifecycle |
326
+ | 0236 | [**Many ideas → one task: the lineage key is the pair** ([task 1003515](https://cloudbongos.com/builders#/task/1003515) · goal 1000071, "Idea routing" — promoted from idea 1000815; the table was built by [task 1002997](https://cloudbongos.com/builders#/task/1002997)). `ideas_task_lineage.task_id` was the PRIMARY KEY on the reasoning that "a task descends from at most one idea", and `linkIdeaTask` wrote `ON CONFLICT (task_id) DO UPDATE SET idea_id = EXCLUDED.idea_id` — so promoting a SECOND idea into a task that already had a link **silently replaced the first idea's lineage**: no error, no warning, no log line. Two ordinary triage promotes at one task were enough, and it happened on prod on 2026-08-21 (ideas 1000811 + 1000812 → task 1003150). Because this table is the rail [ADR 0172](0172-per-craft-compensation-ideator-credit-lane.md) stream 1 pays against, an overwritten row is an **ideator not paid** for work their idea produced, with nothing recording it — and it makes merging duplicate reports into one task a LOSSY move, which is the triage the inbox exists to encourage. The R01 grader named this shape and was told it was unreachable; it was unreachable on the ROUTING path examined and fully reachable on the ordinary PROMOTE path. **Decision: many ideas → one task is a real relation, so the key is the pair** — `ideas_006` re-adds `ideas_task_lineage_pkey` as `PRIMARY KEY (task_id, idea_id)` and both INSERT sites `DO UPDATE SET origin`, never `idea_id`. Refusing the second link would turn a legitimate merge into an error; logging the replacement keeps a lossy ledger. **Where one row is still required the EARLIEST link wins** (`ORDER BY created_at, idea_id`, stated in SQL rather than left to the planner): `sourceIdeaForTask`, `credits.awardIdeaTaskShipCredit`, and the nod queue's one-row-per-task predicate — one constant shared by the list and its count, so the pill can never disagree with the rows. Consequences: a task subsumed from N ideas **counts once toward each idea's** shipped-task credit (and cannot double-count within one — the pair is unique); the per-ship payout still names one idea, the earliest, because paying every co-source ideator is a decision about total worth, not a bug fix; the migration **backfills** from `idea_inbox.promoted_to_task_id`, the weaker pointer that survived each overwrite, labelling restored rows `origin='promote'`; and `updateIdea` still swallows a lineage failure but now reports it as a `LINEAGE GAP` through `api.logger` — a silent swallow on top of a silent overwrite was two layers of one mistake.](0236-many-ideas-one-task-the-lineage-key-is-the-pair.md) | ideas / economy |
327
+ | 0238 | [**The join door is a provisioning SETTINGS key, and the hub column stays the cache** ([task 1002324](https://cloudbongos.com/builders#/task/1002324) · goal 1000046, criterion `proj-visibility-states` — R05). [ADR 0182](0182-project-visibility-two-axes.md) D5 says R05 "moves host — an instance surface, not a hub one", and on the tree both halves of that sentence pointed at nothing: `platform_identity_projects.visibility` (which R04 shipped) is written by **no route** — every write on `routes/projects.js` is the `project.curate` CURATION gate with no ownership check, `upsertProject` deliberately omits the column, and `owner_github_id` there is documented "advisory attribution (not an authority)" — while "the project's own hall settings" is a page that does not exist (`settings.html` is per-BUILDER; the hall's my-projects page was deleted by task 1003308). **Decision: the door is a key in `provisioning.SETTINGS_VOCAB`** (`visibility ∈ public|private|stealth`, default `public`), set through the existing own-scoped `PATCH /provisioning/instances/:id/settings` and rendered as one more `.mSec` section on the manage view an owner already uses. One line in the vocabulary buys the whole task from machinery already proven: `instanceAccessDenied` (own-scoped, Archon override, **404** not 403 — no existence leak), a validator that IS the vocabulary (so an off-list value 400s and a key can never drift out of the schema), a `provisioning_events` audit row, the `<PREFIX>_VISIBILITY` env the runner patches into `web.env`, and `src/branding.js` mapping it onto `project.visibility` in the public manifest. It is a **POLICY** key, not a platform-drawn one like the planet, for two reasons that outlive today: ADR 0182 D6 makes the door the CEILING `joinability` may only narrow and D1/D5 put admission authority on the instance — a door the instance cannot read cannot be a ceiling — and a key with a DEFAULT belongs in the defaulted vocabulary (`PLANET_VOCAB` keys are nullable because null means "the platform picks", and there is no such thing as the platform picking your join door). Nothing here writes the hub column: it stays ADR 0182 D5's cache, and R08 is the report that will feed it — so today an owner's pick is stored, validated, audited and delivered to their instance, and does not yet move the orb (that gap is R06/R08's, named so the next builder does not hunt a bug). The section deliberately ships **no "what your project runs" read-back line**, unlike its two siblings: a deployed project reports no door until it takes a release carrying the field, and a line that could only ever say "this project's server doesn't report this yet" is worse than none. Rejected: a new owner-scoped write route on the hub catalog (D5 forbids the hub authorising the change); a new instance-side settings page on its own (right long-run home, but today a control with no consumer while R08 is unshipped); a `visibility` COLUMN beside `description` (that one earned a column as half the publish gate and is read by SQL — a closed-vocabulary knob is what the jsonb store exists for); and `PLANET_VOCAB` to dodge the restart (a quieter save today, the ceiling rule lost tomorrow).](0238-the-join-door-is-a-provisioning-settings-policy-key.md) | provisioning / public-landing |
328
+ | 0237 | [**A starter bundle is a preset over the always-on core, keyed by the declared project type** ([task 1002338](https://cloudbongos.com/builders#/task/1002338) · goal 1000046, criterion `proj-module-select`). "During setup a default set of modules is pre-selected" can only mean the OPT-IN modules, since every instance already boots with the same sixteen `default: true` core-domain ones — so a bundle is a short list of opt-in module keys layered on that floor, preselected and freely toggled off by R20's picker ([task 1002339](https://cloudbongos.com/builders#/task/1002339)). **The key is the DECLARED type, and the seeded vocabulary is stale:** the task text (2026-07-17) names *Game / SaaS-Tool / Community*, but task [1002772](https://cloudbongos.com/builders#/task/1002772)'s owner decision (2026-08-13) fixed the live closed set as `game · research · business · non-profit · not-sure` — two CHECK constraints, two `PROJECT_TYPES` constants and five wizard buttons — and "community" survives only as a `team_shape`. Two constraints ride the key: the arrow runs **type → modules, never back** (1002772 rejected inferring type from module flags, which flip all through setup), and the R15 interview must **not re-ask** what `type` declares, so R19 adds no `PROJECT_DETAIL_VOCAB` key and no migration. `not-sure` and *undeclared* resolve to the same modules but are deliberately different answers — `type` is nullable precisely so "nobody said" is never back-filled with a guess, and the resolver returns `type: null` with its own copy. **The declaration is pinned by a test, not a runtime catalog read:** a module may not import `src/module-loader/catalog.js` (the one-way rule, [ADR 0083](0083-modular-architecture-strangler-build-plan.md)), so `modules/provisioning/starter-bundles.js` stays PURE like `onboard-plan.js` beside it and `tests/provisioning_starter_bundles.mjs` reads every `modules/*/module.json` off disk — failing the build if a bundle names a module that does not exist, if `ALWAYS_ON_CORE` drifts from the manifests' `default: true` set, if a bundle names an always-on module (a preselect that toggles nothing), or if it names one of the two hub-only modules. The bundles are `game → dev-box, discord`; `research → agents`; `business → agents, dev-box`; `non-profit → discord`; `not-sure → —`, and they are SHORT because today's opt-in catalog is three modules wide and a preselect an owner must audit is worse than none. Served whole at public `GET /provisioning/starter-bundles` (`{ core, bundles }`, same absent gate as `onboard-plan`) so the picker re-preselects without a refetch and R17's recommendations read the same shape. A bundle is **never a publish precondition** — [ADR 0182](0182-project-visibility-two-axes.md) D4 fixes that bar at name + description alone. Rejected: a runtime catalog read, a `bundle` key in the interview vocabulary, inferring the bundle from the description (the deferred AI layer, [task 1003500](https://cloudbongos.com/builders#/task/1003500)), and keeping the seeded Game/SaaS-Tool/Community triple.](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md) | provisioning / project creation |
329
+ | 0239 | [**A recommendation is a table that carries its own reason; governance is a preview and invites are a seam** ([task 1002336](https://cloudbongos.com/builders#/task/1002336) · goal 1000046, criteria `proj-module-select` / `proj-governance-reco` / `proj-invite-builders`). R19 ([ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md)) declared the per-type starter bundle and said outright it is "a declaration and not a rule engine — R17/R22 own the layer that reasons"; this is that layer, and the goal pins it **rule-based**, with the AI/embeddings pass deferred ([task 1003500](https://cloudbongos.com/builders#/task/1003500), idea 1000552). **Two inputs, and the free-text description is not one of them:** the DECLARED `type` (a column) and `team_shape` (the one closed-answer key the R15 interview collects, [task 1002334](https://cloudbongos.com/builders#/task/1002334)) — inferring from prose is the deferred layer ADR 0237 §7 already rejected once. The engine is three frozen tables (`TEAM_SHAPE_RULES` add/remove ONE opt-in module; `GOVERNANCE_RULES` name a preset; `INVITE_RULES` emit a seek) and **every row carries its own `reason` sentence**, shipped with the effect rather than reconstructed by whichever surface renders it — a recommendation an owner cannot be told the reason for is one they cannot disagree with. `bundle.adjustments` is a **DELTA**: a rule whose condition matched but whose module the bundle already carried records NOTHING, because "we switched discord on because you said community" over a module a game would have had anyway is a claim the owner can check and find false. **Governance is a named preview, never a setting** — the governance module is planned and unbuilt (goal 1000030), so a preset is a name plus one sentence of what it feels like (`owner-only` / `trusted-team` / `open-contribution`) and every result carries `applied: false`, one flag instead of the same fact hardcoded in R25, R18, the wizard and the project page. **Invites are a SEAM**: `suggested` is always `[]` because ranking builders means querying the leaderboard in the sibling `economy` module, which the one-way rule ([ADR 0083](0083-modular-architecture-strangler-build-plan.md)) forbids importing — the engine emits `seeking: { kind: 'top-builders', limit }` (or `null` = suggest nobody, a real answer) and R28 ([task 1002347](https://cloudbongos.com/builders#/task/1002347)) turns it into names. **Skipping is an answer:** every table states a default and each is the NARROWEST option (widening later costs one setting; a role granted to strangers is not recovered by setting it back), an off-vocabulary `team_shape` reads as no answer exactly as `normalizeProjectDetail` drops it, and the route answers 200-with-defaults rather than 400 — refusing would make the supported path ([ADR 0182](0182-project-visibility-two-axes.md) D4) an error. `tailored: boolean` is the machine-readable half of `proj-detail-interview`'s details-vs-skip split that R18 ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) proves. Served at public `GET /provisioning/recommendations?type=&team_shape=` (same absent gate as `starter-bundles`; no database, pinned by a `pool`-throws stub), returning ADR 0237 §5's bundle shape plus `adjustments` so a recommendation and R20's preselect can never name different modules. Rejected: reading the description, filling `suggested` from `GET /public/leaderboard`, a weighted scoring function, a reason per matched condition, and a 400 on an unknown shape.](0239-a-recommendation-is-a-table-that-carries-its-own-reason.md) | provisioning / project creation |
330
+ | 0240 | [**A skipped module picker stores nothing, and the bundle is resolved on read** ([task 1002339](https://cloudbongos.com/builders#/task/1002339) · goal 1000046, criterion `proj-module-select`). [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md) declared the starter bundles and closed with "where a chosen selection lands is R20's decision". It lands in a new **nullable** `modules jsonb` column on `provisioning_instances` (`provisioning_014`), read through `starter-bundles.effectiveModules(row)` and **three-valued**: `NULL` = nobody answered (the picker was skipped, or the caller does not ask) and the declared type's bundle is resolved AT READ TIME; `[]` = the owner switched them all off, a real answer; `["dev-box",…]` = what they picked. `NOT NULL DEFAULT '[]'` was rejected because it collapses the first two forever — the `PROJECT_DETAIL_VOCAB` rule ("absent stays absent, skipped stays skipped") applied to a list. **Skipping is safe because the preselect and the fallback are the same function**, `bundleForProjectType`, called twice — not two lists kept in agreement — so `bongos onboard` and any API caller inherit the bundle by omitting the field, and the canonical sequence in `onboard-plan.js` grows **no new step** (the picker refines `type`, an answer already in it). NOT `detail` (its vocabulary is closed + scalar by construction, and [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md) §2 ruled the interview must not ask this) and NOT `settings` (a setting is a policy the RUNNING instance obeys — web.env, a settings-apply intent, a restart). The picker's catalog copy (`OPTIONAL_MODULES`) is **declared beside the bundles**, because `module.json` descriptions are written for builders, `GET /modules` needs a session the signed-out wizard has not got, and `GET /public/modules` serves booleans — pinned the ADR 0237 way, so a new `default: false` module cannot ship without owner-facing copy. `GET /provisioning/starter-bundles` gains `optional` so the preselect and the catalog arrive in **one** read. The route **refuses** an unknown / always-on / hub-only key (`400 bad_modules`) rather than dropping it silently (`badProjectDetail`'s rule), and a re-request reports `modules_ignored`. The column is a DECLARATION, not yet an input to the control-plane runner. Rejected: storing the preselect on a skip (it writes down an answer nobody gave), a new `onboardPlan` step, and rendering the always-on core as sixteen disabled toggles (a disabled control reads as a choice the owner failed to make).](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md) | provisioning / project creation |
331
+ | 0241 | [**The artist gate holds the DEPLOY, not the ship, and it reads a state rather than a judgement** ([task 1003116](https://cloudbongos.com/builders#/task/1003116) · goal 1000074, criterion `artist-blocking-is-per-project` — R05, the DECISION task the goal placed before R04 so no code would be wired to an unsettled gate). Goal 1000074 carries two rules on a collision course: the owner's "blocking strictness is PER PROJECT, not global", and [ADR 0162](0162-review-gates-retired-rank-consistent-ci-only.md), which retired review gates on `main` after a code-owner requirement stranded two graded-PASS ships in a day. **0162 is usually cited by a paraphrase — "a verdict is advisory, never self-executing" — and the paraphrase is wrong as a blanket claim:** 0162's own table of compensating controls marks three of four as *fails closed*, the subagent grader among them. What it actually retired was a gate GitHub could only express as ACCOUNTS — identity-pinned against [ADR 0016](0016-trust-boundary-server-enforced-permissions.md), and stranding with no escape hatch. So the surviving test is: **a gate is admissible when it is evaluated automatically from readable state, consistently for every builder, with a named escape hatch; inadmissible when a particular person must act and it stalls when they do not.** Decision: (1) the gate reads a **STATE** — "is there an unresolved artist review task?" — never an opinion, so the artist's judgement is what they write INTO the task and stays advisory in exactly 0162's sense, while what the system reads is ordinary GDS status; (2) strictness is a project setting `artist_gate ∈ off | advisory | strict` and at `strict` it holds the **DEPLOY, not the ship** (owner ruling) — the builder's work is graded, merged, credited and landed on CI alone exactly as today, so 0162's strand class cannot recur, and the gate attaches to the boundary the artist's ABI mandate is actually about (users see the deployed instance, not `main`; [ADR 0137](0137-upstream-publish-policy-manual-owner-gated.md) already splits land/release/deploy); (3) the escape hatch is the **project OWNER only** — not the builder, not any Metic+, not the artist — via the existing `POST /tasks/:id/override-request`, because a person can be both artist and builder and could pass their own work; a self-release is permitted but never quiet (required rationale, audit-logged as a self-override) so a solo instance cannot deadlock; (4) the default is **`strict`** (owner ruling, against the author's `advisory` recommendation), which is safe only because §2 holds the deploy — the worst case is a pin that waits, and this instance is included once R04 ships; a project with no artist must still be able to deploy, and the refusal must name what holds it and which control releases it; (5) tightening is **not retroactive** — re-gating history would strand the next deploy behind reviews for work nobody remembers and make the setting frightening to raise; R02's flag queue is the right tool for already-shipped copy. Rejected: blocking the ship (recreates 0162's incident exactly, with credits unpaid — [ADR 0120](0120-pay-on-land-and-builder-owned-rebase-gate.md)); advisory at every level (configures nothing); any Metic+ as the hatch (makes `strict` indistinguishable from `advisory`); making the artist's VERDICT the gate (the one thing 0162 genuinely forbids); retroactive tightening; a global constant (ruled out by the owner's constraint by name).](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md) | goal 1000074 / the artist's loop |
332
+ | 0242 | [**A cross-role cascade is a declaration table on the event that already exists** ([task 1003549](https://cloudbongos.com/builders#/task/1003549) · goal 1000074, criterion `artist-review-task-on-ship` — the split R04 invited in its own brief and [ADR 0241](0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md)'s dependency). R04 needs "every ship with copy or visuals GENERATES one", and its brief said the infrastructure did not exist, naming the spark queue as the prototype. Reading both surfaces shows the prototype is the wrong half: the spark queue ([task 1002921](https://cloudbongos.com/builders#/task/1002921)) and the copy-desk flag queue ([task 1003113](https://cloudbongos.com/builders#/task/1003113)) are both **PULL** — filtered listings of rows another role already created — and nothing anywhere generated work on a state transition, which is the **PUSH** R04 asks for. (`cascade-dispatch.js` is model-cost routing; named only because its name has misled more than one reader.) **Decision: no new bus.** The kernel seam registry's own header uses this exact scenario as its worked example, `task.shipped` is already emitted post-commit by both ship paths and already has two consumers (discord's broadcast, sessions' deferred reward) — so a cascade is ONE MORE LISTENER on it, registered in `routes/lifecycle.js` beside the port because that mount is the one boot hook tests also run. A cascade is a frozen entry `{ key, discipline, when(task), title(task), body(task) }` — **a declaration table, not a rules engine**: no condition language, no stored rules, no per-project config, no priority ordering; adding one appends an entry and the runner does not change (the [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md)/[0239](0239-a-recommendation-is-a-table-that-carries-its-own-reason.md) shape, for their reason — the generalisations worth building are the ones two or three real entries have shown they share). **`CASCADES` ships EMPTY and R04 writes the first rule**, asserted so the emptiness is a decision on the record; the mechanism is proved against the tests' own declarations rather than against its first consumer's shape. Four load-bearing properties, all pinned: (1) **it can never fail the ship it reacts to** — post-commit event, isolated listeners, and a runner that catches everything and returns a report, because ADR 0241 settled that an artist may hold a DEPLOY and never a builder's SHIP and a generator that could fail a ship would smuggle that gate back in; verified against a real failure (no database reachable → one log line, resolves); (2) **idempotent on machinery that already exists** — `source: 'cascade'` + `source_ref: cascade-<key>-<taskId>` through `createTask`'s `(source, source_ref)` read, the same one a copy proposal uses ([ADR 0233](0233-a-copy-proposal-is-a-task-carrying-a-patch.md)), so a reconciler re-sweep or a double emit is a no-op; (3) **it says where it came from** — inherits `goal_id`/`version_id`, carries the origin id in the ref and the prose, but NOT `parent_task_id`, which models a SUBTASK, and cascaded work is not part of the ship that triggered it; (4) **files at `backlog`, never `ready`** — generated work is a request, and auto-filing claimable work would let a rule nobody re-read mint claims. A rule key is PERMANENT (half the source_ref; renaming re-files everything it ever generated), pinned by a duplicate-key check. The mechanism is discipline-agnostic — an ideator or engineer cascade is the same entry with a different `discipline`, which is the generalisation that made this its own task. Accepted limit: cascades are declared in code, not configured per project. Rejected: extending the spark queue (a read-side filter that generates nothing), a new event or poller, a rules engine or per-project config, filing at `ready`, `parent_task_id` as the link, and running the cascade inside the ship transaction (which would make a generation failure a ship failure — the exact coupling ADR 0241 rules out).](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md) | goal 1000074 / the artist's loop |
333
+ | 0243 | [**A bundle adjustment belongs to the bundle, not to the advice about it** ([task 1002341](https://cloudbongos.com/builders#/task/1002341) · goal 1000046, criterion `proj-module-select`). R17 ([ADR 0239](0239-a-recommendation-is-a-table-that-carries-its-own-reason.md)) built a rule engine and R20 ([ADR 0240](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md)) built a picker, and nothing connected them: the picker preselected the type's RAW preset while `GET /provisioning/recommendations` separately applied `TEAM_SHAPE_RULES` to that same preset and returned a better answer no surface rendered. Joining them the obvious way — let the picker apply the rules — breaks the property R20 was built on ([ADR 0240](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md) §2: the preselect and the fallback are "one function, called twice"), because an owner who answers the interview, watches the panel preselect `discord`, and then SKIPS the picker would get a project without it. **Decision: `TEAM_SHAPE_RULES` moves from `recommendations.js` down to `starter-bundles.js`** (re-exported, so the public surface is unchanged), `effectiveModules` resolves through the new `bundleForAnswers(type, teamShape)`, and the picker reads the same table the fallback does. The distinction is not a preference: a bundle adjustment is not advice ABOUT the bundle — it IS the bundle once the owner has answered, and it is the only one of R17's three tables that is ever APPLIED (governance is a preview carrying `applied: false`, invites are a seam for R28), so it is the only one that has to live where the resolution happens. `effectiveModules` reads `detail.team_shape` straight off the row rather than through `normalizeProjectDetail` — importing it would point the bundle layer UP at the module that requires it ([ADR 0083](0083-modular-architecture-strangler-build-plan.md)) to buy an answer it already has, since an off-vocabulary shape fires no rule and adjusts nothing. `GET /provisioning/starter-bundles` gains `byTeamShape`: every bundle re-resolved for every ruled answer, with the `adjustments` that got it there — twenty small rows, the whole preselect table in ONE read, for the reason `optional` rides the same payload (the owner flips type and team-shape on two EARLIER panels, and a refetch could land after they moved on or paint a key the panel has no row for). `PATCH /provisioning/instances/:id/detail` returns `modules: effectiveModules(updated)` beside its `publish` verdict and for the same reason — changing `team_shape` MOVES the modules of a `source: 'bundle'` project, so the effect comes back with the cause. The panel renders one reason line per adjustment that ACTUALLY fired, and only while the owner has not answered for themselves (a reason handed back for a toggle they flipped reads as the panel arguing with them). An adjusted bundle returns in CATALOG order, never append order, or the same set of modules would read as a different list depending on whether the picker was answered or skipped; a payload without `byTeamShape` degrades to the R20 answer, which is never wrong, only untailored. No new storage — the adjustment is resolved, never written. Rejected: leaving the rules up in `recommendations.js` and having the picker call `/recommendations` (two lists kept in agreement by hand, plus a round trip per keystroke); importing `normalizeProjectDetail`; storing the adjusted bundle on a skip (it writes down an answer nobody gave); moving governance and invites down too (they are never applied, so there is nothing for them to stay in agreement with); and a reason line per matched condition rather than per actual change.](0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md) | provisioning / project creation |
334
+ | 0245 | [**The way back into an optional step is that step’s own answer route** ([task 1002342](https://cloudbongos.com/builders#/task/1002342) · goal 1000046, criterion `proj-module-select`). R20 ([ADR 0240](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md)) made the create wizard’s module picker skippable and left a project’s module set write-once — the route file said so out loud (“there is no mutation route yet”). R23 closes it WITHOUT a new endpoint: `modules` joins `description` and `detail` on `PATCH /provisioning/instances/:id/detail`, because the wizard’s two optional steps are the same kind of thing — asked optionally at creation, stored on the platform row, reaching no running instance — and that route already carried the ownership gate, the `api:self`/`api:archon:<id>` actor rule, the 404-no-existence-leak shape, and (since [ADR 0243](0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md)) an `effectiveModules` answer it could report but not set. Deliberately NOT the settings channel: a setting is a policy the running instance obeys and would queue a restart to deliver a value the instance does not read. Only an ARRAY is a change — an omitted or null key leaves the column alone — so the three-valued contract’s `NULL` (“never asked”) stays unreachable by a later edit, and `modules` REPLACES where `detail` merges, because a merging write would make removal impossible. The surface is the manage view’s extras band (`/projects?view=manage&id=N`), where a toggle IS the save and the band repaints from the platform’s answer rather than from what was sent.](0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md) | provisioning / project creation |
335
+ | 0244 | [**A catalog search is a view over the rows, never an edit to the selection** ([task 1002340](https://cloudbongos.com/builders#/task/1002340) · goal 1000046, criterion `proj-module-select`). The creation picker ([ADR 0240](0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md)) paints one toggle per optional module, and the catalog is meant to grow — so R21 adds a keyword search over it. A search over a list of links is trivial; a search over a list of TOGGLES is not, because the list is also an answer. Two failure modes follow from letting a row be hidden: the filter **edits** the answer (narrow the rendered array and the read-back array together, and typing `discord` then pressing Next files one module instead of three — silently, with every screen looking right), or the filter **lies** about it (a switched-on module off screen reads as switched off, which is [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md)’s "a preselect they have to audit" in its harsher form — one they cannot see at all). **Decision: the search is a VIEW and `state.modules` is the answer; the view may not write to it.** `moduleQuery` is a plain variable, never part of the draft, so `saveDraft` is not called from the input handler and a search can never turn an untouched picker into an answered one (ADR 0240’s `null` survives); `visibleModules()` derives the shown rows per render while `pickedModules()`, `modulesForCreate()` and `extrasRow()` keep reading the whole catalog; and the panel STATES what the filter is hiding — one line under the field counting what is **on**, not what is visible. Matching is `moduleMatches(mod, terms)`, a pure function: lower-cased **label + summary + key** as one haystack, every whitespace-separated term an AND’d substring. AND because adding a word means the list is still too long; substring because a filter that shows nothing until you finish typing reads as broken; the key because that is what a returning owner names a module by. No ranking (reordering toggles under a typing hand is worse than not), and deliberately **not semantic** — by-meaning search is deferred with the rest of the creation AI layer (idea 1000552). Rejected: filtering the read-back array (failure mode 1), CSS-hiding non-matches (leaves invisible `aria-pressed` toggles in the a11y tree), pinning switched-on rows above the query (a filter that lies the other way), persisting the query in the draft, and debouncing a pass over already-loaded rows.](0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md) |
336
+ | 0247 | [**The join door is ONE composed answer, and dark matter has no live verdict to compose with yet** ([task 1002331](https://cloudbongos.com/builders#/task/1002331) · goal 1000046, criterion `proj-join-policy`). [ADR 0182](0182-project-visibility-two-axes.md) D6 wrote the rule — visibility is the ceiling, join policy may only narrow it — and nothing implemented it: the public `POST /access-requests` read `joinability` alone, so an owner could set their project private or stealth on the map and the application endpoint behaved exactly as before. R12 adds the third knob (`join_grant` = view | apply | full, one line of settings vocabulary, default `full` so nothing existing changes) and composes all three in one pure kernel file, `src/bongos/project-door.js`, published through the doorway so no module can re-open a door the others closed. Three things worth carrying: the door vocabulary is a list ordered widest→strictest, so “the stricter wins” is the data structure rather than a branch; the refusal never names WHICH knob closed the door, because a stealth project that could be identified by its 403 gives back exactly what R06’s server-side redaction protects; and `locked` (dark matter) is specified, tested and deliberately UNWIRED — the instance cannot derive publishability (no description in its pack, and a pushed verdict goes stale the moment the owner writes one), while the hub’s cached copy says every live project is dark matter today, so enforcing off it would refuse every join on the platform.](0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md) | onboarding / provisioning / project creation |
337
+ | 0248 | [**A hub project’s owner is the provisioning row, carried on a port and bound to both halves of the catalog projection** ([task 1003580](https://cloudbongos.com/builders#/task/1003580) · goal 1000046, criterion `proj-invite-builders`). [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) made a wizard-made project resolvable by `POST /projects/invite` and named the remainder: the route is gated `project.curate` (floor metic) with no own-scope arm, so the project’s own owner — a `xenos` on the hub — got 403 on their own project, blocking BV1.R27–R29 and the C6 proof. The arm admits a `project.curate` holder **OR** the owner, where owner means `provisioning_instances.owner_builder_id` — never the hub’s `owner_github_id` columns, which [ADR 0238](0238-the-join-door-is-a-provisioning-settings-policy-key.md) fixes as attribution. Three things worth carrying: the fact travels on a NEW port `provisioning.projectOwnedBy` answering a **boolean**, so the comparison stays on the plane that holds the row and no consumer handles an ownership id it could mis-compare; the port is asked about **both halves of the ADR 0246 projection** (slug AND origin), because a hand-seeded catalog row’s name is otherwise a squattable claim on that project’s membership directory; and ownership is asked BEFORE the target is resolved, so the refusal stays the untouched `requirePermission` 403 and cannot become an existence oracle over stealth projects ([ADR 0182](0182-project-visibility-two-axes.md) D2a). The atom stays spelled in the route’s own middleware list ([ADR 0210](0210-scouting-graduates-on-an-instance-asserted-owner-claim.md)’s rule), so the route still classifies `metic+archon`.](0248-a-hub-project-s-owner-is-the-provisioning-row-carried-on-a-port.md) | onboarding / provisioning / project creation |
338
+
339
+ | 0249 | [**The invite step is the done panel’s first act, not a rail step — and the same widget is the way back** ([task 1002346](https://cloudbongos.com/builders#/task/1002346) · goal 1000046, criterion `proj-invite-builders` / C6). BV1.R27 asks for a skippable invite step *in creation*, but an invite cannot be one of the wizard’s numbered panels: `POST /projects/invite` takes `{ client_id, github_login }` where `client_id` **is the slug**, and per [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) the catalog row that slug resolves is projected **at create** — so before the Create step there is nothing to invite anyone to, and a pre-Create panel could only queue names and replay them, inventing a second unaudited invite path whose failures land after the owner has left. **Decision: the step is the FIRST act of the DONE panel**, keyed on the created record (not `instanceId`, which `startProgress` sets only *after* `renderDone`), so it also returns on a resumed watch; the rail is NOT renumbered (a captured learning on this file: a renumbering couples in four places, and a step that can only run after the terminal step is not a step in the rail). The SAME widget is mounted again on the project’s own page beside *Its extras* — the [ADR 0245](0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md) argument, one implementation and two mounts, so the refusal vocabulary cannot drift. The widget answers neither “is anyone signed in” nor “is this their project”: each call site hands over a slug or nothing (the created record; the own-scoped manage read that 404s for everyone else), because a page that re-derived ownership would be a second, weaker copy of a server rule. **The 403 is quoted carefully and that is the point** — [ADR 0248](0248-a-hub-project-s-owner-is-the-provisioning-row-carried-on-a-port.md) paid two equalised reads per request to make the refusal identical for a real, stealth, absent or malformed target ([ADR 0182](0182-project-visibility-two-axes.md) D2a), and “you lack permission on **this project**” would assert in copy the existence the route refuses to confirm, so the page says only “this account can’t invite people here” and a test pins that wording. `account_not_found` and `unknown_project` are said in the route’s own words (the latter as a moment, not a dead end — the row is projected at create); anything else falls through to the server’s own message. The pending line is drawn from the 201 row’s `membership_kind` and the typed login — **never the `github_id`** it carries, which names someone who has accepted nothing yet. Creation is never blocked: the project is filed before the step is shown, so every refusal leaves it created and the step re-tryable, and Skip sends nothing. Rejected: a rail step before or after Create; the manage page alone (R27 asks for it *in creation*); reading the real membership directory back (no own-scoped route exists, and the session receipt is honest about being one).](0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md) | provisioning / project creation |
340
+ | 0250 | [**Strict versioning — the version boundary is the scope gate** ([task 1003589](https://cloudbongos.com/builders#/task/1003589) · goal 1000086). The work hierarchy ([ADR 0086](0086-goal-scoped-work-hierarchy.md)) has a bottom and no top: tasks close, criteria close, goals close, and a version never does. Measured live — **14 goals reading `achieved`/`archived` while holding 35 unfinished tasks**, 523 open tasks across 37 open goals, and **two versions `building` on one project**; every number was worse than the planning session had recorded two days earlier, which is the argument: the drift is the steady state, not a backlog. The cause is four mechanical gaps, not a social one — `POST /goals` fetches the version row and never reads its `status`; goal achievement is computed from criteria alone and never consults `tasks` (both the `db-goals.js` writer and the `done-when.js` cascade); `POST /goals/:id/archive` checks nothing at all; and no route ships a version, so version status has only ever moved as hand-run SQL on the droplet. **Decision, five rules:** the goal set closes when a version starts `building` (new goals go on the `planning` version — where the next version is scoped in advance) with a named 409 an agent can act on; a goal reaches `achieved` only when its criteria are satisfied **and** no task in it is non-terminal, where terminal is `shipped`|`abandoned` per [task 1003588](https://cloudbongos.com/builders#/task/1003588)'s one vocabulary and the mid-ship `completed`/`confirmed` hold it open; archiving a goal that holds open tasks refuses until the caller dispositions each (abandon with the reason, or move to a named surviving goal); `goal_id` becomes required and the per-version catch-all fallback is deleted; and exactly one version builds per project, auto-closing when its last non-maintenance goal achieves, with the single `planning` version promoting itself so the project is never without a live train. **The escape hatches are deliberate and visible**: an Archon may admit one *named* goal to a building version with a reason, and the overrides are counted on the version (the standard failure of a hard scope gate is a fake hotfix version cut to hold what did not fit — an untraced override is just a slower version of no rule); plus one auto-created maintenance goal per version, exempt from the auto-close count, so bugs never need an override to be filed. **Nothing here can refuse a ship, a claim, or a task** — every rule acts on a *closing* move. Rejected: counting `completed`/`confirmed` as finished (they can fail a grade or strand — the goals **board** buckets them as done, but that is a progress ring, and `task-status.js` keeps the two questions apart); a nightly reconciler that reopens wrongly-closed goals (detection is what you build when prevention is unavailable, and it is available here); a human review gate before a goal may close (the queue [ADR 0183](0183-criteria-close-themselves.md) dismantled — the check is a fact the table already holds); concurrent building versions scoped by module (two live trains means every "what is in this version" question has two answers); retiring versions entirely (the version is the unit the owner sets scope in and `limitations/<version>-shipped.md` archives). **Amends [ADR 0157](0157-archon-is-rank-and-identity-only.md) for exactly one key:** `version.create` returns to the Archon floor. 0157's "authoring delegates, disposition does not" was right about what a version *was* — but once the boundary carries the scope gate, cutting a new version is no longer authoring, it is the way *around* the gate, so create joins close as a disposition over scope.](0250-strict-versioning-the-version-boundary-is-the-scope-gate.md) | lifecycle / versioning / scope |
341
+ | 0251 | [**A suggestion of who to invite is a recruiting surface, and owes the recruiting opt-out** ([task 1002347](https://cloudbongos.com/builders#/task/1002347) · goal 1000046, criterion `proj-invite-builders` / C6). BV1.R28 fills the seam [ADR 0239](0239-a-recommendation-is-a-table-that-carries-its-own-reason.md) §6 left open: the wizard’s pure rule engine emits a **seek** (`solo` → `null`, `small-team` → 3, `community` → 5) and never a list, because ranking builders is a directory read a pure file may not run and `provisioning` may not import a sibling. Two readers could answer “who are the top builders” and they differ by **one SQL predicate**: `economy`’s `leaderboard()` joins `rollupVisibleSql` and deliberately honours **no** recruiting opt-out, while `platform-identity`’s `listScoutingRollup()` joins `scoutingListableSql`, which carries it. The leaderboard is cheaper in every mechanical sense — already a public route, no port, callable straight from the browser. **Decision: use the recruiting directory anyway**, because `account-visibility.js` says the split in its own words (`hide_stats` suppresses *numbers* everywhere; the recruiting opt-out “removes an account from the RECRUITING SURFACE only”, community spec D3), and a list of people the platform proposes you recruit **is** that surface — sourcing it from the leaderboard would route around the one control built for this exact case and name every builder who asked not to be recruited. The rule for later readers: **the cheapness of a public read is not an argument about who consented to appear in it.** The lookup ships as `listInviteSuggestions` on the existing `platform-identity.hub` port, reached with `resolveOptional` and NOT declared in `consumes` (the `catalog-bridge.js` precedent — platform-identity is hub-only, so its absence is “no suggestions”, never a 500); `github_id` **does not cross the port** (explicit whitelist — login, display name, avatar, disciplines, two counts — so a new rollup column cannot leak by default), and the owner-exclusion is applied inside, over-read by the exclusion count so filtering never returns short. The route `GET /provisioning/instances/:id/invite-suggestions` is own-scoped by the **existing** `instanceAccessDenied` (same 404 as the manage read) and deliberately does NOT reuse `/scouting`’s gate ([ADR 0210](0210-scouting-graduates-on-an-instance-asserted-owner-claim.md)): a brand-new owner has no hub-witnessed owner row at the moment the done panel paints, so the correct-looking gate would refuse the exact caller the feature exists for — and reaching the route IS the ownership answer ([ADR 0249](0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md) §4). On the surface: a **second door in** (`setInviteSuggestions`) beside `setInviteSlug`, so the widget still answers neither “signed in” nor “theirs”; the reason is **always** said including when the answer is nobody (`seeking: null` costs no query — rendering a stated answer as an empty list would make it look like a failed lookup); a picked name fills the field and takes the **same write path** as a typed one, so the refusal vocabulary cannot fork; a filed invite stops being a suggestion; and every failure is silent, because a suggestion is an extra and a just-created project must not look broken over one. Rejected: the browser reading the economy leaderboard (the whole decision — consent, not architecture); reusing `/scouting` (right data, gate that refuses this caller); filling `invites.suggested` inside the engine (I/O in a pure file + a sibling import); a `provisioning → economy` port (legal, still the wrong directory — the boundary was never the objection).](0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md) | provisioning / platform-identity / privacy |
342
+ | 0252 | [**The self-hosted visibility report is a PULL, and auto-appear buys a retraction lever** ([task 1002321](https://cloudbongos.com/builders#/task/1002321) · goal 1000046, criterion `proj-visibility-states` / C1, BV1.R02 — a spike whose output is this decision; gates [task 1002327](https://cloudbongos.com/builders#/task/1002327) / R08). **The premise failed first:** a self-hosted instance has no channel to the hub and no credential to open one — `registerClient` has exactly one caller in the tree (the operator's own `provision-net.js`), gated `hosting_shape !== 'co-tenant'`, and no route registers a client. So *federated* ⊆ *operator-provisioned co-tenant* and *self-hosted* holds nothing, deliberately: [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) says do NOT extend federation to `standalone`, and [ADR 0144](0144-federated-single-logout-backchannel.md)'s Known Limitation rests on "every secret-holder is an operator-provisioned, trusted instance … the proper fix must land before federation opens to untrusted third-party clients." [ADR 0182](0182-project-visibility-two-axes.md) D5's "accepts a report from the authenticated `sso_client`" described a client that, for this population, does not exist. **Decision 1 — the channel is a PULL of the instance's already-public manifest** (`GET <origin>/api/gds/instance` → `branding.project.visibility`, a value every core already serves and whose author anticipated this consumer). It buys five things a push must build: no credential (0144's tripwire untouched — the whole reason), **no replay surface** (a pull reads current state, so the `jti` machinery D5 handed R02 is moot — replay is answered by not having a message), no wire change on another instance's upgrade clock ([ADR 0205](0205-federated-checkin-needs-the-hubs-own-signin-witness.md)'s and [ADR 0201](0201-cross-project-application-vouched-row-live-record.md) D2.2's rejected-required-field problem simply absent), the hub still authorising nothing (0182 D5 satisfied, [ADR 0238](0238-the-join-door-is-a-provisioning-settings-policy-key.md)'s rejected `PATCH /projects/:id/visibility` still rejected), and a fetch that already exists behind a real SSRF guard (`project-stats.js` — DNS-resolve-all, `redirect: 'error'`, TTL + negative caches). The pull carries `visibility` and NOTHING else, so the property the map has today — a project owner controls no free-text field that reaches the feed — is not spent. **Decision 2 (the Archon's) — auto-appear:** no human stands between a public project and the map; `featured` keeps only the ranking job [ADR 0182](0182-project-visibility-two-axes.md) D3 left it. Conditional on Decision 4. **Decision 3 (the Archon's) — trust follows provenance:** both populations auto-appear, and the asymmetry is in the GUARDS, not a gate — a control-plane row's `visibility` is *assigned* by the bridge from an own-scoped audited route (authoritative), a self-hosted row's is *pulled* from a manifest the hub does not control (a cached claim with a recency stamp, able to move its own row and nothing else). An instance is authoritative about its own door and about nothing else. **Absent is a THIRD state and must not purge:** `null` (never read) ≠ `false` (read, negative). 0182 D5's fail-closed default holds only for a self-hosted row that has NEVER answered; a row that answered before and is unreachable now keeps its last state with a recency stamp ([ADR 0199](0199-restart-an-unreachable-active-project.md), [ADR 0226](0226-one-stale-timer-reporting-degrades-enforcement-refuses.md)), because [ADR 0247](0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md) D4 found the live shape of that trap — every catalog row at `description: null`, so naive fail-closed would have emptied the map at once. Narrowing is free; widening is what costs. **Decision 4 — auto-appear buys a retraction lever, in the same task** ([ADR 0047](0047-discord-bugs-intake-anti-abuse.md): the straight-through path and its abuse guard are one task, treated as binding): `platform_identity_projects.status` is `(active,hidden)` and **no route writes it** — takedown is hand SQL today, and a one-way door onto a public surface is not a door. R08 ships `PATCH /projects/:id/status` (curate floor, audited), a per-IP limit on the unmetered CORS-open feed read ([ADR 0209](0209-one-budget-for-the-account-existence-oracle.md)), and above all **`hidden` STICKY against the pull** — a retracted project that keeps reporting `public` stays hidden, or the next sweep silently undoes every takedown and the lever is decorative. Also named: `hall_origin` reaches the feed from the remote's own discovery index checked only for *differing* from the project host — the opposite of containment — and gets `shipUrlWithinClientOrigin`'s downward-only rule, filed against R08. Rejected: a pushed credentialed report (the literal R08 — needs the credentials 0144/0145 forbid, plus replay, rate and a wire-compatible field, none of which the pull has); `PATCH /projects/:id/visibility` (0238 already); a one-time admission and ongoing curation (both offered to the Archon, both declined); trusting the pull to move `status`; enforcing "unreported ⇒ dark matter" across all rows; letting the pull carry name/tagline/art. The later ANALYTICS feed is deliberately NOT decided here — it carries per-person data, exactly what [ADR 0205](0205-federated-checkin-needs-the-hubs-own-signin-witness.md) proved cannot ride an unwitnessed self-report, and it needs the 0144 credential work first.](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) | platform-identity / provisioning / federation trust |
343
+ | 0256 | [**The visibility ceiling binds the SIGN-IN gate too, and the knob reader moves to the composer** ([task 1003579](https://cloudbongos.com/builders#/task/1003579) · goal 1000046, criterion `proj-join-policy`). Closes the *KNOWN LIMIT* [ADR 0247](0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md) wrote down: it composed `visibility × joinGrant × joinability` into one door and enforced it at the public application endpoint, leaving `openEnrollmentEnabled()` reading the `joinability` knob alone — so a project set `private` (or `stealth`, or `joinGrant: view`) while `joinability` stayed `open` **queued applications for review and still let any first-time GitHub account sign itself in at xenos**. Half of [ADR 0182](0182-project-visibility-two-axes.md) D6’s table — its whole *Self-join* column — was unenforced wherever the owner had explicitly opened enrolment. **Decision 1:** `openEnrollmentEnabled()` becomes `effectiveJoinDoor(deps) === 'open'`, the same SR-19 move [ADR 0194](0194-joinability-one-door-decision.md) D1 made one level down, so the four sign-in call sites, `firstTimeAdmissionBlocked`, `webAdmissionStatus` and the manifest all follow untouched — and **nothing changes for any existing project**, because `branding.js` normalises an absent visibility to `public` and an absent grant to `full`, the pair that narrows nothing. The ceiling closes **self-enrolment only**: an invited first-timer, an existing builder and the configured `firstAdmin` still get in, or a private project locks out its own founder. **Decision 2:** the manifest’s `admission.policy` now reports the GATE while `brand.project` keeps reporting the SETTING — they may differ, because they answer different questions, and `invite-only` is already what every `apply` project reports, so narrowing into it names no state ([ADR 0247](0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md) D3). **Decision 3 — the mechanical one:** `joinabilityMode()` moves from `src/bongos/auth-admission.js` into `src/bongos/project-door.js` (re-exported, so no import path and no doorway export changes). The door resolved its own joinability input with a lazy `require('./auth-admission')`; the moment the gate requires the door back the two files point at each other, and the `require_cycle_count` ratchet counts edges from **source text**, so laziness is no escape. Rejected: duplicating the composition in the gate (the drift SR-19 exists to prevent), and making `joinabilityMode` a required argument of `effectiveJoinDoor` (turns the published no-argument call into a footgun that silently answers `invite_only`). The reader was only in the admission file because the gate was its first caller; the composer owns its inputs. **Decision 4:** the gate passes no `publishable` verdict, so `locked` still cannot fire from it — [ADR 0247](0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md) D4’s finding stands, and a gate reading the hub’s cached column would refuse every first sign-in on the platform. Also rejected: refusal copy that names the private state (the read-out D3 forbids, and a case-specific sentence makes the refusals distinguishable by another route); a `webAdmissionStatus` special case (it reads the same function, and its anti-drift sweep already pins the agreement). Proof: `tests/admission_ceiling_gate.mjs` boots a real instance on `private` + `open` through the production env channel, then sweeps all 75 knob combinations asserting the gate **is** the door and that exactly one combination — `public`/`full`/`open` — self-enrols.](0256-the-visibility-ceiling-is-the-sign-in-gate-too-and-the-reader-moves-to-the-composer.md) | admission / project visibility / kernel layering |
344
+ | 0253 | [**The takedown lever has two writers, and provenance has to be a column** ([task 1002327](https://cloudbongos.com/builders#/task/1002327) · goal 1000046, criterion `proj-visibility-states` / C1, BV1.R08 — the build). Amends [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) §4/§5: three premises that did not survive contact with the tree. The decisions stand; what changes is what has to be BUILT. **(1) `hidden` is sticky against TWO writers, and 0252 names the wrong one.** 0252 §5 says "no route writes [`status`]" and defends the takedown against the PULL. But `project-catalog.js:upsertOwnedProject` writes `status = EXCLUDED.status` on every conflict, sourced from `catalog-bridge.js:catalogStatusForInstance` — which returns `active` for any instance not torn down — and [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) re-takes that projection at EVERY provisioning write. So the live failure is: curator hides a project → its owner saves any setting → it is back on the map. Stickiness therefore belongs to the ROW (`status_pinned`, set by the curated PATCH, honoured by the projection), not to one writer's manners; the pull separately writes no `status` at all, asserted against the statement text. `retireOwnedProject` deliberately does NOT honour the pin — it is a safety retraction for an origin the project no longer answers on ([ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2), and narrowing is always free. **(2) Provenance must be a stored column.** 0252 §4 tabulates control-plane vs self-hosted rows as though the catalog knew which was which; it has no `hosting_shape` (that lives on `provisioning_instances`, in a module platform-identity may not import — [ADR 0083](0083-modular-architecture-strangler-build-plan.md)) and `owner_github_id` separates nothing (set by the bridge, set by `registerClient`, null on hand-seeded rows). `visibility_source` (`control-plane`|`pull`|NULL) records it on the row, re-asserted in the UPDATE's own WHERE so a mid-sweep projection cannot lose the race — making "the two never write the same row" an invariant the SQL enforces. Rejected a `provisioning.instanceShape` port: a cross-module read in a background loop, for a value that never changes per row. **(3) The tri-state cannot live in `visibility`** (`NOT NULL DEFAULT 'public'`), so the recency stamp IS the null-carrier: `visibility_reported_at IS NULL` is the never-read state, and a failed read writes nothing — which is what makes "keep the last known state" fall out rather than be implemented. The fail-closed feed predicate needed a BACKFILL for [ADR 0247](0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md) D4's exact reason: both columns are new, so unbackfilled it would have emptied the public map on deploy. Migration 020 stamps every existing row `control-plane`, which is safe because it is TRUE — no instance has ever been asked. Two smaller corrections: the manifest key is **`brand`**, not `branding` (0252 §2 and the task body both misname it; reading only the documented spelling makes every pull a silent no-verdict), and the pull is a **POLLER, not a passenger on the feed's fan-out** — the feed excludes exactly the rows that need pulling (a never-answered row is dark matter, so it is never enriched, so it never answers), stealth rows are excluded from enrichment by design, and a read-path pull would make a DB write a side effect of an anonymous CORS-open read. Honest limit recorded: 0247 D4's `description: null` problem is NOT fixed here — 0252 §7 refuses to let the pull carry name/tagline/description, so the publish gate stays unmet for self-hosted rows. The pull moves the door; it does not publish the project.](0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md) | platform-identity / provisioning / abuse guard |
345
+ | 0255 | [**A public list's ordering is part of its payload, so an anonymous row must be ordered by nothing** ([task 1003561](https://cloudbongos.com/builders#/task/1003561) · goal 1000046, which absorbed [task 1003560](https://cloudbongos.com/builders#/task/1003560)). The public projects feed redacts a stealth row down to an allow-list of `state` + `orb` and carries **no stable per-project key on purpose** ([ADR 0182](0182-project-visibility-two-axes.md) D2a) — then sorted `ORDER BY featured DESC, updated_at DESC` and handed the observer one anyway. **Ordering is a covert column:** a position is a comparison result, and enough comparisons reconstruct the key. Three consequences, the third load-bearing — `updated_at` was published despite being deliberately withheld from the projection (it lies as a public "last updated"); a black hole sat BETWEEN NAMED NEIGHBOURS whose order is visible, bracketing its timestamp between two identifiable rows; and it **MOVED when its hidden project was touched**, which is an activity oracle on a public, CORS-open, unauthenticated route, and stable enough between touches to follow one black hole across snapshots. **Decision: a row may be ordered only by what it publishes.** An identified row publishes `created_at` + `origin`, so it sorts by those under `featured` (D3's remaining ranking job) — an order that is a PURE FUNCTION OF THE WIRE PAYLOAD, reproducible by a reader holding only the response. An anonymous row publishes nothing, so it is ordered by nothing about itself: the black holes collate into ONE TRAILING BLOCK permuted by a keyed hash. Four sort terms; the first partitions the blocks so no later term compares across them, and **terms 2-4 are GUARDED** — the "anonymous rows go last" half without the "and are blanked" half is not a fix, since the trailing block would simply re-sort by `created_at`, which narrows *which* project it is for anyone who watched the catalog fill. The identified set is DERIVED from `ORB_BY_STATE` (never re-typed, so SQL and `feedShapeFor()` cannot drift) and is an ALLOW-list, so an unknown fourth enum value sorts anonymous as well as drawing anonymous. The permutation salt is **secret** (origins are guessable domain names — a predictable salt lets an observer hash candidates and match ranks) and **rotates on a 24h epoch** (a permanently stable permutation still makes POSITION a durable handle); random per process, since nothing persists or reads it back. **Pagination is the same decision, not a neighbouring one** — task 1003560's premise was already stale (`<redacted> = 500` shipped with the feed; its own comment called the cap "the cheap half" and pagination "its own task"), and the surviving half edits this exact statement, so it merged in: `?limit=&offset=` per [ADR 0119](0119-api-pagination-contract.md), **no cursor** because the obvious encoding is the last row's timestamp — the very channel this closes — and the **default page IS the maximum (500)** so a static orbs site in ANOTHER REPO is byte-for-byte unchanged, with `page` added beside `projects` rather than wrapping it. Paging cannot aim more fan-out than one request already could: `?limit=` only shrinks, and `?offset=` is bounded by `project-stats.js`'s 10-min per-origin cache, not the request count ([ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) §5.3's per-IP limiter unchanged). Rejected: sorting everything by `created_at` (positions a black hole by a date its row refuses to carry); alphabetical `origin` order for the block (an oracle against a candidate set); `ORDER BY random()` (breaks paging — a row could appear twice or not at all); coarse `updated_at` buckets (a sort key that must be blurred should not be there). **Honest limits, stated rather than implied fixed:** existence and count stay public by design; **the identified set still CHANGES when a project goes stealth**, a LARGER channel than this one and untouched here; and position is stable within one epoch.](0255-a-public-lists-ordering-is-part-of-its-payload.md) | platform-identity / privacy boundary / public API |
346
+ | 0254 | [**A relayed link is contained downward, or it is not published** ([task 1003670](https://cloudbongos.com/builders#/task/1003670) · goal 1000046, criterion `proj-visibility-states` / C1) — the gap [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) §5 named and filed. **The defect:** `hall_origin` reaches the public projects feed from the REMOTE instance’s own discovery index (`GET <origin>/api/bongos` → `index.hall`, `project-stats.js`) and the hub renders it as an `<a href>` under that project’s name — checked only for **differing** from the project’s site host, which is containment turned inside out: it admits every host on the internet and refuses the one that is certainly safe. `{"hall": "https://evil.example/builders"}` was published on the hub’s own domain, attached to the victim. The rule answered a DIFFERENT question correctly ([task 1003257](https://cloudbongos.com/builders#/task/1003257), owner decision (a): *a door only where it opens somewhere else*); nobody had asked it the security one. It stopped being survivable when [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) §3 replaced “whom the operator provisioned” with “whoever points a domain at the hub”. **Decision:** the same **downward-only** rule a pushed ship link already obeys — a relayed URL must point AT OR BELOW the origin that reported it (own host or a **dot-anchored** subdomain; `http(s)` only; no embedded credentials; **refused whole at the length cap, never truncated**, because slicing a URL changes where it points). Containment runs FIRST and compares **hostnames** (a port is not a trust boundary); the unchanged “different door” test runs second and compares **hosts with the port** (a hall on another port really does open elsewhere). Net: a hall link survives only as a strict subdomain — or another port — of the project’s own origin. **One home:** `shipUrlWithinClientOrigin` (a private helper in `platform-identity.js`) is lifted to `origin-containment.js:urlWithinOrigin` and required by BOTH callers — the defect WAS two surfaces answering one question differently, and leaving the answer where only one could reach it is what let them differ. Its own file for the reason its siblings give: `platform-identity.js` is AT the 1500-line ratchet (the lift shrinks it by 11), and deliberately NOT re-exported there — a re-export is a third name for one answer. **Downward only, and the cost stated:** the mirror rule looks symmetric and is not (on a shared apex it lets `foo.github.io` publish links on bare `github.io`, and vouch for every neighbour), so a project whose hall is a SIBLING of its site (site `www.example.com`, hall `builders.example.com`) loses the glyph — the hall stays reachable, the hub stops vouching for the address. [ADR 0253](0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md) §2’s asymmetry again: **narrowing is free; widening is what costs**. One guard added on the way in: a hostless origin (`file:`, `data:` — a stored value is not a validated one) is refused outright, because the old suffix test would have matched a legal trailing-dot FQDN like `evil.example.` against the bare `.` it leaves behind. **Rejected:** mirroring the check in the browser ([ADR 0182](0182-project-visibility-two-axes.md) D2a — the page is not the gate, and a second copy of a containment rule is the drift this ends); a per-project allow-list of hall hosts; copying the rule into `project-stats.js` (the cheapest diff, and it re-creates the cause); truncating at the cap (fine for an `<img src>` display bound, wrong for a containment decision); folding both tests into one hostname comparison (it would silently drop a legitimate same-host, different-port hall).](0254-a-relayed-link-is-contained-downward-or-it-is-not-published.md) | platform-identity / public feed / link containment |
347
+
348
+ | 0246 | [**Catalog membership is a projection of the provisioning row, not a side effect of federation** ([task 1003576](https://cloudbongos.com/builders#/task/1003576) · goal 1000046, criterion `proj-detail-interview` / C3). The BV1.R18 proof ([task 1002337](https://cloudbongos.com/builders#/task/1002337)) demonstrated C3 against live and found its last clause — “then it publishes” — structurally false: the owner’s verdict reads `provisioning_instances` while the public map reads `platform_identity_projects`, and the ONLY bridge was `federateInstance → registerClient → upsertProject`, which `shouldFederate` refuses for anything but `co-tenant` (the wizard hard-codes `standalone`) and which runs ONCE, at provision time, through a COALESCE. So a wizard-made project was never catalogued at all and a co-tenant one that answered later never updated — `publishable: true`, `{"projects": []}`, forever. The same missing row is [task 1003580](https://cloudbongos.com/builders#/task/1003580)’s GAP 1: `POST /projects/invite` resolves against `platform_identity_sso_clients`, whose only writer is that same `registerClient`. Underneath is one conflation — federation mints CREDENTIALS and was incidentally the only thing that minted MEMBERSHIP, while [ADR 0182](0182-project-visibility-two-axes.md) D4 already fixed the map’s gate at IDENTITY, which has nothing to do with hosting topology. **Decision: the catalog row is a PROJECTION of the provisioning row, re-derived at every write that can change its inputs** — two halves, neither sufficient alone. (1) THE ROW SHAPE: every control-plane project gets both rows whatever its hosting shape, and the client row carries NO credential (`secret_hash = ''`, `redirect_uris = '{}'`), so **the shape is the enforcement** rather than a flag a caller could forget — `redirectUriAllowed` refuses `/sso/authorize`, `clientSecretValid` refuses `/sso/token`, `/sso/membership/check-in` and `/sso/activity/rollup`, and the one leg that checked NEITHER (`/sso/device/start`, which resolves a client from a user-typed origin) grew an explicit `clientIsCredentialed` guard. (2) THE TIMING: re-taken on create, on `PATCH …/detail`, on `PATCH …/settings` when the door changes, and on `POST …/domain` (which re-keys the row, retiring the abandoned origin — [ADR 0181](0181-teardown-state-gate-and-address-retention.md) §2’s leak, in the catalog instead of DNS). **NO MIGRATION** — `secret_hash` is `NOT NULL` and `''` satisfies it while hashing to nothing. `description` and `visibility` are ASSIGNED, not COALESCEd (clearing a description must UN-publish); `featured`/`tagline`/`art_url`/`template` are curation and stay untouched, so [ADR 0182](0182-project-visibility-two-axes.md) D3 holds. It grants nothing — the row is attribution per [ADR 0238](0238-the-join-door-is-a-provisioning-settings-policy-key.md), ownership stays `provisioning_instances.owner_builder_id` — and because it writes rows keyed on owner-supplied values (a slug, a BYO domain) each write carries its **claim rule in the ON CONFLICT action’s own WHERE**: the client is refreshed only `WHERE secret_hash = ''` (a real client’s origin, which binds the CLI mint audience, can never be rewritten) and the project only `WHERE owner_github_id = EXCLUDED.owner_github_id` (`NULL = anything` is not true, so the hand-seeded rows cannot be claimed). Writing `visibility` does not contradict [ADR 0182](0182-project-visibility-two-axes.md) D5: the hub authorizes nothing — the authority already ran on the provisioning plane ([ADR 0238](0238-the-join-door-is-a-provisioning-settings-policy-key.md)’s own-scoped, audited PATCH), and a project the control plane does NOT own has no provisioning row, so D5’s instance report (R08) stays the only channel for those. `tearing_down`/`torn_down` project `hidden`; a `no_address` instance is not catalogued at all. Travels on the kernel port `project.catalog`, the `project.publishGate` seam in the other direction ([ADR 0083](0083-modular-architecture-strangler-build-plan.md) forbids the import). Rejected: extending `shouldFederate` to every shape (hands a real secret to instances whose web.env the platform does not write, and keeps the once-at-standup bug); bridging only at the publish moment (leaves the invite with nothing to resolve); registering only at standup (the bug itself); a `catalog_only` boolean column (needs a migration and is weaker than the shape); letting the bridge write `featured`; and backfilling the three existing catalog rows from code.](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) | provisioning / project creation |
349
+
350
+ > ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](0195-adr-numbers-are-checked-like-migration-numbers.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
351
+ | 0257 | [**Auth resolves before the hall mounts anything, and a widget's boot read may never navigate** ([task 1003673](https://cloudbongos.com/builders#/task/1003673) · goal 1000063 — *The front door*). An invite-only instance could not admit its FIRST builder, and the symptom lied about where the fault was: an owner saw an empty Access-requests queue and no approve button, because **signing in does not file a request** — only the landing's *Request access* form does, and that form was unreachable. A signed-out visitor to `/builders` was bounced to GitHub, refused as a first-timer ("request access first"), and pointed back at `/builders` to be bounced again. Reproduced against `main`, not just an old pin. `builders.js` was already careful — `/me` is read `softAuth` and a 401 there renders the landing — but `DOMContentLoaded` called `mountHallWidgets()` FIRST, synchronously, and `goals.js`'s mount-time read of `/goals` goes through a `getJSON` with no `softAuth`, which answers 401 by NAVIGATING. The widget's read raced `renderLanding()` and won. **Decision: the front door settles which page this is before anything else runs** — `boot()` reads `/me` (soft), then either renders the landing and stops (nothing mounts, nothing else fetches) or mounts the hall, handing the resolved payload to `loadAll()` so the page still asks once. Widgets are member surfaces (`renderLanding` hid them all after the fact anyway), so not mounting them while signed out removes the CLASS rather than the one instance of it that was found; **a widget's own boot read may never navigate** is the belt beside it (`goals.js` reads soft and renders nothing without a session). A 401 during boot is not an instruction to go and sign in — only a user action is. `tests/hall_landing_boot.mjs` executes the real `builders.js` in a DOM stub and was confirmed to FAIL against the pre-fix file. Rejected: patching `goals.js` alone (it was merely first); making the 401 redirect soft everywhere (an expired session mid-visit SHOULD be sent to sign in — the line is boot vs user action); open enrollment as the cure (removes the gate [ADR 0050](0050-device-flow-admission-invite-gated-by-default.md) chose deliberately instead of repairing the door).](0257-auth-resolves-before-the-hall-mounts-anything.md) | hall-ui / admission / front door |
352
+ | 0258 | [**The public CLI is a generated client-only package, and its file list is proven by running it** ([task 1003679](https://cloudbongos.com/builders#/task/1003679) · goal 1000054 — *A newcomer can build without the UI*). The core ships private as `@bongos/core` ([ADR 0108](0108-instance-composition-configurable-root.md)), so a newcomer with no credential can install NOTHING and the web hall is the only way in — the owner's words: "how am I supposed to easily take on tasks as a new builder? I should be able to do everything without the UI." Open task 1002025 proposed publishing the core, which ships the SERVER and bypasses [ADR 0099](0099-delayed-redacted-mirror-export.md)'s redaction pipeline (dormant until ~2026-11). **Decision: generate a separate public `@cloudbongos/cli` from the core and do not publish the core.** Four load-bearing parts. (1) SCOPED NAME, because npm shares one namespace between org names and unscoped packages — the bare `cloudbongos` is unpublishable *precisely because the owner owns that org*, which npm reports as "invalid" and reads like the name is taken; and a package name cannot be created on the npm website at all (it exists on first publish, which is why "Add Existing Package" answered `Forbidden`). (2) THE FILE LIST IS DECLARED AND PROVEN BY BEHAVIOUR, never computed — a static require-closure CANNOT answer this, because `src/module-api.js` is the doorway a module may only import ([ADR 0083](0083-modular-architecture-strangler-build-plan.md)) and it *names* every kernel capability, so a static walk sees 33 `src/` files from `start.js` alone where runtime resolves **one** (`api-prefix.js`, which imports nothing) — the same trap as task 1003677's unsatisfiable done-when. So the test packs the tarball, installs it into an empty dir with NO repo, and runs every verb: it may fail for want of a session, never for a missing file. That caught two real holes on its first strengthened run — `clients/bongos-client/index.mjs`, reached by a dynamic `import()` no `require()` walk can see, and a hand-listed `files` array that dropped `clients/` and produced a tarball which installed cleanly then died on first use; both are now build-time refusals, and the `files` array is derived from the manifest. (3) `claim`/`ship`/`dev`/`serve`/`module`/`upgrade`/`onboard`/`doctor`/`exec`/`package-core` are ABSENT and the CLI says WHY plus the next step (`bongos shell` → a cloud box with the full CLI) — a bare `unknown command` teaches nothing, which is the exact failure being fixed. The supported journey is `login` → `start` → `shell`. (4) A REDACTION GATE fails the build closed on non-loopback IPv4 (RFC 5737 doc ranges exempt), token/key shapes, and every domain + owner login readable from the instance's own `config/branding.json` — needles come from host config so the core carries no instance identity ([ADR 0062 §7](0062-medusa-instance-model-decoupling.md)); `cloudbongos.com` is allowlisted as public by design. No `repository` field while the core repo is private (it would 404 for every user and publish the owner's login for nothing). 31 files, one dependency (`undici`), version independent of the core's ([ADR 0161](0161-publish-on-merge.md)). `@bongos/client` is VENDORED, not depended on — a public package depending on a private one is uninstallable. **The owner runs `npm publish`; a builder must not.** Task 1002025 is superseded. Rejected: publishing the core; deriving from a static closure (structurally impossible past the doorway); waiting for the mirror (dormant, and this is a build product not a source release); an unscoped name; depending on `@bongos/client`; a degraded `claim`; shipping all of `src/` to make lazy getters safe.](0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md) | cli / distribution / public surface |
353
+ | 0259 | [**A project’s departure from the public list is public, and the copy says so** ([task 1003672](https://cloudbongos.com/builders#/task/1003672) · goal 1000046 — *Project creation*). Decides the leak [ADR 0255](0255-a-public-lists-ordering-is-part-of-its-payload.md) §6.2 deliberately filed rather than implied fixed. The public projects feed publishes a named list plus a count of anonymous rows, so when a project flips to stealth its named row LEAVES the identified block while the count RISES BY ONE — *“Mercury is gone, and there is one more black hole”* names it, and the moment, from a single diff of two snapshots. **Everything [ADR 0182](0182-project-visibility-two-axes.md) D2a does is defeated by it**, because the identity was never carried ON the anonymous row (`STEALTH_ANONYMOUS_KEEPS` is an allow-list precisely so a column added next year is anonymous by default) — it is carried by the ABSENCE of the named row that used to sit beside it. **Larger than the channel 0255 closed**, which needed repeated sampling and correlation; this one is exact and needs two reads. No `ORDER BY` change touches it: it is inherent in publishing a named list at all, so every fix is a product decision with a visible cost and the owner’s call, not a builder’s. **Decision: accept it, and make the copy say so.** Two reasons it is the right end of the trade and not merely the cheapest — (1) it is ALREADY the honest reading of the existing promise, since D2a made existence and count public *by design*, and a count that is public and truthful is a count whose CHANGES are public; (2) both alternatives damage the control they defend. A **delay window** means the owner clicks the privacy setting and is not private yet *without knowing it*, converting an information leak into a false belief — strictly worse, and the wrong direction for a privacy control. **Decoys** spend the count’s truthfulness (it is truthful today) on a guarantee that is still only probabilistic, and a decoy scheme needs some way to tell padding from real rows — the exact stable per-project key D2a forbids. The promise stealth actually keeps is about the project’s CONTENTS, not the TRANSITION: an anonymous row stays anonymous (no name, tagline, art, origin, or key surviving to the next snapshot); what is not private is stepping off a published list readers may keep copies of. So the copy — which read *“its name, and everything else about it, stay yours”*, every clause true and the paragraph as a whole overselling, because a reader takes it to cover the switch — now names the boundary in BOTH places an owner meets the setting (the choice’s blurb and the what-changes fold), in the plain register 0182’s interview fixed for this card. **A privacy control that oversells is worse than one that admits a limit**, the same standard 0255 §6 set for itself. **No behaviour changes** in `projects-feed.js` — the feed, the redaction allow-list and the keyed rotating permutation are decided FINISHED, not insufficient. `tests/projects_hub.mjs` pins the sentence in both surfaces (the copy IS the fix, so a future edit tightening the blurb must not quietly drop it) and re-applies the no-space-metaphor ban to it. Carries 0255 §6.3’s smaller residual so both live in one place: within one 24h epoch the permutation is stable, so same-day snapshots tell black holes apart (not name them) — accepted on the same reasoning, since the fix is a per-request permutation, which breaks pagination exactly as `ORDER BY random()` was rejected for. Guards recorded for any future attempt: no stable per-project key on an anonymous row, and no anonymous ordering an observer can predict or correlate. Rejected: the delay; decoys; dropping the named list (protects a privacy nobody asked for at the cost of the feature); and saying nothing, which is the failure mode this closes.](0259-a-projects-departure-from-the-public-list-is-public.md) | platform-identity / privacy boundary / copy honesty |
354
+ | 0260 | [**An application IS the consent, and the hub’s own echo is the gate** ([task 1002972](https://cloudbongos.com/builders#/task/1002972) · goal 1000045, criterion C4). Privacy spec **D7** asks that a reviewer see what the applicant’s OWN profile rules would already show, evaluated LIVE at review time. The rule is ONE port composing the two EXISTING views — `getPublicProfileExtras` for a public account, `recruiter-sliver`’s own `sliverShapeFor` for a private one — because a third five-key lookalike **is** the new disclosure class D7 forbids. **The one deliberate difference from the D3 sliver:** `getRecruiterSliver` floors on recruiting REACH, and reusing it verbatim would be wrong in the direction that looks safe — `recruiter_discoverable` defaults to FALSE for a private account, so a private builder who deliberately applied would show their reviewer NOTHING and C4 would be satisfied by an empty box. D7 settles it: *“applying is an explicit act”*. The swap relaxes REACH only; `accountActiveSql` (active **and** terms accepted) and `hide_stats` still floor it, and the proof asserts both directions on the same account so the tempting refactor cannot pass quietly. **The federated route’s gate is the whole security story** ([ADR 0205](0205-federated-checkin-needs-the-hubs-own-signin-witness.md) / security report 1000027): client credentials name a PROJECT and never a person, so `POST /sso/applicant-profile` authenticated alone is a bulk disclosure oracle over every account on the platform, private ones included. `applicationBacksProfileRead` demands a HUB-WRITTEN echo for exactly (this account, this client) inside the same 30-day window — a row only the hub’s own join relay creates — refusing **404, never 403**, and BEFORE any account is read (asserted on the statement log, because a gate that refuses after reading has already done the disclosure work). LIVE means nothing is stored at either end; the queue keys on the VOUCH and never on `github_login` (the one PUBLIC unauthenticated write makes an unvouched login an impersonation surface); and the hub client secret stays in core behind a narrow doorway port. **The path is complete but DORMANT until R09 ([task 1002285](https://cloudbongos.com/builders#/task/1002285)) ships the vouch writer** — nothing writes `applicant_github_id` today, and that dependency was missing from 1002972’s graph. Rejected: routing the port through `getRecruiterSliver`, a snapshot at apply time, resolving by `github_login`, and putting `loadIdpConfig` on the doorway.](0260-the-application-is-the-consent-and-the-echo-is-the-gate.md) | platform identity / privacy |
355
+ | 0261 | [**A preselect always carries a reason; the bundle’s summary is the floor** ([task 1003684](https://cloudbongos.com/builders#/task/1003684) · goal 1000046 — *Project creation*). Resolves a collision between two rules that were each right alone. [ADR 0243](0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md) made `adjustments` a **delta** — a team-shape rule that fires without moving anything claims nothing, because a sentence explaining a change that did not happen is a claim the owner cannot check. [ADR 0237](0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md) says a preselect the owner cannot see a reason for is one they must audit, which is worse than no preselect at all. The two collide whenever a type’s bundle **already contains** what a rule would add: `game` is `[dev-box, discord]` and the `small-team` rule adds `dev-box`, so the rule fires, moves nothing, and correctly reports `adjustments: []`. Confirmed on live core 1.19.580 — `byTeamShape[small-team]` = `{"bundle":["dev-box","discord"],"adjustments":[]}`. The owner then reached step 4 and saw **two extras switched on with no reason beside them**, `#modWhy` an empty hidden div: exactly what 0237 exists to prevent, produced by 0243 behaving correctly. Filed as a dropped adjustment; it was not — the engine is right, and the panel simply had ONE voice and fell silent when the rule had nothing to say. **Decision: the why-line has two voices and the type’s is the floor.** A real adjustment still speaks for itself and is never displaced; when none moved, the type’s own curated `summary` explains the preselect; when the owner has answered the picker themselves the panel stays silent (unchanged — a reason handed back for a toggle they just flipped reads as the panel arguing with them); a bundle with no summary still says nothing, because a floor is not an invention. **No new copy, endpoint or field** — the summary was authored for this job in `starter-bundles.js`, served on every row of `GET /provisioning/starter-bundles`, and rendered NOWHERE in the product until now. 0243 is not weakened: the delta stays a delta and no rule is credited with a change it did not make. The pin `tests/projects_hub_module_picker.mjs` carried a fixture with **no `summary` field**, which made four “no adjustment, no sentence” cases pass for the wrong reason; the fixture now carries the real summaries and those cases assert the new contract, including explicitly that no RULE sentence is invented for a change that did not happen. Rejected: making the rule claim a no-op change (re-introduces the unverifiable claim 0243 removed); widening the bundles so no rule is ever redundant (contorts curated presets for a rendering concern, and the redundancy returns on the next edit); and leaving it silent, declined by the owner once the trade-off was put to them directly.](0261-a-preselect-always-carries-a-reason-the-bundle-summary-is-the-floor.md) | provisioning / starter bundles / owner-facing copy |
356
+ | 0262 | [**A bug never lands in the inbox: the system names the home the reporter didn't** ([task 1003691](https://cloudbongos.com/builders#/task/1003691) · goal 1000086 — *Strict versioning*). `routing.js` returned early on a goal-less filing, so its own `kind='bug'` hard-land branch was UNREACHABLE without a goal — `kind` only ever chose a routed task's STATUS, never whether it routed — and a reported defect sat in `idea_inbox` awaiting the triage pass criterion C1 exists to remove. The premise that [ADR 0250](0250-strict-versioning-the-version-boundary-is-the-scope-gate.md) D4 and [ADR 0235](0235-a-fix-category-is-kindbug-under-a-goal-no-column-required.md) contradict each other is FALSE: 0235's exemption is an advisory silence in `goal-advisory.adviseGoal`, and its actual decision already requires a real `goal_id`. So nothing is amended; this is the explicit decision 0235 §6 said would be needed. **D1** a homeless bug gets the version's MAINTENANCE goal (R18's design), find-or-created under an advisory lock inside the route's own transaction — not the catch-all R11 is deleting, and adding no `allowCatchAll` caller. **D2** the gate is the VECTOR (`api`, `discord-bugs`), not the kind: `#ideas` passes no kind, so the classifier guesses one from keywords, and gating on kind alone would silently overturn [ADR 0234](0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md)'s owner-interview decision. Rejected: goal-less bug tasks (breaks D4 a day after it landed); rejecting the filing (Discord cannot retry, so the report is lost); the catch-all (repopulates the bucket being drained); building R18 whole first.](0262-a-bug-never-lands-in-the-inbox.md) | work intake / idea routing / the version-goal invariant |
357
+ | 0263 | [**How a version closes: auto, early, roll-forward, and the maintenance exemption** ([task 1003593](https://cloudbongos.com/builders#/task/1003593) · goal 1000086 — *Strict versioning*). [ADR 0250](0250-strict-versioning-the-version-boundary-is-the-scope-gate.md) D5 decided a version *can* close; this is the design pass that makes its five build tasks mechanical, so they do not each invent their own refusal codes and payload shapes and meet only in production. **The asymmetry that shrinks the feature:** auto-close fires when the last non-maintenance goal achieves — so by construction every non-maintenance goal is already closed and the set needing a disposition is EMPTY. All the machinery (the disposition map, the refusal that returns the goals, successor lineage) belongs to EARLY close alone; R16 is small and R12 carries the weight. **Auto-close hangs off BOTH achievement writers** (`db-goals.achieveGoalIfComplete` and the `done-when` cascade — the deliberate duplicate D2 already governs, a require cycle being the reason they are not one helper), on the caller `exec` so it sees the ship's uncommitted flip, and **may never fail a ship**: wrapped and swallowed like `closeCompletedWorkForShip`, idempotent under `WHERE status='building'`, so a failure leaves a version un-closed (self-healing on the next reconciler sweep) and never half-closed. **Early close** is the R10 archive two-step verbatim in spirit — refuse with `version_holds_open_goals` **returning the goals**, then proceed with a per-goal `roll_forward`/`abandon` map and a required `reason`; six named codes, because the callers are agents. **Two briefs are corrected by measurement:** the lineage column is `succeeded_by_goal_id` (migration 160, on the OLD row pointing forward) — not the `succeeds_goal_id` both 0250 and R17's brief name, so R17 needs NO migration; and R18's `is_maintenance` flag is load-bearing rather than cosmetic, because answering the close count with a `title NOT LIKE` would let a hand-titled goal silently exempt itself from the gate that decides when a VERSION closes. Promotion runs inside the close txn and BEFORE roll-forward (`ensureMaintenanceGoal` returns null for a non-`building` version, so a carry-over run first silently carries nothing); zero planning versions is not an error; R19's one migration should carry the `building` AND `planning` partial unique indexes so the promotion race resolves like a 23505 rather than a prayer. Rejected: auto-creating the successor version inside a close (the "fake hotfix version" escape hatch 0250 built the override counter to prevent), writing `limitations/<version>-shipped.md` from the route (file I/O in a ship transaction — it stays R23's CLI work), and a version reopen.](0263-how-a-version-closes.md) | version lifecycle / scope closure |
358
+ | 0264 | [**The ten working areas: one goal per area, held until 5,000 builders** ([task 1003696](https://cloudbongos.com/builders#/task/1003696) · goal 1000086 — *Strict versioning*, criterion `sv-cutover-proven-live`). The owner and the full team ran a manual goal review on 2026-09-07 and cut **38 open goals down to 10**, one per working area, each with a named owner and a described 5,000-builder end state: project creation (Rini), account/community (Rini), human project management (Scott), core distribution (Lars/Will), module distribution & economy (Will), the four-role experience incl. agents (Masterqua/Rini), government (Will/Masterqua), the credit economy (Scott), platform analytics (Lars), security (everyone bar Nils and Scott). **Why the rules were not enough on their own:** [ADR 0250](0250-strict-versioning-the-version-boundary-is-the-scope-gate.md) D1 closes the goal set of a BUILDING version — which freezes 38 goals exactly as effectively as it would freeze 10. Enforcement cannot shrink a scope that is already too large, so the rules and the cut are two halves of one move; the horizon is a SCALE (5,000 builders), not a date, because that is the question that decides when this set stops being the right set. **The finding that reorders the goal:** none of the cut can be executed today. R10 ([task 1003597](https://cloudbongos.com/builders#/task/1003597)) made `POST /goals/:id/archive` refuse a goal holding unfinished tasks and demand a disposition — correct, and exactly D3 — but the vector that SUPPLIES a disposition is R14 ([task 1003601](https://cloudbongos.com/builders#/task/1003601)), unshipped. Every one of the 12 deletes and every fold goes through that door, so **R14 is the keystone of the whole cut**, not one feature among thirteen, and is built before the rest of the version-close chain rather than in rank order. It looks like a bug and is not: the refusal is the design, landed one task ahead of its own remedy. "Delete" means `archived`, never a row removal (`goals.status` is open|achieved|archived, migration 160; reversible via `/reopen`) — preserving the rows keeps shipped-task attribution and credit history a delete would strand. Three open goals were NOT covered by the review and are surfaced as open questions rather than guessed: 1000003 (the catch-all — already R25's target), 1000065 (assumed to be area 2's community half), and 1000088 (repo cleanup — maps to no area, needs an owner decision). Deliberately does not decide: the areas' priority order, criteria for the six goals R29 must create (inventing criteria for someone else's area is how a goal acquires scope its owner never agreed to), or any task rehoming (R25/R26).](0264-the-ten-working-areas.md) | scope / goal set / owner decision |
359
+ | 0265 | [**Work with no goal survives a version close, and the order the ten-area cut actually needs** ([task 1003713](https://cloudbongos.com/builders#/task/1003713) · goal 1000086 — *Strict versioning*, criteria `sv-every-task-has-a-goal` + `sv-cutover-proven-live`). `closeVersion`'s post-apply invariant counts GOALS, and every task write inside the close is scoped `WHERE goal_id = $1` — so a task belonging to no goal is reached by no disposition, counted by no re-count, and ships stranded on a version that is over. Not hypothetical: six such tasks already sat on shipped GDS-V3/GDS-V4, and 47 rows on BONGOS-V1 carried a null `goal_id` (R11 required the column going forward but never backfilled, because every catch-all lookup matches the exact title `<VERSION> — general` and BONGOS-V1's is titled 'Work with no goal yet'). Decision: a second invariant in the same transaction refuses a close leaving ANY non-terminal task on the version, failing `CLOSE_LEAVES_STRANDED_TASKS` with the ids; exempting only the maintenance goal (its carry-forward runs in the later `onClosed` hook) — through the SAME `maintenanceGoalExemptSql` the goal count uses, never a second copy — and `__smoke__` fixtures. Consequence recorded here too: because `PATCH /tasks/:id {goal_id}` refuses a cross-version move (409 `goal_version_mismatch`) and `roll_forward` CREATES a successor rather than merging into one, [ADR 0264](0264-the-ten-working-areas.md)'s cut must run home-then-close-then-**fold on the planning version**, where task and destination finally share a version. Rejected: a `fold_into` disposition (puts scope decisions inside a shipping transaction), creating the six new areas on the building version via R07 admissions (spends ten overrides to route around the rule they price), and a backfill instead of a gate (fixes today's rows, not the forty-eighth).](0265-work-with-no-goal-and-the-order-the-cut-needs.md) | lifecycle / version boundary |
360
+ | 0266 | [**The Board Room is its own surface, reachable by whoever may vote** ([task 1003734](https://cloudbongos.com/builders#/task/1003734) · goal 1000111 — *Working area 7, Government*, criterion `wa7-government`; owner decision 2026-09-08). The hall's only **upward-pointing** gate ([ADR 0175](0175-the-board-room-full-idea-ratification.md)) shipped as a hash tab inside `/government`, and two things about that did not survive inspection. **The nav never followed the page:** R16 correctly widened the PAGE gate from `government.manage` (archon) to `page.view.government` (metic+) when the page became three rooms — its own comment says *"the shell follows the widest legitimate audience"* — but `shell.js` still declares that item as label Permissions with gate archon, so a Metic who SITS on the board has no nav link at all and the only word in the nav is Permissions. Watch, Harbor, Gate and Sessions each own a nav item; the voting room was reachable only by knowing a URL fragment. **The surface was narrower than the franchise:** `board.vote.cast` floors at XENOS deliberately (`board.js:149` — the floor is low *so that widening the board works*) while the shell demands metic+, so the moment a constitution widens membership below Metic — the exact act [ADR 0175](0175-the-board-room-full-idea-ratification.md) §6 exists to make a CONFIG change — those members may vote by API and cannot load the page they would vote on, reintroducing the code-path change §6 removed. Decision: **its own page at `/board-room`, gated on the same atom the vote route checks.** The floor stays coarse — membership is still checked in-handler per item against that item's own snapshotted constitution — so a builder who clears the floor but sits on no board sees the room and no ballot, which is honest (§9 made the ballot open on purpose). **The migration is client-side because a fragment never reaches the server:** `#board-room` cannot be caught by any server route, so `government.js` redirects on load, preserving `?item=N` — recorded as a decision because adding a regex to `serve-internal.js` looks like the obvious fix and silently never fires. Three writers move (Discord `board-broadcast.js`, the `board_votes` need href, docs), and the redirect stays a release regardless because announcements already in channel history carry the old link forever. A **waiting-vote count** goes on the nav item: the data already exists (`boardVotesNeed` carries a live count pre-filtered to what the vote route would accept, served on `GET /me`), but `shell.js` has NO badge/bell/dot mechanism at all, so one is built once, generically, zero-is-silent, visible to exactly whoever the item is. Found while scoping and filed separately ([task 1003738](https://cloudbongos.com/builders#/task/1003738)): the hall renders needs as a SINGLE slot and `computeNeeds` sorts only by state with a stable sort, so registry order decides inside the `action_needed` bucket and `boardVotesNeed` sits fourth behind `artKeyNeed` — any builder with an unresolved image key never sees the board notice, which was live for this decision's own owner at the moment they asked for the bell. Rejected: a nav item merely deep-linking to the existing tab (cheaper, keeps the hash links free, but leaves the voting room inside a page whose gate answers a different question); a bell in the global header (a second inbox, needing its own read/unread and dismissal semantics); and a SOUND (`alert.wav`/`chime.wav` already ship, so it would be easy — it was not asked for).](0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md) | government / the board / hall nav |
361
+ | 0267 | [**Unanimity, and the revise-and-re-sit loop** ([task 1003733](https://cloudbongos.com/builders#/task/1003733) · goal 1000111 — *Government*, owner decision 2026-09-08). A Full Idea is ratified by **unanimous** agreement from round one; a sitting that does not carry RETURNS to its author, who revises and re-sits it, unbounded, until the board is unanimous. `unanimous` joins `PASS_RULES` as a fourth answer on the axis these rules actually differ on — **what silence means**: it is two conditions, not one, because "nobody objected" is true of an empty room. Everyone who spoke must have said yes AND somebody must have spoken, so an unvoted sitting RETURNS, which is exactly what makes the clock the owner asked for safe under it ([ADR 0191](0191-majority-and-the-author-who-may-not-decide-alone.md) §4: a deadline may only be given to a rule whose expiry means return). **There is deliberately no membership denominator** — that is the whole difference from `majority`, and it is the owner's "silence does not block" as code: an absent member is not in the reckoning, so one yes out of four carries. The stated cost is that an attentive minority can ratify on a distracted board; the alternative is one person on holiday stopping the pipeline. Trigger changes follow from the rule: a single yes does NOT close it (a later member must still be able to object — the sitting runs its clock, full turnout closes early), while a reasoned objection closes it AT ONCE (no vote-changing in v1 fixes the outcome, and the author needs the feedback to revise). ADR 0191 §3's author rule rides both unchanged. **`BOARD_DEFAULTS` is NOT moved**: a fresh instance stays the day-one monarchy, because under any non-author-yes rule a solo founder can never ratify their own ideas (0191 §5) — adoption is a board amendment, and no env var can change a constitution. The revise-and-re-sit loop already existed (`POST /inbox/:id/resubmit` re-grades and re-fires the window hook); what the board owes it is now pinned by test — a return CLOSES the item so the next window may open, and pays nothing. Named limits: a zero-vote return carries no human objection (the author gets the mechanical weakest-section flag), and this changes nothing live until the separate defect where a ratified amendment does not survive to the next read is fixed.](0267-unanimity-and-the-revise-and-re-sit-loop.md) | government / board room |
362
+ | 0268 | [**The constitution comes from two roots, and a decision that did not take must say so** ([task 1003739](https://cloudbongos.com/builders#/task/1003739) · goal 1000111 — *Government*). On 2026-08-25 cloudbongos.com's board RATIFIED an amendment (rank:metic+ · consent · a 1440-minute sitting) and the constitution never changed — for two weeks `GET /government/constitution` answered the day-one monarchy with the passed amendment sitting in its own `history` array directly beneath the contradicting `board` block. Every sitting since was decided under a rule the board had voted to replace, and it silently un-shipped [ADR 0191](0191-majority-and-the-author-who-may-not-decide-alone.md) (majority) and would have un-shipped [ADR 0267](0267-unanimity-and-the-revise-and-re-sit-loop.md) the same way. **Cause:** `modules/government/config.js` resolved BOTH its neutral starter and its instance pack from one `path.resolve(__dirname, '..', '..')`, under a comment saying the roots coincided *today* — true in a single checkout, false on a STANDALONE instance ([ADR 0108](0108-instance-composition-configurable-root.md) §1), where the server runs `node_modules/@cloudbongos/core` with `WorkingDirectory=<instance repo>`. The neutral path stayed right (it really is core content); the INSTANCE path resolved inside the core package, so the host's own `config/government.json` was never read and `applyBoardAmendment` wrote the ratified amendment into `node_modules`, where the nightly core upgrade ([ADR 0161](0161-publish-on-merge.md) cuts a release per merge) erased it within a day. **Nothing threw and nothing could have** — both paths exist, both are writable, and an absent instance pack is a legal state, so the read fell through to the neutral monarchy exactly as designed. `src/branding.js` had the pattern right three files away. **Fix:** neutral from `resolveCoreRoot()`, instance from `resolveInstanceRoot()`, reached through `src/module-api` — which had exposed NEITHER resolver, and that absence is precisely why the module re-derived the wrong one ([ADR 0083](0083-modular-architecture-strangler-build-plan.md) forbids requiring a core internal). Resolved at load, not per call, because `resolveInstanceRoot()` falls back to `process.cwd()` and a lazy resolve would let a `chdir` move the constitution; `applyBoardAmendment` now mkdirs, since the write targets a directory the core does not own. Single-checkout behaviour is byte-identical, asserted rather than claimed. **The detector, which is the durable half:** `constitutionView` carries a `divergence` block comparing the newest PASSED amendment (sanitized as an apply would write it) field-by-field against what is in force, rendered above the hall's dials in words rather than config keys. It REPORTS and never HEALS — the instance pack is also the file a human editing the constitution touches, so a silent re-apply would revert a legitimate hand edit with no way to tell the two apart. A RETURNED amendment is never compared (that is what *returned* means). Does NOT put the 2026-08-25 amendment back in force — that is a board act. Sibling sweep RUN, not promised: one real hit filed as [task 1003745](https://cloudbongos.com/builders#/task/1003745) (quarantine.js's repo-containment refusal is blind to the instance repo).](0268-the-constitution-comes-from-two-roots.md) | government / instance roots |
363
+ | 0269 | [**The CLI session store is host-keyed at a fixed anchor; the per-brand file stays the active pointer** ([task 1003741](https://cloudbongos.com/builders#/task/1003741) · goal 1000090 — *Working area 4, Bongos Core distribution*). The CLI resolved its session path from the BRANDING PACK, read out of whatever checkout the process stood in — one slot, that moved. Standalone (the public CLI's whole situation) no checkout means no brand, so every instance shared `~/.config/cloudbongos/gds-session.json` and signing into a second DESTROYED the first; in-repo it landed in `~/.config/<slug>/` where the standalone CLI could never find it (`npx … api GET /me` from a bare dir returned the cloudbongos builder while a valid hermeslines session sat on the same machine). Not theoretical — the owner's config dir carries a hand-made `gds-session.<redacted>.bak.json` and four more of the same shape. **Decision: one file per instance keyed by HOST at a FIXED anchor (`~/.config/<FALLBACK_DIR>/instances/<host>.json`), with the per-brand `gds-session.json` left exactly where it is as the ACTIVE pointer.** Four load-bearing parts. (1) The anchor must never read `configHome()`/`configDirName()`/the pack — a test asserts the function body names none, since routing it back through the brand silently reinstates the split. (2) The active pointer does NOT move: every existing reader (in-repo skills, the card hook, the dev box, `readSessionToken`) looks there, and the store is purely additive. (3) The OUTGOING session is archived BEFORE the incoming one lands — a session written before the store existed is not in it, so without this the first login after upgrading still loses it, the bug surviving its own fix. (4) A stored token is VERIFIED against `/api/gds/me` before being reinstated; unverified falls through to the real device flow, `--force` signs in as somebody else. `login` also names the other instances and how to switch, since sessions are now kept rather than overwritten. Files 0600 in a 0700 dir; an `api_base` naming no host is not stored rather than stored under a guess, so nothing can write outside the store. The dir is `instances/` NOT `sessions/` — `sessions` is a live module key and [ADR 0083](0083-modular-architecture-strangler-build-plan.md) §Decision #4 forbids kernel machinery naming one; the fitness gate caught it. Rejected: moving the active pointer (breaks every reader for no visible gain); one file holding a map (changes a format hooks and the dev box parse today); keying under `configHome()` (the bug); restoring without verifying (hands over a dead session that looks live); a new `bongos switch` verb (login is the command already reached for, and the public CLI's surface is deliberately small).](0269-the-cli-session-store-is-host-keyed-at-a-fixed-anchor.md) | cli / auth / distribution |
364
+ | 0270 | [**The rank floor derives from facts, not from a prediction** ([task 1003663](https://cloudbongos.com/builders#/task/1003663) · goal 1000111 — *Working area 7, Government*). [ADR 0084](0084-auto-derived-per-task-rank-floor.md) derives every task’s claim-time `requires_rank` floor from `touches[]` + `security_sensitive`; [ADR 0049](0049-split-parallel-safety-contract.md) then retired PREDICTIVE `touches[]`, which is now backfilled from the real committed diff at SHIP — after the claim the floor exists to gate. So on every create vector that omits it the matcher saw an empty array, and because the floor is computed ONCE at creation and never recomputed, it returned `xenos` forever. Nothing threw: an empty `touches[]` is a legal state. **Measured before the fix: 88 tasks declared `needs_migration` and 68 of them (77%) sat at `xenos`** — including [task 1003372](https://cloudbongos.com/builders#/task/1003372), a READY task to hash the `builder_sessions` token store at rest, claimable by any newcomer; goal 1000111 alone held 20 such rows. Two mechanisms hid it: the authoring routes DISAGREE (`POST /goals/:id/tasks` REQUIRES `touches[]` as its scope-wall blast radius, so the floor derives there, while its sibling `POST /tasks` — the route `/goal-create` actually files seed tasks through — treats it as optional), and `goal-create/SKILL.md` told authors in ONE line to *omit `touches[]`* and *never hand-set `requires_rank`*: omit the only input, never set the output. **Decision:** the floor derives from facts a task already carries at create time, never from a prediction — `deriveRequiredRank` gains `opts` with two touches-free inputs, `needs_migration` (the direct successor to the `migrations/` entry `POST /tasks` used to fold into `touches[]`) and `scopeProtected` (the owning goal’s `scope_modules` wall, ALWAYS available since `goal_id` is required, read through the same `scopeIncludesProtected` primitive the pre-push hook, grader and main-audit use, so it cannot drift). Derived at the LOWEST create layer in `createTask` after `goalIdEffective` resolves — the ADR 0096 argument — so every vector inherits it and patching the two routes separately (which is how they came to disagree) is not repeated. `updateTaskRequiresRank` re-derives with the same four inputs, else an Archon setting a migration task to `xenos` would silently undo the create-time floor; raise-only throughout; `archon` still never auto-assigned; a strict `=== true` so a stray truthy value cannot wall off the open queue. One-time backfill `core_236` raises still-CLAIMABLE rows only (the floor is a claim-time gate; rewriting shipped rows changes no live decision) and never lowers. **Accepted cost:** a protected goal wall is blunt — any protected module in the wall floors the whole goal, so a `hall-ui` task in a mixed goal floors at Metic too; same semantics the ideas vector has enforced since task 1003066, fails safe, and the remedy is to narrow the wall. Zero claimable `newcomer_friendly` tasks exist today, and no carve-out was added for them on purpose. Rejected: re-requiring predictive `touches[]` (re-litigates ADR 0049); recomputing at ship (too late by construction); deriving from `module_key` (null on most tasks, including the reporter).](0270-rank-floor-derives-without-a-prediction.md) | permissions / task lifecycle |
365
+ | 0271 | [**The ideator surface is two halves: Bongos owns the prose, the harness owns the chrome** ([task 1003506](https://cloudbongos.com/builders#/task/1003506) · goal 1000095 — *Working area 6, Governor / Builder / Artist / Ideator experience*, criterion `wa6-kernel-and-packs`; owner idea 1000732, 2026-08-16). The ask — *no need for an ideator to see all the text and technical work, or the inner Bongos logic* — is two problems in one sentence. **The PROSE half ships here**, through three seams that already existed: a `machinery` field on `interaction-prefs` (per-PERSON, default `show`, riding the live Conductor injection rail — advisory 8, which means task [#1002514](https://cloudbongos.com/builders#/task/1002514)’s claim that `contractFor()` has zero consumers is STALE); the `ideator` directive in `discipline-modes.json` (per-ROLE, the file whose own docs say *to give a discipline its own experience later, add an entry here*); and a new surface section in `/ideate`, which the directive names as the only home of the experience. Default `show` means no existing builder changes. **The CHROME half is the harness’s and is deferred to a spike, not declared impossible** — this record was nearly written on the wrong assumption that config cannot reach it. Two real levers exist: **A**, shipped client defaults (`/focus` renders *only your last prompt, a one-line tool-call summary, and the final response*; `alwaysThinkingEnabled:false` stops thinking being generated; `showTurnDuration`/`spinnerTipsEnabled`/`statusLine` trim chrome) — but `/focus` is fullscreen-only (research preview), `viewMode`’s committability is UNVERIFIED, and thinking suppression is per-model; and **B**, an app-owned surface over `claude -p --output-format stream-json`, whose documented streaming-UI pattern (working indicator on a `tool_use` block, render on `result`) IS the ask, is model-independent, and is the recorded bias. **Bypass is REJECTED** despite the idea proposing it, on three grounds ending in a technical one: the least technical role is the worst audience for removing confirmations; the prompt was never the gate ([ADR 0016](0016-trust-boundary-server-enforced-permissions.md) — rank is checked server-side, and sub-Metic protected-path globs already narrow an ideator); and a project **cannot** ship it — `defaultMode: bypassPermissions` in `.claude/settings.json` silently does not take effect and starts the session in Manual. Rejected also: a hall ROOM on the Studio’s precedent (goal 1000074’s *a place, not a form*) — right for an artist, wrong here, because the ideator’s complaint is about the Claude session’s surface, which the hall does not render; and waiting on per-session declared role, whose dependency is softer than filed since `discipline` + the claim-time fork already carry the craft axis. When declaration lands the contract MOVES into the Ideator pack with nothing undone.](0271-the-ideator-surface-is-two-halves.md) | roles / ideator experience / session surface |
366
+ | 0273 | [**The hall nav can gate on a permission, and the Board Room leaves Government** ([task 1003744](https://cloudbongos.com/builders#/task/1003744) · goal 1000111 — *Working area 7, Government*, criterion `wa7-government`; owner decision 2026-09-09). `shell.js` `gateAllows()` resolved six gate keys and every one was a RANK, so a nav item whose page is gated on an ATOM had to borrow the nearest rank floor and hope the two agreed. [ADR 0266](0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md) ended that: the Board Room page asks `board.vote.cast`, which floors at XENOS deliberately (`board.js` — the floor is low *so that widening the board works*), while its nav item read the Government group's metic floor; the carve filed the gap rather than guessing at it. Latent today because the constitution seats `rank:archon`, live the moment an instance widens membership below Metic — the exact act [ADR 0175](0175-the-board-room-full-idea-ratification.md) §6 exists to make a CONFIG change — at which point those members reach the room by URL and by the `board_votes` deep link with no sidebar entry. **Decision: a `perm:<atom>` gate key, answered by the SERVER.** `NAV_PERMISSION_ATOMS` in `routes/me.js` is a frozen allowlist of the atoms a nav item may gate on; `/me` resolves each through the `government` port and returns `permissions: { atom: boolean }`; `gateAllows` reads that map and never re-derives it (the social-mode precedent — a page that decided for itself could disagree with the rest of the hall); the Board Room item declares the SAME key `requireBoardVotePage` checks, so the two cannot drift. An ALLOWLIST rather than the full resolved set, because the full set is the shape of the instance's governance and a cosmetic sidebar has no business shipping it on every page load. `permissions: null` (government off, port missing, resolver threw) means *cannot say*, not *holds nothing*, and HIDES the link — `default: false` and OFF-is-the-fallback, applied. Cosmetic throughout ([ADR 0016](0016-trust-boundary-server-enforced-permissions.md)): the page gate and each route's `requirePermission` are the wall. **The placement was the owner's call, and the reason this task existed:** gating honestly while leaving the item in the Government group would put a WIDER item under a narrower head, so a newcomer board member would see a group labelled "Government" holding one link — more authority than they hold. Declined; the item MOVES to the unlabeled project group after Tasks, because a sitting is work the project does, not a monitoring surface. Government's audience, head and floor are unchanged and it is rank surface throughout again. Also rejected: widening the Government head to show whenever any item inside is visible, and a single-item "Board" group. `tests/hall_nav.mjs` gained the MISSING HALF of its group invariant — it checked only that a head is never wider than its items, never that an item is wider than its own head (an orphan link under a hidden label, exactly the declined arrangement) — and `GATE_WIDTH` learned `government`, which had been `undefined` so every comparison against that group passed vacuously. The harness `/me` fixture carries `permissions`, or the item would be permanently invisible in `scripts/hall-preview`. **Not done:** no other item re-gated — `people` still reads `government` for `project.curate`.](0273-hall-nav-can-gate-on-a-permission.md) | hall nav / permissions / government |
367
+ | 0272 | [**A craft is an identity, not a toolchain: `artist` becomes core** ([task 1003719](https://cloudbongos.com/builders#/task/1003719) · goal 1000111 — *Government*). Reported from Discord on 2026-08-15 as "cant save preferences of role types in account page"; reproduced live 2026-09-09: `GET /me` returns `preferred_disciplines: ["artist","ideator"]` and `PATCH /me/disciplines` with that SAME array returns `422 invalid_discipline {bad:["artist"]}` — the API handing a builder a value and refusing to accept it back. **Cause:** `updateBuilderDisciplines` validates against `enabledDisciplines()`, and R59 (task 1198) put `artist` in the module half on the reasoning that the craft existed to make the game pixel art, so it rode `art-pipeline`. **That gate had stopped existing** — art-pipeline was never converted to a discovered module, there is no `modules/art-pipeline/module.json`, and so NOTHING declared `contributes.disciplines: ["artist"]`. Measured on a FULL repo checkout with the pipeline present on disk: `["engineer","ideator","ui"]`. The craft was offered by no instance in any configuration, and is unreachable by construction on a published core anyway, since [ADR 0098](0098-public-mirror-publish-manifest.md) excludes `modules/art-pipeline/` for a hard copyright reason (ripped reference maps). Meanwhile it stayed first-class everywhere else: the `builders.preferred_disciplines` CHECK (086/176), `tasks.discipline`, and the help-request craft roster all accept it — so a TASK could be an artist task and a PERSON could not be an artist. **The tests were green because they skipped:** the artist assertion sat behind `if (!EFFECTIVE["art-pipeline"]) return;`, always false, so the only real leg never ran. **Decision:** `artist` joins `CORE_DISCIPLINES` → `["engineer","artist","ideator"]` (migration 176 enum order); modules still contribute their own (ui-design → ui). A craft names WHO A BUILDER IS, not which toolchain ships — an instance can have artists without a pixel-art pipeline, and binding the identity to one optional module was the category error. The new invariant, pinned with no skip guard over the widest module set: anything the DB CHECK will store, some instance shape must offer. Rejected: enabling art-pipeline (impossible — absent from the package, and enabling an absent module throws at boot); writing the missing manifest (works only in a full checkout); an instance-declared discipline list (leaves the core CHECK describing a craft core cannot offer, and does not fix the vanilla case); stripping unofferable crafts from stored rows (deletes a builder stated identity to make a validator mistake look consistent). No migration — the rows were always CHECK-valid; only the validator disagreed.](0272-a-craft-is-an-identity-not-a-toolchain.md) | crafts / instance composition |
368
+ | 0274 | [**One kernel, three role packs: the always-loaded instructions stop being an engineer's** ([task 1002990](https://cloudbongos.com/builders#/task/1002990) · goal 1000095 — *Working area 6, Governor / Builder / Artist / Ideator experience*, criterion `wa6-kernel-and-packs`). The root `CLAUDE.md` loads in full every session for every craft, and it was an engineer's document: 250 lines / 32,422 chars of claim-before-working, worktree-per-claim, `DEPS_NOT_SHIPPED`, `touches[]` and `/merge-mode`. An ideator and an artist each paid the full token cost of a pipeline they were not going to run and read it as instructions addressed to them — and it CONTRADICTED the same goal's `wa6-drift-stops-the-session`, which says ideation *"needs no claim"*, while the kernel's hard rule said every change is backed by one. The craft-specific half lived in three SKILLS (`/dev`, `/ideate`, `/paint`), the wrong carrier twice over: a skill's description is resident in every session's listing whatever craft is running (27,022 chars against a ~8,000 budget, three of them over-length), and a skill must be INVOKED — a session that never types the command works from the kernel alone, which is the engineer's document again. **Decision: a role-neutral kernel plus one pack per craft under `docs/packs/`, bound by a registry CI checks.** The kernel keeps what is true for every craft (the lifecycle, the money, the trust boundary, the identity split, the charter, navigation) and drops to 216 lines / 22,572 chars, clearing the character target it had been over; the per-line test gains a second half — *is it true for every craft?* The Engineer pack inherits today's methodology; the Ideator pack absorbs `/ideate` and writes down the two grades the criterion names (a quick idea stays ONE LINE, then Claude ASKS whether to develop it into a Full Idea — five questions drawn out of the conversation, in the ideator's own words, scored on completeness); the Artist pack absorbs `/paint`, generalised off the pixel-art pipeline per [ADR 0272](0272-a-craft-is-an-identity-not-a-toolchain.md) — show-first, as little interpretation as possible, and ARRIVE PRECALCULATED, because a session that opens with intake questions is a form. The three skills are deleted. `scripts/gds/discipline-modes.json` becomes the ROLE REGISTRY: a core craft names a `pack` (a file to read), a module-contributed discipline may still name a `skill` (`ui` → `/design`, unchanged as ADR 0272 requires), and `claim.js` prints whichever with the right verb. **`scripts/gds/role-pack-guard.js` HARD-FAILS CI** on a pack path that resolves to nothing, an entry with neither pointer, fewer than three packs, or a pack over budget (16,000 hard / 13,000 target) — hard precisely BECAUSE `claim.js` fails open: a broken path degrades to silence, the role is hollow, and no error says so, so CI is the only gate ("CI budgets each pack"). `docs/packs/` over `.claude/packs/` because `docs/` is already owned by the `methodology` scope key — a new top-level path resolves to NO key, and the scope map's own comment records the cost (a missing key is an empty wall, and goal-scope-check then denies every member-authored task); it is registered explicitly in the default-deny publish allowlist, in lockstep with the registry, or a released core would print an unresolvable pack path on every claim. `ship-card.js` moved with the claim (it would have shipped an "Open the work" button firing a deleted command — the 1003487 divergence in reverse) and `tests/idea_ideate_full.mjs` moved with its subject, its thirteen Full-Idea assertions passing against the pack unchanged. Governor is deliberately DEFERRED — three packs, not four, until the owner decides; the guard floors at three. Rejected: keeping the skills beside the packs (two homes drift, and the listing cost stays); packs AS skills (the exact failure being fixed); duplicating the safety rules into all three packs (the charter forbids it — three copies drift and the kernel becomes the one nobody trusts); leaving the guard advisory like the doc-comment check (that one gates a judgement, this one gates a resolvable path). **Not done:** the kernel is 216 lines against a ~200 target — still a warning, as it was at 250; the line count is the context-budget cluster's work (tasks 1003620/1003687/1003688), which the goal's audit sequenced behind this one.](0274-one-kernel-three-role-packs.md) | methodology / roles / context budget |
369
+ | 0275 | [**A role's written responsibility has one source, and the pack's copy is generated** ([task 1003732](https://cloudbongos.com/builders#/task/1003732) · goal 1000095 — *Working area 6*, criterion `wa6-written-responsibilities`; statements fixed by the area owner 2026-09-08). The criterion is one sentence — *"one source, shown on the profile, injected into the pack, referenced by grading"* — and before this task the three statements existed only in the criterion record, nowhere in the tree. The interesting half is why ONE source and not three good copies: if each consumer holds its own, they drift, and the drift has a specific unfair shape — the project shows a builder one standard on their profile, teaches their session a second, and grades the shipped work against a third, so someone is judged by a rule they were never shown. **Decision: a frozen map at `src/role-responsibilities.js` (verbatim owner text, imports nothing), read by three consumers, with the one copy that CAN drift generated and CI-gated.** The hall reads it through `clientModules()` — already injected into every served page as `window.__MODULES__` and already carrying the discipline roster the statements are keyed by, so the profile renders with no new route, no fetch and no copy. The grader reads `responsibilityFor()` through the module doorway and states the standard in the prompt as the project's, not its own. The PACKS get a generated block: markdown cannot require a JS module, so their copy is the only one that can go stale — and it is the worst to lose, being what a session is actually instructed by. `gen-role-responsibilities.js` writes it from the ROLE REGISTRY (so a renamed pack, or Governor when the owner decides, needs no edit), and `role-pack-guard.js` fails CI on drift. Three consequences worth the record. (1) `fitness.js` needed an EXEMPTION for the owner's own prose: the ideator's statement opens "Make good ideas" and `ideas` is a module key, so the no-module-keys scan flagged an English word — paraphrasing to satisfy a scanner is precisely what the task forbids, so the reason is written down and the file's import DIRECTION is still guarded. (2) Registering the freshness check inside `checkGeneratedArtifactsFresh` pushed `fitness.js` one line past its 1,500-line ratchet; rather than raise the baseline (which the check forbids in its own message) the assertion MOVED into `role-pack-guard.js`, which already owns the packs — one check now answers "are the packs correct". (3) The generator matches each pack's own line ending, because `.md` checks out CRLF on Windows and LF in CI, and a generator that always wrote `\n` would make the gate red on one checkout and green on the other for a file nobody touched. A craft with no statement (`ui`, module-contributed per [ADR 0272](0272-a-craft-is-an-identity-not-a-toolchain.md); Governor, deferred per [ADR 0274](0274-one-kernel-three-role-packs.md)) renders NOTHING everywhere — no empty quote, no grader line — because a grader told a role has no written standard would supply its own. The profile's avatar was repinned to the top row (the head grid is `align-items:center`, fine for three short lines, wrong once the block made the column tall); verified in both themes in the hall preview. `tests/role_responsibilities.mjs` asserts the criterion's actual content by sweeping `src/`, `scripts/`, `modules/`, `tests/` and `docs/packs/` for any second copy. Rejected: three hand-written paragraphs (the drift would be invisible); a dedicated route for the hall (a fetch and a failure mode for static text already on a rail); storing the statements in the DB (not live state — a row puts the project's written standard outside code review); letting the grader describe a craft from its name (the invented standard this removes); raising `oversized_file_count` to let `fitness.js` grow.](0275-one-source-for-a-written-role-responsibility.md) | roles / grading / generated artifacts |
370
+ | 0276 | [**The skill-listing budget cannot hold 64 skills and every trigger, so the number is ratcheted and the choice goes to the owner** ([task 1003620](https://cloudbongos.com/builders#/task/1003620) · goal 1000095 — *Working area 6*, criterion `wa6-role-experience`). Every SKILL.md description is resident in EVERY session for every craft, and `skill-lint` budgets the whole listing at 8,000 chars. It has been trimmed twice ([#1003548], [#1003587]) and grown back both times, because the 400-char per-description aim was a WARNING THAT ACCUMULATES. This task rewrote 49 of the 50 `.claude/skills/` descriptions — 17 warnings to 0, listing 25,624 → 19,316 chars, about 1,570 tokens returned to every session — with every trigger phrase preserved and CHECKED MECHANICALLY (a checker diffed the quoted phrases against HEAD and caught five real losses, all restored; four dropped strings were UI labels, not triggers). **8,000 was not reached, and the arithmetic says it cannot be:** across 64 skills the names (880) plus the mandatory quoted triggers (~5,370) are an irreducible ~6,250, leaving ~27 chars per skill to say what the skill DOES. Reaching the budget means trigger-only descriptions — trading truncation for the loss of the semantic signal routing leans on when the user’s words do not literally match a trigger. **Decision: ratchet `skill_listing_chars` at 19,316** in `fitness-ratchets.js` (fails open if the linter cannot load), so it cannot grow while the real question is open; the baseline is deliberately ABOVE skill-lint’s budget and is not a claim the budget is met. **Open and owner-gated:** delist skills (64 is a menu), scale the budget with the roster, or accept trigger-only text — recommendation is delist then rescale, filed as a blocker. `fitness.js`’s over-budget warning STAYS, as the visible trace of that question. Rejected: raising `LISTING_BUDGET_CHARS` to silence the warning (deletes the signal, not the debt); re-cutting the 14 module ui-design descriptions a week after [#1003587] wrote them, for ~2,000 chars toward a target still 9,000 away.](0276-the-skill-listing-budget-cannot-hold-every-trigger.md) | skills / context budget / routing |
371
+ | 0277 | [**A box is "in use" only while a human is attached, and the claim expires** ([task 1003507](https://cloudbongos.com/builders#/task/1003507) · goal 1000095 — *Working area 6*, criterion `wa6-role-experience`). `sweep-idle` skipped any box with `claude_active = true` at ANY age, and `claude_active` is a LATCH, not a level: only a heartbeat ping writes it, and `infra/box-heartbeat.sh` exits WITHOUT pinging when it sees nothing — so silence, the very signal the sweep exists to act on, could never clear it. Reproduced against the shipped selector: skipped at `idleMinutes` of 10, 120, 1440 and **5,256,000** (ten years). Not reaped late; never. The second leg is that `load > 0.2` kept `last_activity_at` bumping every 5 minutes anyway (a devcontainer plus an idle `claude` clears that floor on its own), so EITHER leg alone kept a box alive — which is why bounding only the veto looks like a fix and is not: the incident box's heartbeat was FRESH. Measured: `example-owner`'s box up since 2026-08-12, tmux `otb` UNATTACHED since 2026-08-15 18:01 UTC, `claude` burning 16 min of CPU across 25h of wall clock, **$20.21** of mostly-unattended compute. A THIRD defect, found while testing this and confirmed on clean `origin/main`, made the sweep inert regardless: `loadDeps()` read `_deps` before anything declared it, so it threw `ReferenceError` on first call and every command resolving a DigitalOcean client went with it — including `sweep-idle --apply`, which builds that client before the park loop. **The idle sweep could not park any box at all**, hidden because the suite's only `apply: true` test relied on the veto emptying the idle set before `makeDo()` was reached: one bug shielded by the other. **Decision: a box may not stay active longer than `BOX_UNATTENDED_MAX_HOURS` (12) without evidence a HUMAN was attached.** The heartbeat already computed that signal and folded it into one boolean; it now reports `attached` separately (login session, inbound SSH, open ttyd, or an ATTACHED tmux client via `#{session_attached}` — the signal that separates this box from a working one) and the server stamps `last_attached_at`. `claude_active` keeps its veto but it EXPIRES, bounded by a `claude_active_since` edge stamp. `pgrep -x claude` and the load floor remain reasons the box PINGS, never evidence anyone is THERE — a running process is not a person, and that distinction is the whole decision. The two `NULL` defaults deliberately DISAGREE: `last_attached_at` NULL means "no data" and keeps a pre-1003507 box on the old behaviour (core_238 pointedly does NOT backfill it — a backfilled `now()` starts a clock nothing can advance and parks every un-upgraded box one cap later, and box source sync is not prompt: idea 1000745 records 257 commits behind for three days), while `claude_active_since` NULL is REFUSED because an unknown latch age is the forever-latch itself, and is backfilled so the state is unreachable after deploy. 12h because parking is reversible since task 1002726 (snapshot kept), so a false positive costs one wake against $20.21 for no cap. Both clocks clear at park/wake/deprovision, or a woken box inherits an expired latch and is parked instantly — the fix reintroducing the bug from the far side. Rejected: tracking attachment INSTEAD of bounding the latch (the silent box keeps `true` forever, so the reported hole survives); bounding the latch alone (built first, and the verification probe caught it — the fresh heartbeat meant lifting the veto changed nothing); deleting the load floor (it is what makes an autonomous run count); measuring from `active_since` (that is uptime — parks a box worked on for days); raising `BOX_IDLE_MINUTES` (no threshold reaches an unbounded veto).](0277-a-box-is-in-use-only-while-a-human-is-attached.md) | dev box / cost / idle sweep |
372
+ | 0278 | [**A gated project still takes applications, and the exemption is scoped to the verb** ([task 1003525](https://cloudbongos.com/builders#/task/1003525) · the apply write itself in [task 1003624](https://cloudbongos.com/builders#/task/1003624) · goal 1000106 — *Working area 1, Project creation*; owner decision 2026-09-11). A project's owner sets who may SEE it (`platformVisibility`, [ADR 0192](0192-platform-visibility-member-door.md)) and who may JOIN it (`joinability`, [ADR 0194](0194-joinability-one-door-decision.md)) independently — and set to their middle values, members-only AND apply-to-join, the project took no applications at all: the member door refused every cookie-less request with `401` before the public `POST <api>/access-requests` could answer, because that write was not on the exempt list. The two settings composed into **"nobody can apply"**, which nobody chose. It survived because nothing LIED about it — the hub's join box relayed the project's own `401` honestly as `members_only`, and the hall's landing, where the apply form lives, is itself behind the door; the composition was simply unreachable. Found by the R14 proof ([task 1002333](https://cloudbongos.com/builders#/task/1002333)). ADR 0192 §3 had fixed the exempt list at "the door, the manifest, the probes and the downloads — and nothing wider" and left widening it as an owner call, which is what this is. **Decision: yes — and BOTH halves are exempted, each scoped to one path and one verb.** `POST <api>/access-requests` (it grants nothing — an application is a row in a queue the owner still reviews, [ADR 0201](0201-cross-project-application-vouched-row-live-record.md), already public on every non-gated project) and `GET <api>/access-requests/status` (without it the answer is half an answer: `bongos login` cannot re-poll the device flow after a `not_approved` — the `device_code` is spent — so an applicant would file a request and then wait on an approval they can never observe). `EXEMPT` entries may now be `{ re, methods }` beside the bare `RegExp`s, and `isExempt(path, method)` takes the verb as an OPTIONAL second argument that **fails closed** for a scoped entry when none is given, so the one-argument static callers cannot accidentally widen. **The verb is load-bearing, not tidiness:** the bare `GET` on `<api>/access-requests` is the OWNER'S QUEUE (`requireBuilder` + `access_request.review`), the surface listing would-be builders by name with their vouch state — a path-only exemption would have silently taken the member door off the front of it, leaving one layer where there were two, and the queue's own `requirePermission` still holding is exactly what makes that loss easy to miss. **It opens no oracle the gate was closing:** the status route's boolean twin `GET <api>/auth/web/admission-status` is ALREADY reachable on a gated project inside the `auth/*` subtree §3 exempts whole (§3 records that cost in as many words), and the two share ONE per-IP budget on purpose ([ADR 0209](0209-one-budget-for-the-account-existence-oracle.md)) so neither can be alternated against the other. What it DOES add, stated as the honest cost: applicant detail — `pending`/`dismissed`/`none` over the twin's bare `admitted`. Whether that answer should collapse is ADR 0209's still-open owner question and is deliberately NOT decided here. No hub change: `joinRelayOutcome` maps the RELAYED status, so it carries the project's real answer the moment the `401` stops. Rejected: "gated means gated" — hide *Apply to join* and say so in the manage blurb (coherent, and the call went the other way); exempting the path without the verb; exempting the write alone; collapsing the status response while the route happened to be open (that is how a deferred decision gets made by accident).](0278-a-gated-project-still-takes-applications.md) | project visibility / join door / member door |
373
+ | 0279 | [**An upgrade is proven by the served version, not by a health check** ([task 1002884](https://cloudbongos.com/builders#/task/1002884) · goal 1000090 — *Working area 4, Bongos Core distribution*; from idea 1000682). On 2026-08-11 the auto-upgrade sweep printed `✓ upgrade complete — core 1.19.13 → 1.19.56`, wrote a success row to `core_upgrades` and exited 0 while live kept serving **1.19.13**. Three shipped checks formed a closed loop that could not see the failure they existed to catch: the `systemctl restart` failed with `Interactive authentication required` (a `User=` unit, no TTY) and `upgrade.js` treated it as a WARNING and fell through; `readInstalledCoreVersion()` then confirmed the version on **disk**, where `npm install` had correctly put it; and `pollHealth()` got a 200 from the **still-running old process**, because a health check confirms a port is served, never *what* serves it. A lying tool is worse than a broken one — nothing goes looking. The damage outlived the incident: `subscribe.js` had already routed the unattended lane around `bongos upgrade` in favour of `go-live.js`, citing this false-pass in a comment. **Decision: a bump is confirmed by asking the running process what version it is.** (1) A failed restart enters the same auto-rollback path as a failed install/migrate/health and exits non-zero — rolling back rather than merely erroring keeps disk and process consistent, since disk-ahead-of-process is the state that made the incident invisible. (2) The served version is read back from `/version` (`coreVersion`, since 1.17.2, prelaunch-gate exempt), defaulting to the `--health-url` origin and overridable via `--version-url` — derived rather than opt-in because every existing call site passes only `--health-url`, and a check you must opt into is off exactly where it is needed. (3) A **mismatch** fails (proof of failure → roll back); an **unreadable** endpoint only warns (absence of proof — refusing every such bump would regress harder than the false-pass), except under an explicit `--version-url`, which asks for proof and therefore gets a failure. That strict mode is what the unattended subscription lane now passes, its roster already carrying the URL. **Escalation is `sudo -n`, not a hand-placed polkit rule:** `restartService()` retries a failed restart through `sudo -n` when not root — the idiom `dev.js`/`dev-lib.js` already use and consistent with `provision.js`, `upgrade.js` having been the one place that restarted without escalating. The live `/etc/polkit-1/rules.d/<redacted>.rules` mitigation is superseded: a rebuilt box inherits code, not hand-placed `/etc` files (polkit route kept in [`docs/recipes/instance-service-restart.md`](../recipes/instance-service-restart.md)). `--no-health-check` stays the single escape hatch and now waives the read-back too. Rejected: comparing `startedAt` (cannot distinguish a restart onto the same old core from one onto the new); making an unreadable endpoint fatal by default; requiring `--version-url` everywhere (absent from every current call site — the same "off where it matters" failure in a new costume); keeping polkit as the answer.](0279-an-upgrade-is-proven-by-the-served-version-not-the-health-check.md) | core distribution / upgrade verification |
374
+ | 0280 | [**The unattended lane may drive a co-tenant, because the roster entry IS the per-instance decision** ([task 1003843](https://cloudbongos.com/builders#/task/1003843) · goal 1000106; from [task 1003521](https://cloudbongos.com/builders#/task/1003521), which built co-tenant mode into `go-live.js` and deliberately left this caller alone). `subscribe.js` keeps a SECOND COPY of go-live's target contract, and it drifted: `GO_LIVE_REQUIRED` still listed `deployTimer` after 1003521 made it optional, so `goLiveTargetFor()` returned `missing: ['deployTimer']` for every provisioned co-tenant, and `rawTopologyFor()`'s fixed field list dropped `pinMode` before the target was built — the same stale-normalizer trap that function's own comment was written to fix, one field later. **1003521 deferred this as a policy judgement call; reading the fallback shows the premise was wrong.** `mapped.ok === false` never meant "skip this instance" — it meant "deploy it the WEAKER way": a bare `bongos upgrade`, no backup, no deploy-timer guard, and a pin COMMITTED AND PUSHED into the co-tenant's own repo, which is exactly what `pinMode` exists to prevent. The lane was already auto-deploying co-tenants; the only open question was whether it did so well. A THIRD defect sat behind the other two and would have turned the silent downgrade into a hard failure had only the first been fixed: the normalizer yields `deployTimer: null`, an explicit null SURVIVES `JSON.stringify` into `--target-json`, and go-live's `validateTarget` runs `isSafeToken(null)` on it and rejects the whole target. A FOURTH sat PAST the go-live call and is what separates "reachable" from "safe": after any successful upgrade the lane runs its own pin net, `commitPin()`, which commits the pin files and PUSHES them to `origin` — a net that predates `pinMode`. Under `local-commit` go-live leaves the tree clean so it no-ops by luck; under **`leave-dirty`** the pin is in the working tree BY DESIGN, so the net committed it and pushed it into the customer's own repo — the lane honouring `pinMode` right up to the moment it undid it. **Decision: the lane may drive a co-tenant and needs no new gate, because listing an instance in `config/update-subscriptions.json` already IS the explicit per-instance operator decision** — the roster ships empty, nothing is touched until an operator lists it, and the routine is autonomy-gated (`requiresAutonomy: true`, default OFF) on top of that; a second consent gate asks the same person the same question twice. So: `GO_LIVE_REQUIRED` now MIRRORS go-live's `REQUIRED_FIELDS`, with a test reading BOTH source files and failing on drift; `deployTimer` moves to the optional-field loop so a co-tenant OMITS the key rather than carrying a null; `rawTopologyFor` carries `pinMode`; `resolveGoLive` gains a `needsCoTenant` probe leg; and `goLiveEngineFor()` makes the decision ONCE for both the dry run and the apply path, which had re-derived it separately. The pin net now obeys the same `pinMode` go-live was given — `commitPin()` takes `push` (default true, so the platform instance is unchanged), `local-commit` commits without pushing, `leave-dirty` skips the net, the rollback tidy follows the same rule, and the "not durable" advice stops telling an operator to `git push` a repo we do not own; its refusal to touch a worktree carrying NON-pin changes is unchanged in every mode. **The probe is CONDITIONAL on purpose** — demanding `pinMode` of every target would strip the go-live path from any instance between the `--target-json` release and 1003521's, trading a live regression for a hypothetical one. Rejected: a second consent gate (an `allowCoTenant` flag); deferring to the unspecced fleet control plane ([task 1001948](https://cloudbongos.com/builders#/task/1001948)) — which does not leave co-tenants alone, it leaves them pushing pins into customer repos; dropping `deployTimer` from `GO_LIVE_REQUIRED` and nothing else (the fix as proposed — trades a silent downgrade for a hard rejection); probing unconditionally; reaching co-tenant mode WITHOUT teaching the durability net about it (nominal support that breaks its own promise seconds later); and teaching `loadSubscriptions()` about `pinMode` instead, since that normalizer ships in the control plane's vendored core that nobody bumps — the exact trap `rawTopologyFor` routes around.](0280-the-unattended-lane-may-drive-a-co-tenant-because-the-roster-is-the-decision.md) | core distribution / unattended deploy |
375
+ | 0281 | [**An instance’s identity is its own unix account and its own password-authed Postgres role** ([task 1003369](https://cloudbongos.com/builders#/task/1003369) · goal 1000094, audit ref `B3` of the 2026-08-29 security audit). [ADR 0111](0111-instance-hosting-provisioning-module.md) made standup automatic and [ADR 0128](0128-provisioning-runner-privilege-and-db-auth-model.md) settled how the RUNNER escalates; neither settled who the INSTANCE is once it runs, and the answer was that everybody was the same person. `serviceUnit` hardcoded `User=lars`, and **co-tenant is the default shape** — so on a shared box every instance’s process ran under one uid, and `pool.js` connected by **passwordless peer auth**. Two consequences, neither a bug in any route: a unix user may read `/proc/<pid>/environ` of its own processes, so every instance could read every sibling’s `GITHUB_APP_PRIVATE_KEY`, hub client secret and OAuth secret; and peer auth answers “which unix user are you”, so any code as `lars` could open a pool against **any** database on the box by naming it — including the control plane’s own `builders` and `builder_sessions`. The `0600` `web.env` and the per-instance DB were already correct: sound walls with one door, and every instance had the key. **Decision: each instance gets its own unix account and its own password-authed PG role, both derived from its slug, both created by the provision run.** The account is `bongos-<slug>`, hash-folded past utmp’s 32 chars — prefixed because a project may legally be called `postgres`, and hash-suffixed because two slugs sharing a 16-char prefix would otherwise fold onto ONE account, which is the shared identity being removed. Its home is `/var/lib/<user>` and deliberately **not** under `/home`, since the unit sets `ProtectHome=read-only` and the server writes `builder-secret.key` inside its config home on first boot — a `/home` account would make that `EROFS`. The unit gains `ProtectSystem=strict` / `ProtectHome=read-only` / `PrivateTmp=true` with `ReadWritePaths` as the whole allow-list; **the checkout the service runs is deliberately not writable by it**. The role is `LOGIN` + password, owns its own DB, and is NOT superuser/CREATEDB/CREATEROLE — with **`REVOKE CONNECT … FROM PUBLIC` as the half that actually isolates**, because PG grants `CONNECT` to `PUBLIC` by default and per-role passwords alone leave a sibling one peer-auth hop away. Grants run in a SECOND statement **after** the migrate, since the privileged migrate runs as `postgres` and owns every table it creates; `ALTER DEFAULT PRIVILEGES` aims the same grant forward. The password is minted per run and never stored — the same run sets it on the role and rewrites the `0600` env file, so **every provision rotates it** — and `pool.js` reads `PGUSER`/`PGPASSWORD` **explicitly** rather than via `pg`’s env defaults, because the whole defect was a connection whose identity nothing in the code stated. **Absence is the old behavior:** no password ⇒ no credential lines and no pool keys, so an instance provisioned before this keeps its peer auth and the rollout cannot brick a box. Two consequences worth the record. (1) **The generated SQL contains no double quote, and that is load-bearing** — every literal is dollar-quoted and every identifier goes through `format(%I)`, because these commands are embedded in the dedicated droplet’s cloud-init as `- [ bash, -lc, "…" ]`, a YAML DOUBLE-QUOTED scalar: a plain `"role"` identifier reads fine in `psql` and silently truncates the line. A test asserts the absence rather than leaving it to habit. (2) **A partial, stated honestly** — the account is added to the app-user group so it can read the checkout it runs from (without it the unit fails at exec with `EACCES`), so FILE-level co-tenant separation is not what this delivers; the process and database boundaries are. Rejected: a `DATABASE_URL` in `web.env` (needs no pool change, but trades a stated, assertable identity for a socket-path parse that is `pg-connection-string`’s business); per-role passwords WITHOUT the `PUBLIC` revoke (reads like isolation, and peer auth walks around it); `ProtectSystem=full`, or `strict` with the working directory writable (makes the code the service executes writable by the service, in a change whose subject is containment); keeping `User=lars` and isolating with a second mechanism (the shared uid IS the `/proc` vector); storing the password in `provisioning_instances` (a live credential in plaintext, to buy stability the rotation does not need).](0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md) | tenant isolation / provisioning / DB auth |