@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
@@ -21,6 +21,12 @@
21
21
  /* Row actions inside a ledger cell: ghost pills in a row. */
22
22
  .watch-actions { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
23
23
 
24
+ /* The reader's own roster row (task 1003720). Nobody may change their own rank
25
+ or standing — the server refuses both outright — so that row states a fact
26
+ where the others carry a control, and a fact is written in the faint ink, not
27
+ painted as a disabled pill. The foot line under the table says why. */
28
+ .watch-self { color: var(--ink-faint); }
29
+
24
30
  /* A sealed or failed section says so in place. Three states, three sentences:
25
31
  "nothing here" is the kit's empty state, this is "you may not see this" and
26
32
  "could not load". */
@@ -745,6 +745,13 @@
745
745
  // sits at a rank outside this set, we inject it so the select still shows it.
746
746
  const RANK_OPTIONS = ['xenos', 'thetes', 'metic', 'archon'];
747
747
 
748
+ // Who is reading this roster (task 1003720). `load()` already fetches /me to
749
+ // resolve the page's floor and then threw the id away, so the roster — which
750
+ // returns EVERY builder, the viewer included — could not tell which row was
751
+ // the reader's own. See renderRoster for why that mattered.
752
+ let viewerId = null;
753
+ const isViewer = (b) => viewerId != null && b && b.id != null && String(b.id) === String(viewerId);
754
+
748
755
  function renderRoster(data, target) {
749
756
  const builders = Array.isArray(data.builders) ? data.builders : [];
750
757
  setAttention('att-stale', builders.filter((b) => b.stale_claim).length);
@@ -762,16 +769,39 @@
762
769
  ],
763
770
  pager: makePager('watch-roster', 'otb.watch.rosterPerPage'),
764
771
  rowHtml: (b) => {
772
+ // THE VIEWER'S OWN ROW OFFERS NOTHING (task 1003720, re-filed from 1002951
773
+ // — "I cannot press offboard", reported by the owner from Discord).
774
+ //
775
+ // The roster returns every builder, so the reader's own row is in it. Both
776
+ // routes behind that row refuse a self-change OUTRIGHT and always will:
777
+ // PATCH /builders/:id/rank answers cannot_change_own_rank and
778
+ // /builders/:id/status answers cannot_change_own_status — the guard that
779
+ // stops the last Archon demoting or offboarding themselves and locking
780
+ // everyone out. The row painted a live rank <select> and a live Offboard
781
+ // pill anyway, so from the owner's seat the page carried a control that
782
+ // could only ever answer with a raw refusal slug in a toast. That is what
783
+ // "I cannot press offboard" describes.
784
+ //
785
+ // This is not a SOFT gate (the primer's acknowledge control, which opens
786
+ // once you have read enough and so stays in the tab order as
787
+ // aria-disabled) — it is an action that can never open for this reader.
788
+ // So the row renders no control at all and the section's foot line says
789
+ // why, rather than painting a dead one.
790
+ const self = isViewer(b);
765
791
  const opts = RANK_OPTIONS.slice();
766
792
  if (b.rank && !opts.includes(b.rank)) opts.unshift(b.rank);
767
- const rankSelect =
768
- `<select data-roster-rank data-id="${escapeHtml(b.id)}" data-current="${escapeHtml(b.rank || '')}" aria-label="Rank">` +
769
- opts.map((rk) => `<option value="${escapeHtml(rk)}"${rk === b.rank ? ' selected' : ''}>${escapeHtml(rankLabel(rk))}</option>`).join('') +
770
- '</select>';
793
+ const rankCell = self
794
+ ? `<span class="watch-self">${escapeHtml(rankLabel(b.rank))}</span>`
795
+ : `<select data-roster-rank data-id="${escapeHtml(b.id)}" data-current="${escapeHtml(b.rank || '')}" aria-label="Rank">` +
796
+ opts.map((rk) => `<option value="${escapeHtml(rk)}"${rk === b.rank ? ' selected' : ''}>${escapeHtml(rankLabel(rk))}</option>`).join('') +
797
+ '</select>';
771
798
  const isInactive = String(b.status) === 'inactive';
772
799
  const statusBtn = isInactive
773
800
  ? ghost(`data-roster-status="active" data-id="${escapeHtml(b.id)}"`, 'Reactivate')
774
801
  : ghost(`data-roster-status="inactive" data-id="${escapeHtml(b.id)}"`, 'Offboard', true);
802
+ const actionCell = self
803
+ ? '<span class="watch-self">That\'s you</span>'
804
+ : `<div class="watch-actions">${statusBtn}</div>`;
775
805
  const claims = num(b.active_claim_count);
776
806
  const staleFlag = b.stale_claim ? ' <span class="badge badge--sev-medium">stale</span>' : '';
777
807
  // task 1467: the roster name links to the builder's public profile page.
@@ -784,17 +814,28 @@
784
814
  `<td>${rosterNameCell}` +
785
815
  (b.github_login ? ` <span class="ov-mono">@${escapeHtml(b.github_login)}</span>` : '') +
786
816
  '</td>' +
787
- `<td>${rankSelect}</td>` +
817
+ `<td>${rankCell}</td>` +
788
818
  `<td class="num">${fmtInt(b.total_credits)}</td>` +
789
819
  `<td class="num">${fmtInt(b.karma)}</td>` +
790
820
  `<td class="num${b.stale_claim ? ' warn' : ''}">${fmtInt(claims)}${staleFlag}</td>` +
791
821
  `<td><span class="badge badge--status">${escapeHtml(b.status || '—')}</span></td>` +
792
- `<td><div class="watch-actions">${statusBtn}</div></td>` +
822
+ `<td>${actionCell}</td>` +
793
823
  '</tr>'
794
824
  );
795
825
  },
796
826
  }).render(builders);
797
827
 
828
+ // The one asymmetry in the table, stated once under it rather than repeated
829
+ // on the row. Appended as an ELEMENT, not with `innerHTML +=`: re-parsing the
830
+ // body would drop the pager's own wiring. The rank is written from
831
+ // rankLabel(), never a hardcoded "Archon", so the floor cannot drift.
832
+ if (builders.some(isViewer)) {
833
+ const note = document.createElement('p');
834
+ note.className = 'ov-foot';
835
+ note.textContent = `Your own row carries no controls. Nobody can change their own rank or standing — that guard is what stops the last ${rankLabel('archon')} locking everyone out — so another ${rankLabel('archon')} does it.`;
836
+ target.appendChild(note);
837
+ }
838
+
798
839
  // Rank change via the <select>. Bespoke: a 'change' listener that rolls the
799
840
  // select back to its previous value on failure, delegated so a page flip
800
841
  // cannot strand a row without one.
@@ -1305,6 +1346,11 @@
1305
1346
  if (!res.ok) throw new Error(`me → ${res.status}`);
1306
1347
  const payload = res.data;
1307
1348
  builder = (payload && payload.builder) ? payload.builder : payload;
1349
+ // Kept for the roster, which must know which row is the reader's own
1350
+ // (task 1003720). Null when /me answered without one — isViewer then
1351
+ // matches nothing, so the roster degrades to the old every-row-acts shape
1352
+ // rather than marking the wrong row as yours.
1353
+ viewerId = (builder && builder.id != null) ? builder.id : null;
1308
1354
  } catch (err) {
1309
1355
  console.error('[watch] failed to read viewer', err);
1310
1356
  $('#watch-sub').textContent = 'Could not verify your access. Go back and try again.';
@@ -73,9 +73,13 @@
73
73
  // 'engineer'). This is the whole point of the mechanism: the
74
74
  // discipline of the generated task is not the discipline of the
75
75
  // ship that caused it.
76
- // when(task) a predicate over the shipped task row. Must be pure and total —
77
- // a throw is caught and treated as "no match", because a broken
78
- // predicate must not stop the other rules.
76
+ // when(task, ctx) a predicate over the shipped task row and the run CONTEXT.
77
+ // Must be pure and total — a throw is caught and treated as "no
78
+ // match", because a broken predicate must not stop the other
79
+ // rules. `ctx` is the per-run policy the runner resolved (today:
80
+ // `{ artistGate }`), injected rather than read, so this file keeps
81
+ // its "importable with nothing behind it" property. A rule that
82
+ // does not care about policy simply ignores the second argument.
79
83
  // title(task) / body(task) the generated task's title and description.
80
84
  // kind optional task kind; must be one the classifier knows
81
85
  // (task-classifier.VALID_KINDS). Defaults to 'unclassified' —
@@ -151,7 +155,14 @@ const CASCADES = Object.freeze([
151
155
  kind: 'unclassified',
152
156
  // The anti-recursion guard (source !== CASCADE_SOURCE) is FIRST so a
153
157
  // cascade-generated task is rejected before any other test runs.
154
- when: (t) => t.source !== CASCADE_SOURCE && hasCopyOrVisualWork(t),
158
+ //
159
+ // The project's artist_gate at 'off' means "not generated" (ADR 0241 §2's
160
+ // table, task 1003575) — the one level at which this rule files nothing. It
161
+ // is read from ctx, never from branding, so the predicate stays pure; an
162
+ // absent ctx (a caller that resolved no policy) files, because the gate's
163
+ // default is 'strict' and silence must not be read as an opt-out.
164
+ when: (t, ctx) => (ctx || {}).artistGate !== 'off'
165
+ && t.source !== CASCADE_SOURCE && hasCopyOrVisualWork(t),
155
166
  title: (t) => `Artist review: the copy and visuals that shipped in task ${t.id}`,
156
167
  body: artistReviewBody,
157
168
  }),
@@ -201,13 +212,13 @@ function validateCascades(rules = CASCADES) {
201
212
  // ---------------------------------------------------------------------------
202
213
 
203
214
  // The task rows a shipped task cascades into. Pure: no database, no clock, no
204
- // side effect — hand it a task-shaped object and a rule table and it returns
205
- // the specs that would be filed.
215
+ // side effect — hand it a task-shaped object, a rule table and the run's policy
216
+ // context and it returns the specs that would be filed.
206
217
  //
207
218
  // A rule whose `when` THROWS is treated as no-match and reported in `errors`,
208
219
  // never propagated: one broken predicate must not cost the other rules their
209
220
  // tasks, and it certainly must not reach the ship.
210
- function cascadesFor(task, rules = CASCADES) {
221
+ function cascadesFor(task, rules = CASCADES, ctx = {}) {
211
222
  const matched = [];
212
223
  const errors = [];
213
224
  if (!task || task.id == null) return { matched, errors: ['no task'] };
@@ -216,7 +227,7 @@ function cascadesFor(task, rules = CASCADES) {
216
227
  if (shape.length) { errors.push(`cascade "${rule && rule.key}": ${shape.join('; ')}`); continue; }
217
228
  let hit = false;
218
229
  try {
219
- hit = rule.when(task) === true;
230
+ hit = rule.when(task, ctx) === true;
220
231
  } catch (e) {
221
232
  errors.push(`cascade "${rule.key}": when() threw (${e && e.message}) — treated as no match`);
222
233
  continue;
@@ -281,6 +292,14 @@ function cascadeTaskSpec(rule, task) {
281
292
  // goal_id / version_id / discipline)
282
293
  // deps.findTaskBySourceRef({...}) -> an existing row or null
283
294
  // deps.createTask(spec) -> the created row
295
+ // deps.artistGateMode() -> 'off' | 'advisory' | 'strict' — the
296
+ // project's artist gate (ADR 0241). Injected
297
+ // so this file reads no branding pack; a
298
+ // reader that throws leaves the context key
299
+ // undefined, which FILES (the gate's own
300
+ // default is 'strict', and a policy read
301
+ // that failed must not silently behave like
302
+ // 'off').
284
303
  // deps.log -> optional { info, error }
285
304
  async function runCascadesForShip(shipped, deps = {}) {
286
305
  const report = { created: [], skipped: [], errors: [] };
@@ -292,7 +311,16 @@ async function runCascadesForShip(shipped, deps = {}) {
292
311
  const task = deps.getTask ? await deps.getTask(id) : null;
293
312
  if (!task) { report.errors.push(`cascade: task ${id} not readable after ship`); return report; }
294
313
 
295
- const { matched, errors } = cascadesFor(task, deps.rules || CASCADES);
314
+ // The per-run policy context. Resolved once, outside the rule loop, and
315
+ // never allowed to cost the run: a reader that throws is reported and leaves
316
+ // the key undefined rather than suppressing every rule that consults it.
317
+ const ctx = {};
318
+ if (typeof deps.artistGateMode === 'function') {
319
+ try { ctx.artistGate = deps.artistGateMode(); }
320
+ catch (e) { report.errors.push(`cascade: artistGateMode() threw (${e && e.message}) — policy unread`); }
321
+ }
322
+
323
+ const { matched, errors } = cascadesFor(task, deps.rules || CASCADES, ctx);
296
324
  report.errors.push(...errors);
297
325
 
298
326
  for (const { rule, spec } of matched) {
@@ -94,8 +94,11 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
94
94
 
95
95
  // ADR 0120 part 3: the needs_rebase claim gate. A builder who owes a rebase
96
96
  // on any un-landed confirmed task (flagged by the reconciler, part 2) is
97
- // blocked from taking on NEW work until they LAND it (rebase the branch +
98
- // re-ship reconciler) or RELEASE it both clear the flag. Their
97
+ // blocked from taking on NEW work until they LAND it (`git merge origin/main`
98
+ // + re-ship task 1002788: MERGE, never rebase, because the branch is already
99
+ // published and server-mediated publish is append-only, so rewriting it makes
100
+ // the task permanently un-landable while the gate keeps their queue shut)
101
+ // or RELEASE it — both clear the flag. Their
99
102
  // already-active claims are untouched (we only refuse fresh claims, never
100
103
  // freeze in-flight work). Since the reward now lands only on ship (part 1),
101
104
  // the builder is motivated to clear it; since it gates their queue, they
@@ -527,9 +527,10 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
527
527
  // precedent). membershipKind defaults to 'member'.
528
528
 
529
529
  // THE COLUMN IS `membership_kind` (task 1003013, ADR 0177 — ADR 0174 reserves the
530
- // other word for the three crafts). The legacy column still EXISTS for one
531
- // release, kept in lockstep by core_220's trigger so a rolled-back deploy still
532
- // reads a schema it understands: write the new name only, step 2 drops the old.
530
+ // other word for the three crafts). The rename is COMPLETE: core_240 (task
531
+ // 1003089) dropped the legacy column and its lockstep trigger, and the new column
532
+ // now carries NOT NULL + DEFAULT 'member' itself rather than resting on that
533
+ // trigger. There is no second spelling left to keep in step.
533
534
  //
534
535
  // ARRIVING AS A MEMBER CLOSES ANY PENDING ASK (task 1002888 / idea 1000688).
535
536
  // The membership write used to be a bare upsert that never looked at
@@ -19,15 +19,66 @@ const { mostRecentClaimHolderId } = require('./db-shared.js');
19
19
  // believes the grader was wrong and asks an Archon to confirm anyway.
20
20
 
21
21
 
22
+ // THE SECOND THING AN OVERRIDE REQUEST CAN BE (task 1003575, ADR 0241 §3).
23
+ //
24
+ // The original request is a GRADER BYPASS: a `completed` task whose ship the
25
+ // subagent panel rejected, flipped to `confirmed` and paid. ADR 0241 needed an
26
+ // escape hatch for a different refusal — a deploy held by an unresolved artist
27
+ // review — and was explicit that it must be "the mechanism that already exists…
28
+ // no new machinery, no second authority path". So it is the SAME table, the same
29
+ // request flow and the same decide endpoint; only the target class and the effect
30
+ // of an approval differ.
31
+ //
32
+ // A GATE RELEASE differs from a grader bypass in three ways, and each one is the
33
+ // reason it could not simply reuse the existing path unchanged:
34
+ //
35
+ // • Its target is NOT at 'completed'. An artist review sits at `backlog` and
36
+ // usually always will — ADR 0241 guard #4 calls "a strict project with no
37
+ // artist" the normal case for a new instance. The completed-only check below
38
+ // would have made the one documented escape hatch unusable for the exact
39
+ // scenario it was written for.
40
+ // • Approving it CONFIRMS NOTHING AND PAYS NOTHING. A released review is still
41
+ // an open request on the board; what the release buys is that it stops
42
+ // holding the pin. Flipping it to `confirmed` and crediting it would pay for
43
+ // work nobody did and erase the request.
44
+ // • Only the PROJECT OWNER may decide it (§3, owner ruling: not the builder,
45
+ // not any Metic+, not the artist) — a narrower gate than the Archon floor the
46
+ // grader bypass carries, enforced here rather than only at the route so no
47
+ // second caller can widen it.
48
+ //
49
+ // Whether a task is in this class is asked of core's ONE artist-gate reader, so
50
+ // "is this an unresolved artist review" has the same answer here, at the deploy
51
+ // gate and in the cascade that files them.
52
+ function gateReleaseTarget(taskRow) {
53
+ const gate = api.artistGate;
54
+ return gate.isArtistReview(taskRow) && !gate.isReviewResolved(taskRow);
55
+ }
56
+
57
+ // WHO is asked "who owns this project?" — and the answer is the doorway unless we
58
+ // are demonstrably in a test. Same NODE_ENV gate as the `deps.pool` seam below,
59
+ // and for a sharper version of its reason (#538): this reader decides who may
60
+ // release a deploy-blocking review, so a future caller that forwarded an options
61
+ // object carrying a `getFoundingBuilder` key — a batch decide path, a CLI helper,
62
+ // a careless refactor — would otherwise silently swap out the owner check in
63
+ // production. Pure over its inputs, so the gate itself is unit-testable without a
64
+ // database (which the decide path, holding a transaction, is not).
65
+ function resolveOwnerReader(deps = {}, env = process.env) {
66
+ return (typeof deps.getFoundingBuilder === 'function' && env.NODE_ENV === 'test')
67
+ ? deps.getFoundingBuilder
68
+ : api.getFoundingBuilder;
69
+ }
70
+
71
+ const OVERRIDE_TASK_COLUMNS = 'id, status, source, source_ref, merged_into_task_id';
72
+
22
73
  async function createOverrideRequest({ taskId, requesterId, rationale }) {
23
74
  const { rows: taskRows } = await pool.query(
24
- `SELECT id, status FROM tasks WHERE id = $1`,
75
+ `SELECT ${OVERRIDE_TASK_COLUMNS} FROM tasks WHERE id = $1`,
25
76
  [taskId]
26
77
  );
27
78
  if (taskRows.length === 0) {
28
79
  throw Object.assign(new Error('task not found'), { code: 'TASK_NOT_FOUND' });
29
80
  }
30
- if (taskRows[0].status !== 'completed') {
81
+ if (taskRows[0].status !== 'completed' && !gateReleaseTarget(taskRows[0])) {
31
82
  throw Object.assign(new Error('task not in completed state'), {
32
83
  code: 'TASK_NOT_COMPLETED',
33
84
  currentStatus: taskRows[0].status,
@@ -83,6 +134,30 @@ async function decideOverrideRequest({ requestId, archonId, decision, note }, de
83
134
  // (same pattern as credit_grant.mjs). Production callers omit it → module pool.
84
135
  const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
85
136
  return withTx(async (client) => {
137
+ // Which KIND of override this is — decided from the target task, before the
138
+ // request row is touched, because the two classes differ in who may decide
139
+ // them and in what an approval does. See gateReleaseTarget above.
140
+ const { rows: targetRows } = await client.query(
141
+ `SELECT ${OVERRIDE_TASK_COLUMNS} FROM tasks
142
+ WHERE id = (SELECT task_id FROM task_override_requests WHERE id = $1)`,
143
+ [requestId]
144
+ );
145
+ const isGateRelease = targetRows.length > 0 && gateReleaseTarget(targetRows[0]);
146
+
147
+ // THE NARROWER GATE (ADR 0241 §3). The grader bypass admits any Archon; a
148
+ // gate release admits the PROJECT OWNER and nobody else — "not the builder,
149
+ // not any Metic+, and not the artist" is the owner's own wording, and the
150
+ // reason it is here rather than only at the route is that a second caller
151
+ // (a CLI, a future surface) would otherwise inherit the wider Archon floor.
152
+ if (isGateRelease) {
153
+ const owner = await resolveOwnerReader(deps)();
154
+ if (!owner || String(owner.id) !== String(archonId)) {
155
+ throw Object.assign(new Error('only the project owner may release an artist gate'), {
156
+ code: 'OVERRIDE_OWNER_ONLY',
157
+ });
158
+ }
159
+ }
160
+
86
161
  // RETURNING requested_by_builder_id so the approve path doesn't re-read the
87
162
  // row it just locked (#538): the work was the ORIGINAL REQUESTER's, and both
88
163
  // the credit AND the ship attribution must go to them.
@@ -102,7 +177,41 @@ async function decideOverrideRequest({ requestId, archonId, decision, note }, de
102
177
  const row = rows[0];
103
178
 
104
179
  let confirmResult = null;
105
- if (decision === 'approve') {
180
+ let gateRelease = null;
181
+ if (decision === 'approve' && isGateRelease) {
182
+ // A RELEASE CONFIRMS NOTHING AND PAYS NOTHING. The approved request row IS
183
+ // the release — the deploy gate reads it and stops holding — and the review
184
+ // stays an ordinary open request on the board, because the work it asks for
185
+ // has not been done. Anything else here would pay for work nobody did.
186
+ //
187
+ // And it is NEVER QUIET (ADR 0241 §3). A self-release — the owner releasing
188
+ // a review filed against their own work, which on a solo instance is the
189
+ // normal case and is deliberately PERMITTED rather than impossible — is
190
+ // recorded as such, so the ledger shows who waved what through and why.
191
+ // The rationale already lives on the request row; this names the act.
192
+ const selfOverride = String(row.requested_by_builder_id) === String(archonId);
193
+ gateRelease = { taskId: String(row.task_id), selfOverride };
194
+ const writeAudit = deps.insertAuditLog || api.insertAuditLog;
195
+ // Best-effort: the release is durable in the request row, and losing the
196
+ // audit line must not roll back a decision the owner made. A failure here
197
+ // is loud in the logs rather than silent.
198
+ try {
199
+ await writeAudit({
200
+ builderId: archonId,
201
+ route: '/api/bongos/override-requests/:id/decide',
202
+ method: 'POST',
203
+ requestBodyRedacted: {
204
+ override_kind: 'artist_gate_release',
205
+ self_override: selfOverride,
206
+ task_id: String(row.task_id),
207
+ request_id: String(row.id),
208
+ },
209
+ responseStatus: 200,
210
+ });
211
+ } catch (e) {
212
+ api.logger('lifecycle').error(`[override] artist-gate release ${row.id}: audit write failed — ${e && e.message}`);
213
+ }
214
+ } else if (decision === 'approve') {
106
215
  // BOTH the credit AND the ship attribution (shipped_by, which drives the
107
216
  // builders' hall + peer-vote feed) go to the builder who actually DID the
108
217
  // work — the task's most-recent claim holder — never the approving Archon.
@@ -162,7 +271,7 @@ async function decideOverrideRequest({ requestId, archonId, decision, note }, de
162
271
  }
163
272
  }
164
273
 
165
- return { request: row, confirmResult };
274
+ return { request: row, confirmResult, gateRelease };
166
275
  }, { pool: activePool });
167
276
  }
168
277
 
@@ -178,4 +287,8 @@ module.exports = {
178
287
  createOverrideRequest,
179
288
  decideOverrideRequest,
180
289
  listOverrideRequests,
290
+ // pure — exported so the release class and its authority seam can be asserted
291
+ // without a database (the decide path itself holds a transaction and cannot be)
292
+ gateReleaseTarget,
293
+ resolveOwnerReader,
181
294
  };
@@ -1037,6 +1037,68 @@ async function updateTaskStatus(id, status) {
1037
1037
  await pool.query(`UPDATE tasks SET status = $2 WHERE id = $1`, [id, status]);
1038
1038
  }
1039
1039
 
1040
+ // The cascade-filed ARTIST REVIEW rows that are still open (ADR 0241, task
1041
+ // 1003575) — the state the deploy gate reads. Narrow projection on purpose: the
1042
+ // caller needs an id and a filing date to name what holds a deploy and to apply
1043
+ // the non-retroactivity cutoff, and nothing else belongs on that wire. The TITLE
1044
+ // is deliberately absent: it is builder-written text, nothing consumes it, and a
1045
+ // value that is safe as JSON today is an injection vector the first time a
1046
+ // surface renders it as HTML.
1047
+ //
1048
+ // THIS IS A PRE-FILTER, NOT THE RULE. The SQL narrows to "cascade artist reviews
1049
+ // that are not obviously finished"; the authoritative verdict — including ADR
1050
+ // 0241 §5's non-retroactivity cutoff — is src/bongos/artist-gate.js's pure
1051
+ // `unresolvedArtistReviews`, so the definition of "unresolved" lives in exactly
1052
+ // one place and a change to it cannot land in the JS and miss the SQL. The
1053
+ // pre-filter is deliberately the LOOSER of the two: a row it lets through is
1054
+ // re-judged, a row it drops is never seen again.
1055
+ //
1056
+ // The (source, source_ref) shape is the cascade's own (modules/lifecycle/
1057
+ // cascade.js `cascadeSourceRef`) and is passed IN rather than spelled here, so
1058
+ // this query cannot drift from the rule that files the rows.
1059
+ //
1060
+ // AN APPROVED OVERRIDE REQUEST IS A RELEASE (ADR 0241 §3, db-overrides.js): the
1061
+ // project owner has waved this review past, with a rationale and an audit line,
1062
+ // so it no longer holds a deploy. It is still an open request on the board — the
1063
+ // release buys the pin, not the work — which is why this is excluded HERE rather
1064
+ // than by changing the task's status.
1065
+ //
1066
+ // Indexed by `idx_tasks_source_ref` (core_240): without it this is a
1067
+ // sequential scan of `tasks` on a route any builder can call and every deploy
1068
+ // does call.
1069
+ //
1070
+ // THE CAP CANNOT WEAKEN THE GATE, and it is worth saying why rather than leaving
1071
+ // a reader to work it out. Every row this returns is a HOLDER — resolved and
1072
+ // released reviews are excluded in SQL — so a project with more open reviews than
1073
+ // the cap still gets a held verdict from the first one. What truncation can
1074
+ // affect is the COUNT in the refusal, so the caller is told when it happened
1075
+ // (`truncated`) and says "200+" rather than a number it cannot stand behind.
1076
+ // Oldest-first is the right order for the same reason: the cutoff is a date, so
1077
+ // the rows nearest the gate's own boundary are the ones kept.
1078
+ const ARTIST_REVIEW_SCAN_LIMIT = 200;
1079
+
1080
+ async function listOpenArtistReviews({ source, refPrefix, resolvedStatuses = [], limit = ARTIST_REVIEW_SCAN_LIMIT } = {}) {
1081
+ if (!source || !refPrefix) return { reviews: [], truncated: false };
1082
+ const cap = Math.max(1, Math.min(1000, Number(limit) || ARTIST_REVIEW_SCAN_LIMIT));
1083
+ const { rows } = await pool.query(
1084
+ `SELECT id, status, source, source_ref, created_at, merged_into_task_id
1085
+ FROM tasks t
1086
+ WHERE t.source = $1
1087
+ AND t.source_ref LIKE $2
1088
+ AND t.merged_into_task_id IS NULL
1089
+ AND NOT (t.status = ANY($3::text[]))
1090
+ AND NOT EXISTS (
1091
+ SELECT 1 FROM task_override_requests r
1092
+ WHERE r.task_id = t.id AND r.status = 'approved')
1093
+ ORDER BY t.created_at ASC
1094
+ LIMIT $4`,
1095
+ // cap + 1: one row past the cap is how "there are more" is learned without a
1096
+ // second COUNT query that could disagree with the page it describes.
1097
+ [source, `${refPrefix}%`, resolvedStatuses, cap + 1]
1098
+ );
1099
+ return { reviews: rows.slice(0, cap), truncated: rows.length > cap };
1100
+ }
1101
+
1040
1102
  module.exports = {
1041
1103
  CREDITS_AWARDED_SUBQUERY,
1042
1104
  REWARD_SUGGESTION_CAP,
@@ -1047,6 +1109,7 @@ module.exports = {
1047
1109
  getTask,
1048
1110
  listActiveClaims,
1049
1111
  listClaimableTasks,
1112
+ listOpenArtistReviews,
1050
1113
  listTasks,
1051
1114
  rankCrossDisciplineRecommendations,
1052
1115
  rewardGateAssignment,
@@ -1,7 +1,7 @@
1
1
  -- lifecycle_001_goal_first_leads.sql — backfill goal OWNERS into the member list
2
2
  -- (task 1735). The goal authority model: the OWNER is goals.created_by (fixed at
3
- -- create, moved only by POST /goals/:id/transfer); goal_members.role 'lead' is
4
- -- the MANAGER tier the hall renders. From now on db.createGoal inserts the
3
+ -- create, moved only by POST /goals/:id/transfer); goal_members.membership_kind
4
+ -- 'lead' is the MANAGER tier the hall renders. From now on db.createGoal inserts the
5
5
  -- creator's lead row atomically; this brings the goals created before that rule
6
6
  -- up to the same shape so owners appear in their own member lists.
7
7
  --
@@ -13,14 +13,22 @@
13
13
  -- Data-only (no schema change), idempotent (the upsert converges), and
14
14
  -- replay-safe on a fresh DB (zero goals → zero rows; no builder id is
15
15
  -- hard-coded — learning 1238).
16
+ --
17
+ -- THE COLUMN IS `membership_kind` (task 1003089, ADR 0177). This file wrote the
18
+ -- legacy `role` until core_240 dropped it, and it is the reason that DROP was not
19
+ -- a pure deletion: module migrations run AFTER the core ones on every fresh
20
+ -- install, so a stale name here would have broken `cloudbongos init` for new
21
+ -- instances while staying invisible on existing ones (already recorded in
22
+ -- `schema_migrations`, so never re-run). Zero-row on a fresh DB, but a missing
23
+ -- column is a plan-time error, not a row-time one.
16
24
 
17
25
  BEGIN;
18
26
 
19
- INSERT INTO goal_members (goal_id, builder_id, role)
27
+ INSERT INTO goal_members (goal_id, builder_id, membership_kind)
20
28
  SELECT g.id, g.created_by, 'lead'
21
29
  FROM goals g
22
30
  WHERE g.created_by IS NOT NULL
23
- ON CONFLICT (goal_id, builder_id) DO UPDATE SET role = 'lead';
31
+ ON CONFLICT (goal_id, builder_id) DO UPDATE SET membership_kind = 'lead';
24
32
 
25
33
  INSERT INTO schema_migrations (version) VALUES ('lifecycle_001_goal_first_leads')
26
34
  ON CONFLICT (version) DO NOTHING;
@@ -25,7 +25,8 @@
25
25
  "visuals",
26
26
  "kickoff-seed",
27
27
  "rot",
28
- "project-settings"
28
+ "project-settings",
29
+ "artist-gate"
29
30
  ],
30
31
  "pollers": [
31
32
  "publish-reconciler"
@@ -0,0 +1,49 @@
1
+ // modules/lifecycle/pg-constraint-fail.js — turn a Postgres CHECK violation into a
2
+ // 400 that names the constraint, instead of an opaque 500 (task 1002845).
3
+ //
4
+ // WHY. A range the route validator accepts but the DB CHECK rejects is a
5
+ // self-inflicted wound: the INSERT raises SQLSTATE 23514, the generic catch logs
6
+ // it server-side and answers `create_failed` / "internal error", and the caller —
7
+ // an agent, usually — has no idea which field was wrong. Idea 1000456 records
8
+ // roughly four blind retries spent on exactly that during task 1604, and task
9
+ // 1002845 measured it still happening: POST /tasks {manual_degree:0} → 500
10
+ // "internal error".
11
+ //
12
+ // The first fix is always to make the validator match the DB so the INSERT never
13
+ // sees a bad value (task 1233 did that for `priority`; task 1002845 for
14
+ // `manual_degree`). This is the BACKSTOP for the next mismatch nobody has found
15
+ // yet — it cannot know which field is at fault, but the constraint name says so.
16
+ //
17
+ // WHAT IS SAFE TO ECHO. `err.constraint` only — a schema identifier the operator
18
+ // wrote, carrying no request or row data. NEVER `err.detail`: Postgres puts the
19
+ // offending ROW VALUES in it ("Failing row contains (1, secret, ...)"), so echoing
20
+ // it would leak other columns of the caller's own record into an error body, and
21
+ // on a shared instance potentially values the caller never sent.
22
+
23
+ 'use strict';
24
+
25
+ // Postgres SQLSTATE for a CHECK constraint violation.
26
+ const CHECK_VIOLATION = '23514';
27
+
28
+ // describeCheckViolation(err) → { code, status, message, details } | null
29
+ //
30
+ // Pure. Returns null for anything that is not a CHECK violation, so a caller can
31
+ // use it as a guard and fall through to its existing generic handling.
32
+ function describeCheckViolation(err) {
33
+ if (!err || err.code !== CHECK_VIOLATION) return null;
34
+ const constraint = typeof err.constraint === 'string' && err.constraint.trim()
35
+ ? err.constraint.trim()
36
+ : null;
37
+ return {
38
+ code: 'constraint_violation',
39
+ status: 400,
40
+ message: constraint
41
+ // Deliberately does not guess WHICH field: one CHECK can span several
42
+ // columns, and naming the wrong one is worse than naming none.
43
+ ? `a value in this request violates the database constraint "${constraint}" — check the field it guards against the accepted range`
44
+ : 'a value in this request violates a database constraint',
45
+ details: { constraint },
46
+ };
47
+ }
48
+
49
+ module.exports = { describeCheckViolation, CHECK_VIOLATION };