@bongos/core 1.19.715 → 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 (414) hide show
  1. package/.bongos-core.json +472 -407
  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/design/hall-direction-v2.md +3 -3
  291. package/docs/design/landing-page-direction-v2.md +1 -1
  292. package/docs/design/projects-hub-direction-v2.md +1 -1
  293. package/docs/file-map.md +3 -1
  294. package/docs/module-api-changelog.md +34 -1
  295. package/docs/modules-contract.md +25 -25
  296. package/docs/onboarding/browser-terminal-guide.md +1 -1
  297. package/docs/onboarding/diagrams/README.md +3 -3
  298. package/docs/onboarding/drachmae-and-karma.md +13 -13
  299. package/docs/onboarding/primer.md +7 -7
  300. package/docs/packs/artist.md +1 -1
  301. package/docs/packs/ideator.md +1 -1
  302. package/docs/project-context.template.md +3 -3
  303. package/docs/recipes/builder-box-lifecycle.md +5 -5
  304. package/docs/recipes/claude-design-loop.md +1 -1
  305. package/docs/recipes/core-release-pipeline.md +55 -14
  306. package/docs/recipes/figma-design-loop.md +1 -1
  307. package/docs/recipes/gds-db-backup.md +2 -2
  308. package/docs/recipes/instance-deploy.md +2 -2
  309. package/docs/recipes/instance-service-restart.md +3 -3
  310. package/docs/recipes/local-dev.md +2 -2
  311. package/docs/recipes/managed-settings-remote-control.md +3 -3
  312. package/docs/recipes/memory-backup.md +4 -4
  313. package/docs/recipes/memory-sync-conflicts.md +2 -2
  314. package/docs/recipes/multi-builder-merge.md +2 -2
  315. package/docs/recipes/ops-gotchas.md +4 -4
  316. package/docs/recipes/overnight-builder.md +1 -1
  317. package/docs/recipes/packaging-the-core.md +6 -6
  318. package/docs/recipes/private-npm-distribution.md +5 -5
  319. package/docs/recipes/search-index-ingestion.md +3 -3
  320. package/docs/recipes/self-host.md +5 -5
  321. package/docs/recipes/server-bring-up.md +1 -1
  322. package/docs/recipes/ship-and-regrade.md +2 -2
  323. package/docs/recipes/standalone-live-docs.md +1 -1
  324. package/docs/recipes/ui-look-before-you-ship.md +1 -1
  325. package/docs/recipes/upgrading-the-core.md +2 -2
  326. package/docs/recipes/work-on-a-task-from-your-dev-box.md +6 -6
  327. package/docs/research/cursor-competitive-analysis.md +3 -3
  328. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +7 -7
  329. package/migrations/core_240_artist_gate_indexes.sql +48 -0
  330. package/migrations/core_240_goal_members_drop_legacy_role.sql +79 -0
  331. package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
  332. package/modules/lifecycle/cascade.js +37 -9
  333. package/modules/lifecycle/db-claims.js +5 -2
  334. package/modules/lifecycle/db-goals.js +4 -3
  335. package/modules/lifecycle/db-overrides.js +117 -4
  336. package/modules/lifecycle/db-tasks.js +63 -0
  337. package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +12 -4
  338. package/modules/lifecycle/module.json +2 -1
  339. package/modules/lifecycle/pg-constraint-fail.js +49 -0
  340. package/modules/lifecycle/routes/artist-gate.js +101 -0
  341. package/modules/lifecycle/routes/claims.js +13 -1
  342. package/modules/lifecycle/routes/lifecycle.js +6 -0
  343. package/modules/lifecycle/routes/task-write-routes.js +7 -0
  344. package/modules/lifecycle/routes/tasks.js +24 -0
  345. package/modules/lifecycle/task-classifier.js +10 -1
  346. package/modules/lifecycle/tests/cascade.mjs +43 -0
  347. package/modules/provisioning/provisioning.js +79 -3
  348. package/modules/provisioning/routes/provisioning.js +6 -1
  349. package/modules/provisioning/tests/provisioning.mjs +8 -8
  350. package/modules/security/secret-scrub.js +40 -4
  351. package/modules/status-ui/public/README.md +1 -1
  352. package/modules/ui-design/docs/design-contract.md +2 -2
  353. package/modules/ui-design/kit/README.md +1 -1
  354. package/modules/ui-design/kit/serve.js +1 -0
  355. package/modules/ui-design/skills/README.md +6 -6
  356. package/modules/ui-design/skills/brandkit/SKILL.md +1 -1
  357. package/modules/ui-design/skills/design-taste-frontend/SKILL.md +1 -1
  358. package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +1 -1
  359. package/modules/ui-design/skills/gpt-taste/SKILL.md +1 -1
  360. package/modules/ui-design/skills/high-end-visual-design/SKILL.md +1 -1
  361. package/modules/ui-design/skills/image-to-code/SKILL.md +1 -1
  362. package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +1 -1
  363. package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +1 -1
  364. package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
  365. package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +1 -1
  366. package/modules/ui-design/skills/minimalist-ui/SKILL.md +1 -1
  367. package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +1 -1
  368. package/modules/ui-design/skills/stitch-design-taste/SKILL.md +1 -1
  369. package/modules/ui-design/skills/style/SKILL.md +2 -2
  370. package/modules/ui-design/styles/README.md +3 -3
  371. package/modules/ui-design/styles/blueprint/DESIGN.md +1 -1
  372. package/modules/ui-design/styles/chrome-world/DESIGN.md +1 -1
  373. package/package-lock.json +2 -2
  374. package/package.json +1 -1
  375. package/scripts/gds/audit-rank-roles.js +172 -0
  376. package/scripts/gds/build-cli-package.js +1 -0
  377. package/scripts/gds/claim.js +94 -9
  378. package/scripts/gds/fitness.js +8 -8
  379. package/scripts/gds/go-live.js +139 -5
  380. package/scripts/gds/land-watch.js +13 -5
  381. package/scripts/gds/mirror-redact.js +13 -2
  382. package/scripts/gds/package-core.js +11 -3
  383. package/scripts/gds/release.js +55 -0
  384. package/scripts/gds/ship-land.js +10 -2
  385. package/scripts/gds/ship-regen.js +41 -8
  386. package/scripts/gds/start.js +7 -4
  387. package/scripts/gds/worktree-claim-guard.js +27 -1
  388. package/scripts/public-mirror-export.js +17 -4
  389. package/src/bongos/api-errors.js +9 -0
  390. package/src/bongos/artist-gate.js +204 -0
  391. package/src/branding.js +15 -0
  392. package/src/module-api.js +34 -1
  393. package/tests/api_path_404.mjs +28 -0
  394. package/tests/artist_gate.mjs +234 -0
  395. package/tests/artist_gate_release.mjs +283 -0
  396. package/tests/artist_gate_route.mjs +174 -0
  397. package/tests/box_scope_predicate.mjs +117 -0
  398. package/tests/claim_error_surface.mjs +163 -1
  399. package/tests/cli_exit_no_abort.mjs +256 -3
  400. package/tests/currency_label.mjs +11 -4
  401. package/tests/go_live.mjs +144 -0
  402. package/tests/main_worktree_busy_reason.mjs +167 -0
  403. package/tests/membership_kind_lockstep.mjs +88 -0
  404. package/tests/mirror_redact.mjs +14 -0
  405. package/tests/module_api.mjs +6 -0
  406. package/tests/provision_settings_apply.mjs +13 -5
  407. package/tests/provisioning_settings.mjs +11 -7
  408. package/tests/provisioning_settings_apply.mjs +4 -4
  409. package/tests/provisioning_settings_env.mjs +59 -7
  410. package/tests/public_mirror_export.mjs +12 -0
  411. package/tests/rank_roles_audit.mjs +114 -0
  412. package/tests/scrubber_corpus.mjs +70 -2
  413. package/tests/start_rebase_warning.mjs +50 -0
  414. package/tests/task_field_bounds_match_db.mjs +140 -0
@@ -0,0 +1,283 @@
1
+ // tests/artist_gate_release.mjs
2
+ //
3
+ // THE ESCAPE HATCH (task 1003575, ADR 0241 §3) — releasing a deploy that an
4
+ // unresolved artist review is holding.
5
+ //
6
+ // ADR 0241 was explicit that this must be the mechanism that already exists —
7
+ // the override request — and NOT a second authority path. So a release is the
8
+ // same table, the same request flow and the same decide endpoint as the grader
9
+ // bypass, and everything worth pinning here is where the two DIVERGE:
10
+ //
11
+ // • a release may be opened on a task that is NOT 'completed'. An artist review
12
+ // sits at `backlog`, and ADR 0241 guard #4 calls a strict project with no
13
+ // artist "the normal case" for a new instance — so a completed-only gate
14
+ // would make the one documented hatch unusable for exactly the case it was
15
+ // written for. (This is what the first cut of this task got wrong.)
16
+ // • only the PROJECT OWNER may decide it — narrower than the grader bypass's
17
+ // Archon floor, and enforced in the data layer so no second caller inherits
18
+ // the wider one.
19
+ // • approving it CONFIRMS NOTHING and PAYS NOTHING. The review stays open on
20
+ // the board; what stops is its hold on the pin.
21
+ // • it is never quiet: a self-release is permitted (a solo project would
22
+ // otherwise deadlock) and recorded as a self-override.
23
+ //
24
+ // DB-free: decideOverrideRequest's #538 `deps.pool` seam drives the txn with a
25
+ // SQL-aware fake, and the module pool is stubbed on the doorway for the reads.
26
+ //
27
+ // Run: node --test tests/artist_gate_release.mjs
28
+
29
+ import { strict as assert } from 'node:assert';
30
+ import { createRequire } from 'node:module';
31
+ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
32
+
33
+ process.env.NODE_ENV = 'test';
34
+
35
+ const require = createRequire(import.meta.url);
36
+
37
+ // The reward port must be registered before decideOverrideRequest is driven
38
+ // (the grader-bypass branch resolves it) — the override_attribution.mjs setup.
39
+ const achievements = require('../modules/economy/achievements.js');
40
+ achievements.evaluateAfterShip = async () => [];
41
+ achievements.evaluateAfterCreditAward = async () => [];
42
+ achievements.evaluateAfterClaim = async () => [];
43
+ achievements.reconcileBuilderAchievements = async () => [];
44
+ const seams = require('../src/module-seams.js');
45
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
46
+
47
+ const api = require('../src/module-api.js');
48
+
49
+ // The module-level pool createOverrideRequest reads through. Stubbed BEFORE the
50
+ // db facade is required, because db-overrides.js destructures it at load.
51
+ let poolRows = [];
52
+ let poolQueries = [];
53
+ api.pool = { query: async (sql, params) => { poolQueries.push({ sql, params }); return { rows: poolRows }; } };
54
+
55
+ const db = require('../modules/lifecycle/db.js');
56
+ const overrides = require('../modules/lifecycle/db-overrides.js');
57
+
58
+ const { test, summary } = makeRunner();
59
+
60
+ const OWNER = 7;
61
+ const ARCHON = 42;
62
+ const ARTIST = 99;
63
+
64
+ const review = (over = {}) => ({
65
+ id: 9001, status: 'backlog', source: 'cascade',
66
+ source_ref: 'cascade-artist-review-on-ship-7001', merged_into_task_id: null, ...over,
67
+ });
68
+ const ordinary = (over = {}) => ({ id: 555, status: 'backlog', source: 'manual', source_ref: null, merged_into_task_id: null, ...over });
69
+
70
+ const flat = (q) => String(q.sql).replace(/\s+/g, ' ').trim();
71
+ const find = (queries, re) => queries.find((q) => re.test(flat(q)));
72
+ const all = (queries, re) => queries.filter((q) => re.test(flat(q)));
73
+
74
+ // =========================================================================
75
+ // Which tasks are in the release class
76
+ // =========================================================================
77
+
78
+ await test('the release class is exactly "an artist review that is still open" — asked of core\'s one reader', () => {
79
+ assert.equal(overrides.gateReleaseTarget(review()), true);
80
+ // Resolved reviews are not held, so there is nothing to release.
81
+ assert.equal(overrides.gateReleaseTarget(review({ status: 'shipped' })), false);
82
+ assert.equal(overrides.gateReleaseTarget(review({ status: 'abandoned' })), false);
83
+ assert.equal(overrides.gateReleaseTarget(review({ merged_into_task_id: 9100 })), false);
84
+ // And an ordinary task never is — a release must not become a second way to
85
+ // wave a graded ship through at any status.
86
+ assert.equal(overrides.gateReleaseTarget(ordinary()), false);
87
+ assert.equal(overrides.gateReleaseTarget(ordinary({ status: 'completed' })), false);
88
+ });
89
+
90
+ // =========================================================================
91
+ // Opening one
92
+ // =========================================================================
93
+
94
+ await test('a request MAY be opened on a backlog artist review — the case the hatch exists for', async () => {
95
+ poolRows = [review()]; poolQueries = [];
96
+ // The INSERT returns the created row; the fake answers every statement with
97
+ // poolRows, which is enough to prove the completed-check did not refuse.
98
+ const row = await db.createOverrideRequest({ taskId: 9001, requesterId: ARTIST, rationale: 'x'.repeat(30) });
99
+ assert.ok(row, 'the request was created rather than refused');
100
+ assert.ok(find(poolQueries, /^INSERT INTO task_override_requests/), 'it reached the insert');
101
+ });
102
+
103
+ await test('an ordinary task at backlog is STILL refused — the class is narrow, not an open door', async () => {
104
+ poolRows = [ordinary()]; poolQueries = [];
105
+ await assert.rejects(
106
+ () => db.createOverrideRequest({ taskId: 555, requesterId: ARTIST, rationale: 'x'.repeat(30) }),
107
+ (e) => e.code === 'TASK_NOT_COMPLETED' && e.currentStatus === 'backlog'
108
+ );
109
+ assert.equal(all(poolQueries, /^INSERT INTO task_override_requests/).length, 0, 'nothing was written');
110
+ });
111
+
112
+ await test('a RESOLVED artist review is refused too — a dismissed review holds nothing to release', async () => {
113
+ poolRows = [review({ status: 'shipped' })]; poolQueries = [];
114
+ await assert.rejects(
115
+ () => db.createOverrideRequest({ taskId: 9001, requesterId: ARTIST, rationale: 'x'.repeat(30) }),
116
+ (e) => e.code === 'TASK_NOT_COMPLETED'
117
+ );
118
+ });
119
+
120
+ // =========================================================================
121
+ // Deciding one
122
+ // =========================================================================
123
+
124
+ // The txn fake: answers the target-task SELECT with `target`, the request UPDATE
125
+ // with an approved row, and everything else with nothing.
126
+ function txPool({ target, requesterId = ARTIST, decision = 'approve' }) {
127
+ return makeSqlAwareClient((s) => {
128
+ if (/FROM tasks WHERE id = \(SELECT task_id/.test(s)) return { rows: target ? [target] : [] };
129
+ if (/^UPDATE task_override_requests/.test(s)) {
130
+ return { rows: [{ id: 1, task_id: target ? target.id : 0, status: decision === 'approve' ? 'approved' : 'denied', requested_by_builder_id: requesterId }] };
131
+ }
132
+ if (/^UPDATE tasks/.test(s)) return { rows: [{ id: target.id, title: 't', credits_reward: 10, kind: 'feature' }] };
133
+ return { rows: [] };
134
+ });
135
+ }
136
+
137
+ const owner = async () => ({ id: OWNER });
138
+
139
+ await test('an ARCHON who is not the owner cannot release — and the refusal costs the review nothing', async () => {
140
+ const pool = txPool({ target: review() });
141
+ await assert.rejects(
142
+ () => db.decideOverrideRequest(
143
+ { requestId: 1, archonId: ARCHON, decision: 'approve' },
144
+ { pool, getFoundingBuilder: owner, insertAuditLog: async () => {} }
145
+ ),
146
+ (e) => e.code === 'OVERRIDE_OWNER_ONLY'
147
+ );
148
+ assert.equal(all(pool._client.queries, /^UPDATE task_override_requests/).length, 0, 'the request stays open');
149
+ assert.ok(find(pool._client.queries, /^ROLLBACK/), 'and the txn rolls back');
150
+ });
151
+
152
+ await test('the OWNER releases it — and the release confirms nothing and pays nothing', async () => {
153
+ const audits = [];
154
+ const pool = txPool({ target: review() });
155
+ const out = await db.decideOverrideRequest(
156
+ { requestId: 1, archonId: OWNER, decision: 'approve' },
157
+ { pool, getFoundingBuilder: owner, insertAuditLog: async (a) => { audits.push(a); } }
158
+ );
159
+
160
+ assert.equal(out.request.status, 'approved');
161
+ assert.equal(out.gateRelease.taskId, '9001');
162
+ // The three things a grader bypass does that a release must NOT.
163
+ assert.equal(out.confirmResult, null, 'no confirm result — nothing was confirmed');
164
+ assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0, 'the review is NOT flipped to confirmed');
165
+ assert.equal(all(pool._client.queries, /INSERT INTO credit_log/).length, 0, 'and nothing is paid for work nobody did');
166
+ assert.ok(find(pool._client.queries, /^COMMIT/), 'the release commits');
167
+ });
168
+
169
+ await test('a SELF-release is permitted and is recorded as a self-override — never quiet (ADR 0241 §3)', async () => {
170
+ const audits = [];
171
+ const pool = txPool({ target: review(), requesterId: OWNER });
172
+ const out = await db.decideOverrideRequest(
173
+ { requestId: 1, archonId: OWNER, decision: 'approve' },
174
+ { pool, getFoundingBuilder: owner, insertAuditLog: async (a) => { audits.push(a); } }
175
+ );
176
+ // Permitted: a solo instance where the owner is also the builder and the artist
177
+ // would otherwise deadlock, which ADR 0241 §3 rules out explicitly.
178
+ assert.equal(out.gateRelease.selfOverride, true);
179
+ assert.equal(audits.length, 1, 'exactly one audit line for the act');
180
+ assert.equal(audits[0].builderId, OWNER);
181
+ assert.equal(audits[0].requestBodyRedacted.override_kind, 'artist_gate_release');
182
+ assert.equal(audits[0].requestBodyRedacted.self_override, true, 'the ledger says WHO waved through their OWN work');
183
+ assert.equal(audits[0].requestBodyRedacted.task_id, '9001');
184
+ });
185
+
186
+ await test('a release of SOMEONE ELSE\'S review is audited too, and marked as not a self-override', async () => {
187
+ const audits = [];
188
+ const pool = txPool({ target: review(), requesterId: ARTIST });
189
+ const out = await db.decideOverrideRequest(
190
+ { requestId: 1, archonId: OWNER, decision: 'approve' },
191
+ { pool, getFoundingBuilder: owner, insertAuditLog: async (a) => { audits.push(a); } }
192
+ );
193
+ assert.equal(out.gateRelease.selfOverride, false);
194
+ assert.equal(audits.length, 1, 'every release is on the record, self or not');
195
+ });
196
+
197
+ await test('an audit write that FAILS does not roll back a decision the owner made', async () => {
198
+ const pool = txPool({ target: review() });
199
+ const out = await db.decideOverrideRequest(
200
+ { requestId: 1, archonId: OWNER, decision: 'approve' },
201
+ { pool, getFoundingBuilder: owner, insertAuditLog: async () => { throw new Error('audit table down'); } }
202
+ );
203
+ assert.equal(out.request.status, 'approved', 'the release is durable in the request row');
204
+ assert.ok(find(pool._client.queries, /^COMMIT/));
205
+ });
206
+
207
+ await test('DENYING a release changes nothing — the review goes on holding the pin', async () => {
208
+ const pool = txPool({ target: review(), decision: 'deny' });
209
+ const out = await db.decideOverrideRequest(
210
+ { requestId: 1, archonId: OWNER, decision: 'deny' },
211
+ { pool, getFoundingBuilder: owner, insertAuditLog: async () => {} }
212
+ );
213
+ assert.equal(out.request.status, 'denied');
214
+ assert.equal(out.gateRelease, null, 'nothing was released');
215
+ assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0);
216
+ });
217
+
218
+ await test('the GRADER BYPASS is untouched: a completed ordinary task still needs only an Archon', async () => {
219
+ // The owner check must not have leaked onto the path it does not belong to —
220
+ // that would be a silent tightening of an unrelated authority.
221
+ const pool = txPool({ target: ordinary({ status: 'completed' }) });
222
+ const out = await db.decideOverrideRequest(
223
+ { requestId: 1, archonId: ARCHON, decision: 'approve' },
224
+ { pool, getFoundingBuilder: owner, insertAuditLog: async () => {} }
225
+ );
226
+ assert.equal(out.gateRelease, null, 'not a release');
227
+ assert.ok(out.confirmResult, 'the ordinary bypass still confirms');
228
+ assert.ok(find(pool._client.queries, /^UPDATE tasks/), 'and still flips the task');
229
+ });
230
+
231
+ // =========================================================================
232
+ // The gate READS the release
233
+ // =========================================================================
234
+
235
+ await test('an approved override is excluded by the gate\'s own read — a released review stops holding', async () => {
236
+ poolRows = []; poolQueries = [];
237
+ await db.listOpenArtistReviews({ source: 'cascade', refPrefix: 'cascade-artist-review-on-ship-', resolvedStatuses: ['shipped', 'abandoned'] });
238
+ const q = flat(poolQueries[0]);
239
+ // The exclusion has to be IN THE QUERY: the release is not a status change, so
240
+ // nothing downstream would notice it otherwise — which is precisely how the
241
+ // first cut of this task shipped an escape hatch that released nothing.
242
+ assert.match(q, /NOT EXISTS \( SELECT 1 FROM task_override_requests r WHERE r\.task_id = t\.id AND r\.status = 'approved'\)/);
243
+ assert.deepEqual(poolQueries[0].params.slice(0, 3), ['cascade', 'cascade-artist-review-on-ship-%', ['shipped', 'abandoned']]);
244
+ });
245
+
246
+ await test('the scan asks for ONE row past its cap, so "there are more" needs no second COUNT', async () => {
247
+ poolRows = []; poolQueries = [];
248
+ const out = await db.listOpenArtistReviews({ source: 'cascade', refPrefix: 'p-', resolvedStatuses: [], limit: 3 });
249
+ assert.equal(poolQueries[0].params[3], 4, 'cap + 1');
250
+ assert.deepEqual(out, { reviews: [], truncated: false });
251
+
252
+ // With cap+1 rows back, the extra one is dropped and truncation is declared —
253
+ // a COUNT that could disagree with the page it describes is what this avoids.
254
+ poolRows = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]; poolQueries = [];
255
+ const full = await db.listOpenArtistReviews({ source: 'cascade', refPrefix: 'p-', resolvedStatuses: [], limit: 3 });
256
+ assert.equal(full.reviews.length, 3);
257
+ assert.equal(full.truncated, true);
258
+ });
259
+
260
+ await test('the owner-check seam is NODE_ENV-gated like deps.pool — it cannot be swapped in production', () => {
261
+ // The one check deciding who may release a deploy-blocking review must not be
262
+ // overridable by an options object that happens to carry the key: a batch decide
263
+ // path or a careless options-forwarding refactor would otherwise hand an
264
+ // attacker-shaped `{ getFoundingBuilder: () => me }` straight past the gate.
265
+ //
266
+ // Asserted on the RESOLVER rather than through decideOverrideRequest, because
267
+ // the deps.pool seam is gated the same way — so a "production" run of the whole
268
+ // function fails on a real database connection long before it reaches this
269
+ // check, and a test that passed on THAT would be proving nothing.
270
+ const injected = async () => ({ id: ARCHON });
271
+ const doorway = api.getFoundingBuilder;
272
+
273
+ assert.equal(overrides.resolveOwnerReader({ getFoundingBuilder: injected }, { NODE_ENV: 'test' }), injected,
274
+ 'a test keeps its seam');
275
+ for (const NODE_ENV of ['production', 'development', undefined]) {
276
+ assert.equal(overrides.resolveOwnerReader({ getFoundingBuilder: injected }, { NODE_ENV }), doorway,
277
+ `NODE_ENV=${NODE_ENV}: the injected reader is ignored and the doorway decides`);
278
+ }
279
+ // And with nothing injected it is the doorway either way.
280
+ assert.equal(overrides.resolveOwnerReader({}, { NODE_ENV: 'test' }), doorway);
281
+ });
282
+
283
+ summary();
@@ -0,0 +1,174 @@
1
+ // tests/artist_gate_route.mjs
2
+ //
3
+ // GET /artist-gate — the one read the deploy consults (ADR 0241, task 1003575).
4
+ //
5
+ // The pure verdict logic is pinned in tests/artist_gate.mjs; what is worth pinning
6
+ // HERE is the wiring, and specifically the three things a route can get wrong in a
7
+ // way no pure test would see:
8
+ //
9
+ // • 'off' must not even ASK the board. At `off` no reviews are filed, so a query
10
+ // would be a per-deploy read that can only ever return rows the gate ignores.
11
+ // • the DB read must be handed the CASCADE's own (source, ref) spelling, not a
12
+ // second copy of it — a drifted prefix silently matches nothing, and a gate
13
+ // that finds nothing looks exactly like a gate that is satisfied.
14
+ // • `holds` and `halt` must agree. Two fields describing one fact is two chances
15
+ // to disagree, and the caller trusts whichever it read.
16
+ //
17
+ // DB-free: the data-access fn is stubbed on the cached module object (the
18
+ // provisioning_settings.mjs pattern).
19
+ //
20
+ // Run: node --test tests/artist_gate_route.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import { strict as assert } from 'node:assert';
24
+ import { createRequire } from 'node:module';
25
+ import http from 'node:http';
26
+
27
+ const require = createRequire(import.meta.url);
28
+
29
+ const db = require('../modules/lifecycle/db.js');
30
+ const api = require('../src/module-api.js');
31
+ const gateModule = require('../src/bongos/artist-gate.js');
32
+
33
+ let rows = [];
34
+ let scanTruncated = false;
35
+ let queries = [];
36
+ db.listOpenArtistReviews = async (opts) => { queries.push(opts); return { reviews: rows, truncated: scanTruncated }; };
37
+
38
+ let mode = 'strict';
39
+ let since = null;
40
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, rank: 'thetes' }; next(); };
41
+ // The route must resolve the level through the doorway's ONE reader; stubbing it
42
+ // here is also what proves it does not read branding().project behind its back.
43
+ api.artistGate = { ...gateModule, artistGateMode: () => mode, artistGateSince: () => since };
44
+
45
+ const express = require('express');
46
+ const buildRouter = require('../modules/lifecycle/routes/artist-gate.js');
47
+ const app = express();
48
+ app.use((_req, res, next) => {
49
+ res.fail = (code, opts) => res.status((opts && opts.status) || 400).json({ error: { code } });
50
+ next();
51
+ });
52
+ app.use('/api/bongos', buildRouter());
53
+
54
+ const server = app.listen(0);
55
+ await new Promise((r) => server.once('listening', r));
56
+ const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
57
+
58
+ function get(path) {
59
+ return new Promise((resolve, reject) => {
60
+ http.get(`${base}${path}`, (res) => {
61
+ let d = '';
62
+ res.on('data', (c) => { d += c; });
63
+ res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, headers: res.headers, raw: d }); });
64
+ }).on('error', reject);
65
+ });
66
+ }
67
+
68
+ const review = (over = {}) => ({
69
+ id: '9001', title: 'Artist review: the copy and visuals that shipped in task 7001',
70
+ status: 'backlog', source: 'cascade', source_ref: 'cascade-artist-review-on-ship-7001',
71
+ created_at: '2026-09-12T00:00:00.000Z', merged_into_task_id: null, ...over,
72
+ });
73
+
74
+ function reset({ level = 'strict', cutoff = null, open = [], truncated = false } = {}) {
75
+ mode = level; since = cutoff; rows = open; scanTruncated = truncated; queries = [];
76
+ }
77
+
78
+ test("at 'off' the board is never asked — nothing is filed, so there is nothing to read", async () => {
79
+ reset({ level: 'off', open: [review()] });
80
+ const r = await get('/artist-gate');
81
+ assert.equal(r.status, 200, r.raw);
82
+ assert.equal(r.body.mode, 'off');
83
+ assert.equal(r.body.holds, false);
84
+ assert.deepEqual(r.body.reviews, []);
85
+ assert.equal(queries.length, 0, 'a per-deploy query whose rows are ignored is a query that should not run');
86
+ });
87
+
88
+ test('the DB read is handed the cascade\'s OWN source and ref prefix, never a second copy', async () => {
89
+ reset({ open: [] });
90
+ await get('/artist-gate');
91
+ assert.equal(queries.length, 1);
92
+ assert.equal(queries[0].source, gateModule.ARTIST_REVIEW_SOURCE);
93
+ assert.equal(queries[0].refPrefix, gateModule.ARTIST_REVIEW_REF_PREFIX);
94
+ assert.deepEqual(queries[0].resolvedStatuses, gateModule.RESOLVED_REVIEW_STATUSES);
95
+ });
96
+
97
+ test("at 'strict' an open review is reported as holding, with the refusal already composed", async () => {
98
+ reset({ open: [review()] });
99
+ const r = await get('/artist-gate');
100
+ assert.equal(r.body.mode, 'strict');
101
+ assert.equal(r.body.holds, true);
102
+ assert.deepEqual(r.body.reviews.map((t) => t.id), ['9001']);
103
+ assert.equal(r.body.halt.code, 'ARTIST_REVIEW_UNRESOLVED');
104
+ assert.match(r.body.halt.detail, /override-request/, 'the caller must not have to invent the way out');
105
+ });
106
+
107
+ test("at 'advisory' the same open review is reported but holds nothing", async () => {
108
+ reset({ level: 'advisory', open: [review()] });
109
+ const r = await get('/artist-gate');
110
+ assert.equal(r.body.holds, false);
111
+ assert.equal(r.body.halt, null);
112
+ assert.deepEqual(r.body.reviews.map((t) => t.id), ['9001'], 'advisory still SHOWS the queue — it just does not stop');
113
+ });
114
+
115
+ test('the cutoff is applied to what the board returned (ADR 0241 §5)', async () => {
116
+ reset({ cutoff: '2026-09-11T00:00:00.000Z', open: [review({ id: '9001', created_at: '2026-09-01T00:00:00.000Z' }), review({ id: '9002' })] });
117
+ const r = await get('/artist-gate');
118
+ assert.deepEqual(r.body.reviews.map((t) => t.id), ['9002'], 'the pre-raise review is not this gate\'s business');
119
+ assert.equal(r.body.since, '2026-09-11T00:00:00.000Z', 'and the cutoff itself is reported, so the answer is checkable');
120
+ });
121
+
122
+ test('`holds` and `halt` can never disagree', async () => {
123
+ for (const open of [[], [review()]]) {
124
+ for (const level of ['off', 'advisory', 'strict']) {
125
+ reset({ level, open });
126
+ const r = await get('/artist-gate');
127
+ assert.equal(r.body.holds, r.body.halt !== null, `${level} / ${open.length} open`);
128
+ }
129
+ }
130
+ });
131
+
132
+ test('the payload carries NO task title — the one untrusted string it could have carried', async () => {
133
+ reset({ open: [review()] });
134
+ const r = await get('/artist-gate');
135
+ assert.deepEqual(Object.keys(r.body.reviews[0]).sort(), ['created_at', 'id']);
136
+ assert.doesNotMatch(r.raw, /Artist review: the copy/, 'builder-written board text never reaches this wire');
137
+ });
138
+
139
+ test('a capped scan reports a FLOOR, not a census — the verdict is unchanged either way', async () => {
140
+ reset({ open: [review({ id: '9001' }), review({ id: '9002' })], truncated: true });
141
+ const r = await get('/artist-gate');
142
+ assert.equal(r.body.truncated, true, 'the caller must never read the list as "and that is all"');
143
+ assert.equal(r.body.holds, true, 'truncation cannot weaken the gate — every row returned is a holder');
144
+ assert.match(r.body.halt.message, /^2\+ unresolved artist reviews/);
145
+ });
146
+
147
+ test('truncation is DROPPED when the cutoff shortened the page — "200+" over three rows is the worse lie', async () => {
148
+ reset({
149
+ cutoff: '2026-09-11T00:00:00.000Z',
150
+ open: [review({ id: '9001', created_at: '2026-09-01T00:00:00.000Z' }), review({ id: '9002' })],
151
+ truncated: true,
152
+ });
153
+ const r = await get('/artist-gate');
154
+ assert.equal(r.body.truncated, false);
155
+ assert.match(r.body.halt.message, /^1 unresolved artist review /);
156
+ });
157
+
158
+ test('the read is never cached — a dismissed review must free the deploy immediately', async () => {
159
+ reset({ open: [review()] });
160
+ const r = await get('/artist-gate');
161
+ assert.equal(r.headers['cache-control'], 'no-store');
162
+ });
163
+
164
+ test('a database failure is a 500 with no internals, never a quiet "nothing holds"', async () => {
165
+ reset({ open: [] });
166
+ db.listOpenArtistReviews = async () => { throw new Error('relation "tasks" does not exist'); };
167
+ const r = await get('/artist-gate');
168
+ assert.equal(r.status, 500);
169
+ assert.equal(r.body.error.code, 'artist_gate_read_failed');
170
+ assert.doesNotMatch(r.raw, /relation/, 'the error text never reaches the wire');
171
+ db.listOpenArtistReviews = async (opts) => { queries.push(opts); return { reviews: rows, truncated: scanTruncated }; };
172
+ });
173
+
174
+ test.after(() => server.close());
@@ -0,0 +1,117 @@
1
+ // tests/box_scope_predicate.mjs
2
+ //
3
+ // task 1002593 — "is this a box-scope refusal?" must have exactly ONE definition,
4
+ // and it must survive the error-envelope shape.
5
+ //
6
+ // THE BUG THIS PINS. A box-scoped session (ADR 0053) may call only the dev-box
7
+ // bootstrap endpoints; everything else answers 403 box_scope. Callers branched on
8
+ // `res.data.error === 'box_scope'`. Then routes moved to the object error envelope
9
+ // (ADR 0116) — `data.error` became `{ code, message, details }` — and every one of
10
+ // those comparisons silently became dead code. Nothing failed; the branch just
11
+ // stopped firing, and builders got the generic 403 path instead of the guided
12
+ // re-auth the branch existed to give them. Task 1002162 fixed it in cli-lib by
13
+ // adding isBoxScope(); scripts/gds/ship-land.js was the last site still deciding
14
+ // for itself, and task 1002593 repointed it.
15
+ //
16
+ // WHY A DRIFT GUARD AND NOT JUST UNIT CASES. The original defect was not a wrong
17
+ // answer — it was a comparison that could never be true. A unit test of the good
18
+ // predicate cannot see a second, dead one somewhere else in the tree; only a scan
19
+ // can. So the last test here is the one that actually protects against a repeat.
20
+ //
21
+ // Run: node tests/box_scope_predicate.mjs
22
+
23
+ import { strict as assert } from 'node:assert';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { createRequire } from 'node:module';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const CLI_DIR = path.join(ROOT, 'scripts', 'gds');
32
+ const { isBoxScope } = require(path.join(CLI_DIR, 'cli-lib.js'));
33
+
34
+ let passed = 0;
35
+ let failed = 0;
36
+ function t(name, fn) {
37
+ try { fn(); passed++; console.log(` PASS ${name}`); }
38
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
39
+ }
40
+
41
+ const ENVELOPE = { status: 403, data: { error: { code: 'box_scope', message: 'box-scoped session' } } };
42
+ const LEGACY = { status: 403, data: { error: 'box_scope' } };
43
+
44
+ t('fires for the CURRENT object error envelope — the shape that killed the old comparison', () => {
45
+ assert.equal(isBoxScope(ENVELOPE), true);
46
+ });
47
+
48
+ t('the old hand-rolled comparison is provably dead against that same shape', () => {
49
+ // Not a tautology: this is the regression itself. If someone reintroduces
50
+ // `data.error === 'box_scope'` anywhere, this is what it evaluates to.
51
+ assert.equal(ENVELOPE.data.error === 'box_scope', false,
52
+ 'the envelope shape must NOT satisfy a bare string comparison — that is the whole defect');
53
+ });
54
+
55
+ t('still fires for the LEGACY flat shape, so a pinned older core keeps working', () => {
56
+ assert.equal(isBoxScope(LEGACY), true);
57
+ });
58
+
59
+ t('does not fire on a different code at 403', () => {
60
+ assert.equal(isBoxScope({ status: 403, data: { error: { code: 'rank_too_low_for_task' } } }), false);
61
+ assert.equal(isBoxScope({ status: 403, data: { error: 'forbidden' } }), false);
62
+ });
63
+
64
+ t('does not fire on a non-403, even carrying the code', () => {
65
+ assert.equal(isBoxScope({ status: 200, data: { error: { code: 'box_scope' } } }), false,
66
+ 'status is part of the contract — a 200 mentioning box_scope is not a refusal');
67
+ assert.equal(isBoxScope({ status: 500, data: { error: { code: 'box_scope' } } }), false);
68
+ });
69
+
70
+ t('tolerates missing/!odd bodies instead of throwing', () => {
71
+ for (const bad of [undefined, null, {}, { status: 403 }, { status: 403, data: null }, { status: 403, data: {} }]) {
72
+ assert.equal(isBoxScope(bad), false, `${JSON.stringify(bad)} must be a quiet false, not a throw`);
73
+ }
74
+ assert.equal(isBoxScope({ status: 403, data: { error: null } }), false,
75
+ 'a null error must not be read as an envelope');
76
+ });
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // The drift guard. One definition, and no second opinion anywhere in the CLI.
80
+ // ---------------------------------------------------------------------------
81
+ t('no CLI script decides box-scope for itself — cli-lib owns the predicate', () => {
82
+ const files = fs.readdirSync(CLI_DIR).filter((f) => f.endsWith('.js'));
83
+ // Fail loudly on a scan that found nothing: an empty sweep looks exactly like a
84
+ // clean one, and a guard that silently stops scanning protects nothing.
85
+ assert.ok(files.length > 20, `only ${files.length} scripts scanned — the sweep is not reading the CLI directory`);
86
+
87
+ const offenders = [];
88
+ let sawTheCanary = false;
89
+ for (const f of files) {
90
+ const src = fs.readFileSync(path.join(CLI_DIR, f), 'utf8');
91
+ if (f === 'cli-lib.js') {
92
+ // The one legitimate definition. Confirms the scan can actually see the
93
+ // pattern it is looking for, so a regex that has drifted off the source
94
+ // shape reports as a failure here rather than as a clean sweep everywhere.
95
+ sawTheCanary = /===\s*'box_scope'/.test(src);
96
+ continue;
97
+ }
98
+ // Strip comments: several files DISCUSS the old comparison on purpose.
99
+ const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
100
+ for (const m of code.matchAll(/[^\n]*===\s*'box_scope'[^\n]*/g)) {
101
+ offenders.push(`${f}: ${m[0].trim()}`);
102
+ }
103
+ }
104
+
105
+ assert.ok(sawTheCanary, 'cli-lib.js no longer contains the comparison — this guard is looking for the wrong pattern');
106
+ assert.deepEqual(offenders, [],
107
+ `these sites decide box-scope themselves instead of calling isBoxScope(); that is how the comparison died last time:\n ${offenders.join('\n ')}`);
108
+ });
109
+
110
+ t('ship-land.js — the site task 1002593 repointed — uses the shared predicate', () => {
111
+ const src = fs.readFileSync(path.join(CLI_DIR, 'ship-land.js'), 'utf8');
112
+ assert.match(src, /isBoxScope\s*\(/, 'the box-scope branch must call the shared predicate');
113
+ assert.match(src, /require\('\.\/cli-lib'\)/, 'and import it from cli-lib rather than redefining it');
114
+ });
115
+
116
+ console.log(`\nbox_scope_predicate: ${passed} passed, ${failed} failed`);
117
+ process.exit(failed === 0 ? 0 : 1);