@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
@@ -0,0 +1,167 @@
1
+ // tests/main_worktree_busy_reason.mjs
2
+ //
3
+ // task 1002871 — every bail that touches the shared main checkout must identify
4
+ // the OCCUPANT, not just the symptom.
5
+ //
6
+ // THE INCIDENT (idea 1000604, 2026-07-28). A shared main checkout sat parked on
7
+ // `task-1002503-backfill-recovery` with 2 unpushed commits and 4 modified
8
+ // generated files, and blocked the auto-merge leg for NINE confirmed tasks. Most
9
+ // of that idea was fixed: ship-merge.js now detects a main checkout on a feature
10
+ // branch, names it, and states the cure.
11
+ //
12
+ // What was NOT fixed is the exact shape of the incident. mainWorktreeBusy() runs
13
+ // FIRST, and only ever said 'a merge is already in progress' or 'the working tree
14
+ // is dirty or has unmerged files'. A checkout that is BOTH parked AND dirty — the
15
+ // 2026-07-28 state precisely — short-circuits there, so the named-branch
16
+ // diagnostic was reachable only when the tree was clean: the one case where
17
+ // nobody needs telling twice.
18
+ //
19
+ // The decision is pure now so the exact string is pinned rather than eyeballed.
20
+ // This is the ship hot path and the string IS the deliverable.
21
+ //
22
+ // Run: node tests/main_worktree_busy_reason.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { readFileSync } from 'node:fs';
26
+ import path from 'node:path';
27
+ import { createRequire } from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const { describeMainWorktreeBusy } = require(path.join(ROOT, 'scripts', 'gds', 'ship-regen.js'));
33
+ const { parkedMainCheckoutWarning } = require(path.join(ROOT, 'scripts', 'gds', 'worktree-claim-guard.js'));
34
+
35
+ let passed = 0;
36
+ let failed = 0;
37
+ function t(name, fn) {
38
+ try { fn(); passed++; console.log(` PASS ${name}`); }
39
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
40
+ }
41
+
42
+ // --- describeMainWorktreeBusy ------------------------------------------------
43
+
44
+ t('a clean main checkout on main is not busy', () => {
45
+ assert.equal(describeMainWorktreeBusy({ mergeInProgress: false, dirty: false, branch: 'main' }), null);
46
+ });
47
+
48
+ t('THE INCIDENT: dirty AND parked on a feature branch names the branch', () => {
49
+ const reason = describeMainWorktreeBusy({
50
+ mergeInProgress: false, dirty: true, branch: 'task-1002503-backfill-recovery',
51
+ });
52
+ assert.match(reason, /dirty or has unmerged files/, 'keeps the original symptom');
53
+ assert.match(reason, /task-1002503-backfill-recovery/, 'and NAMES the squatting branch — the whole point');
54
+ assert.match(reason, /not `main`/, 'and says what it should have been');
55
+ });
56
+
57
+ t('a merge in progress on a feature branch also names it', () => {
58
+ const reason = describeMainWorktreeBusy({ mergeInProgress: true, dirty: true, branch: 'task-999' });
59
+ assert.match(reason, /merge is already in progress/);
60
+ assert.match(reason, /task-999/, 'the branch is named on this arm too, not just the dirty one');
61
+ });
62
+
63
+ t('merge-in-progress wins over dirty — one reason, the most specific', () => {
64
+ const reason = describeMainWorktreeBusy({ mergeInProgress: true, dirty: true, branch: 'main' });
65
+ assert.match(reason, /merge is already in progress/);
66
+ assert.ok(!/dirty/.test(reason), 'must not concatenate both symptoms into one run-on reason');
67
+ });
68
+
69
+ t('on `main` itself the branch is NOT named — that line would be pure noise', () => {
70
+ const reason = describeMainWorktreeBusy({ mergeInProgress: false, dirty: true, branch: 'main' });
71
+ assert.equal(reason, 'the working tree is dirty or has unmerged files',
72
+ 'saying "(on main, not main)" in every bail trains the reader to skip the line');
73
+ });
74
+
75
+ t('an unreadable branch degrades to the old wording rather than inventing one', () => {
76
+ for (const branch of [null, undefined, '', 'HEAD']) {
77
+ const reason = describeMainWorktreeBusy({ mergeInProgress: false, dirty: true, branch });
78
+ assert.equal(reason, 'the working tree is dirty or has unmerged files',
79
+ `branch=${JSON.stringify(branch)} must not produce a half-formed name`);
80
+ }
81
+ });
82
+
83
+ t('a non-default defaultBranch is honoured', () => {
84
+ assert.equal(describeMainWorktreeBusy({ dirty: true, branch: 'trunk', defaultBranch: 'trunk' }),
85
+ 'the working tree is dirty or has unmerged files', 'the repo default is not hardcoded to main');
86
+ assert.match(describeMainWorktreeBusy({ dirty: true, branch: 'main', defaultBranch: 'trunk' }),
87
+ /on `main`, not `trunk`/);
88
+ });
89
+
90
+ t('no facts at all is a quiet null, never a throw', () => {
91
+ assert.equal(describeMainWorktreeBusy(), null);
92
+ assert.equal(describeMainWorktreeBusy({}), null);
93
+ });
94
+
95
+ // --- parkedMainCheckoutWarning (the release.js half) -------------------------
96
+
97
+ t('release warning is silent when the main checkout is on main', () => {
98
+ assert.deepEqual(parkedMainCheckoutWarning({ branch: 'main' }), []);
99
+ assert.deepEqual(parkedMainCheckoutWarning({ branch: null }), [], 'an unreadable branch says nothing');
100
+ assert.deepEqual(parkedMainCheckoutWarning({}), []);
101
+ });
102
+
103
+ t('release warning names the parked branch, the cost, and the cure', () => {
104
+ const out = parkedMainCheckoutWarning({ branch: 'task-1002503-backfill-recovery' }).join('\n');
105
+ assert.match(out, /task-1002503-backfill-recovery/, 'names the occupant');
106
+ assert.match(out, /nowhere to merge INTO/, 'states the cost, not just the fact');
107
+ assert.match(out, /someone else/, 'and that the cost lands on other builders too');
108
+ assert.match(out, /checkout main/, 'and gives the cure');
109
+ });
110
+
111
+ t('release warning NEVER offers to restore the tree — on EITHER branch of the message', () => {
112
+ // Both arms, deliberately. A probe that made only the clean-tree line offer a
113
+ // restore kept this test green when it checked the dirty arm alone: the
114
+ // "we will not touch it" promise has two renderings and both must keep it.
115
+ for (const dirty of [true, false]) {
116
+ const out = parkedMainCheckoutWarning({ branch: 'task-42', dirty }).join('\n');
117
+ assert.match(out, /nothing here will touch it/i,
118
+ `dirty=${dirty}: switching the tree back would be a destructive guess — the branch may hold real work`);
119
+ assert.ok(!/\brestore it\b/i.test(out),
120
+ `dirty=${dirty}: must not offer to restore the tree on the builder's behalf`);
121
+ }
122
+ const dirtyOut = parkedMainCheckoutWarning({ branch: 'task-42', dirty: true }).join('\n');
123
+ assert.match(dirtyOut, /uncommitted changes/i, 'and when it IS dirty, say so — that is why we do not touch it');
124
+ });
125
+
126
+ // --- the probes that gather the facts must not build shell strings -----------
127
+ //
128
+ // Caught by the grader on this task: the first cut ran
129
+ // `git -C "${root}" rev-parse …` through execSync, which spawns a shell. A
130
+ // resolved filesystem path is NOT trusted input — a directory containing a quote,
131
+ // backtick or `$()` breaks the quoting and executes as shell. The repo's
132
+ // established pattern passes the directory to child_process instead of
133
+ // interpolating it into a command string, and release.js now goes further and
134
+ // uses an argv array with no shell at all.
135
+ t('release.js never interpolates a path into a git command string', () => {
136
+ const raw = readFileSync(path.join(ROOT, 'scripts', 'gds', 'release.js'), 'utf8');
137
+ // Strip comments first: the file DESCRIBES the unsafe form on purpose, to stop
138
+ // the next reader reintroducing it. Scanning prose as if it were code is how a
139
+ // guard reds on its own documentation — which this one did, first run.
140
+ const src = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
141
+ assert.ok(!/git\s+-C\s+["'`]?\$\{/.test(src),
142
+ 'a `git -C "${path}"` command string is shell-interpolated — pass the directory to child_process instead');
143
+ assert.ok(!/execSync\s*\(\s*[`'"]git /.test(src),
144
+ 'a git command built as a shell string re-opens the injection this test exists to close');
145
+ assert.match(src, /execFileSync\('git', args/,
146
+ 'git must be invoked with an argv array, which never reaches a shell');
147
+ assert.match(src, /cwd,/, 'and the directory must travel as cwd, not inside the command');
148
+ // The stripper must not have eaten the file: a guard over an empty string
149
+ // passes every negative assertion above and proves nothing.
150
+ assert.ok(src.length > 500, `comment-stripping left only ${src.length} chars — the scan is reading nothing`);
151
+ });
152
+
153
+ t('release.js imports nothing outside the published CLI package', () => {
154
+ const src = readFileSync(path.join(ROOT, 'scripts', 'gds', 'release.js'), 'utf8');
155
+ const manifest = readFileSync(path.join(ROOT, 'scripts', 'gds', 'build-cli-package.js'), 'utf8');
156
+ const locals = [...src.matchAll(/require\('\.\/([A-Za-z0-9._-]+)'\)/g)].map((m) => m[1]);
157
+ assert.ok(locals.length > 0, 'no local requires found — this guard is reading the wrong shape');
158
+ for (const name of locals) {
159
+ const file = name.endsWith('.js') ? name : `${name}.js`;
160
+ assert.ok(manifest.includes(`scripts/gds/${file}`),
161
+ `release.js requires ./${name}, which is not in the CLI package FILES list — `
162
+ + '`bongos release` would crash at require time in the published package (it did once already)');
163
+ }
164
+ });
165
+
166
+ console.log(`\nmain_worktree_busy_reason: ${passed} passed, ${failed} failed`);
167
+ process.exit(failed === 0 ? 0 : 1);
@@ -35,9 +35,13 @@ import assert from 'node:assert/strict';
35
35
  import { test } from 'node:test';
36
36
  import fs from 'node:fs';
37
37
  import path from 'node:path';
38
+ import { createRequire } from 'node:module';
38
39
  import { fileURLToPath } from 'node:url';
39
40
  import { authSource } from './helpers.mjs';
40
41
 
42
+ const require = createRequire(import.meta.url);
43
+ const lint = require('../scripts/gds/lint-migrations.js');
44
+
41
45
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
42
46
  const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
43
47
 
@@ -111,3 +115,87 @@ test('the sender writes BOTH spellings, computing the kind once (the cross-versi
111
115
  assert.equal((body.match(/membershipKind\(\{ githubLogin \}\)/g) || []).length, 1,
112
116
  'the kind is computed ONCE per check-in, not per field');
113
117
  });
118
+
119
+ // ── THE CONTRACT PHASE — the test of the shim's own removal (task 1003089) ─────
120
+ //
121
+ // ADR 0174's Lineage lesson: a time-boxed shim outlives its release unless its
122
+ // removal ships with the test that says it is gone. core_240 closed the GOAL
123
+ // membership window — the trigger, the function and `goal_members.role` are all
124
+ // dropped, and `membership_kind` carries NOT NULL + DEFAULT itself rather than
125
+ // resting on a trigger surviving.
126
+ //
127
+ // The PROJECT membership half (platform_identity_010) is deliberately NOT here:
128
+ // it is still inside its cross-version federation window, its contract phase is
129
+ // task 1003092, and the expand-phase block above is what still guards it. Do not
130
+ // merge the two — the whole point of the pair is that they close on different
131
+ // schedules.
132
+ const CONTRACT = 'migrations/core_240_goal_members_drop_legacy_role.sql';
133
+
134
+ test('core_240: the contract phase drops the trigger, the function AND the column', () => {
135
+ const sql = read(CONTRACT);
136
+ assert.match(sql, /DROP TRIGGER IF EXISTS goal_members_membership_kind_lockstep ON goal_members/);
137
+ assert.match(sql, /DROP FUNCTION IF EXISTS goal_members_membership_kind_lockstep\(\)/,
138
+ 'the FUNCTION outlives the trigger unless it is dropped by name — dropping only the trigger '
139
+ + 'leaves a dangling plpgsql body naming a column that no longer exists');
140
+ assert.match(sql, /ALTER TABLE goal_members\s+DROP COLUMN IF EXISTS role/);
141
+ assert.match(sql, /ALTER COLUMN membership_kind SET NOT NULL/,
142
+ 'the guarantee core_220 deferred to step 2 arrives here, or the column never gets one');
143
+ assert.match(sql, /ALTER COLUMN membership_kind SET DEFAULT 'member'/,
144
+ 'safe only NOW: a default while the legacy column was still written is the task-1003093 bug');
145
+ });
146
+
147
+ test('core_240: the forward-unsafe pragma is present, and its FIRST line carries the reason', () => {
148
+ const scan = lint.scanMigrationSql(read(CONTRACT));
149
+ assert.deepEqual([...scan.unsafe].sort(), ['drop-column', 'set-not-null'],
150
+ 'the linter must still SEE both operations — an acknowledged migration is not an exempt one');
151
+ assert.ok(scan.acknowledged,
152
+ 'a drop-column with no pragma reds tests/migrations_forward_safe.mjs, which is the point');
153
+ // ACK_RE captures a SINGLE line, so a reason spread over a paragraph records
154
+ // nothing in the lint output. The release a rollback could restore is the one
155
+ // fact that makes the drop safe, so it belongs on that line.
156
+ assert.match(scan.ackReason, /1\.19\.238/,
157
+ 'the pragma must name the release that stopped reading the column, not merely assert safety');
158
+ });
159
+
160
+ test('the FRESH-INSTALL seed writes the new name — the writer that is not application code', () => {
161
+ // The last writer of `goal_members.role` was a MODULE migration, not a query:
162
+ // lifecycle_001_goal_first_leads.sql named it in the INSERT list AND the
163
+ // ON CONFLICT arm. Module migrations run AFTER the core ones on every fresh
164
+ // install, so a stale name here survives the DROP invisibly on an existing
165
+ // instance (already in schema_migrations, never re-run) and breaks
166
+ // `cloudbongos init` for every new one. Zero rows on a fresh DB does not save
167
+ // it: a missing column is a plan-time error, not a row-time one.
168
+ const code = lint.stripSqlComments(read('modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql'));
169
+ assert.match(code, /INSERT INTO goal_members \(goal_id, builder_id, membership_kind\)/);
170
+ assert.match(code, /DO UPDATE SET membership_kind = 'lead'/,
171
+ 'the ON CONFLICT arm names the column too — renaming only the INSERT list still fails to plan');
172
+ assert.ok(!/\brole\b/.test(code),
173
+ 'no statement in the fresh-install seed may name the dropped column');
174
+ });
175
+
176
+ test('no migration outside the three that own its lifecycle still names goal_members.role', () => {
177
+ // Comments are stripped first: the history is allowed to DISCUSS the old column,
178
+ // and this rule is about statements. The three exemptions are the migrations that
179
+ // created the column, expanded away from it, and dropped it.
180
+ const OWNS_ITS_LIFECYCLE = new Set([
181
+ '160_goal_tier.sql', // created it
182
+ 'core_220_goal_members_membership_kind.sql', // expand: read it to backfill; the trigger kept it in step
183
+ 'core_240_goal_members_drop_legacy_role.sql', // contract: dropped it
184
+ ]);
185
+ const dirs = [path.join(ROOT, 'migrations')];
186
+ for (const d of fs.readdirSync(path.join(ROOT, 'modules'), { withFileTypes: true })) {
187
+ if (!d.isDirectory()) continue;
188
+ const md = path.join(ROOT, 'modules', d.name, 'migrations');
189
+ if (fs.existsSync(md)) dirs.push(md);
190
+ }
191
+ const offenders = [];
192
+ for (const dir of dirs) {
193
+ for (const f of fs.readdirSync(dir)) {
194
+ if (!f.endsWith('.sql') || OWNS_ITS_LIFECYCLE.has(f)) continue;
195
+ const code = lint.stripSqlComments(fs.readFileSync(path.join(dir, f), 'utf8'));
196
+ if (/goal_members/.test(code) && /\brole\b/.test(code)) offenders.push(f);
197
+ }
198
+ }
199
+ assert.deepEqual(offenders, [],
200
+ `these migrations touch goal_members and still name the dropped column: ${offenders.join(', ')}`);
201
+ });
@@ -161,6 +161,20 @@ t('findSecretLeak detects a secret shape, clean text passes', () => {
161
161
  assert.equal(findSecretLeak('token ghp_' + 'a'.repeat(36)), true);
162
162
  assert.equal(findSecretLeak('just ordinary prose here'), false);
163
163
  });
164
+ // Regression (task 1003873). findSecretLeak is a DIFFERENTIAL audit — it re-scrubs and asks
165
+ // "did anything change?" — so it must run with the same options as the scrub it is auditing.
166
+ // Given none, it re-redacts the long ADR filename that task 1002460's allowlist deliberately
167
+ // spared, mistakes its own disagreement for a leak, and (the gate being fail-closed) reports
168
+ // a phantom secret in every published doc.
169
+ t('findSecretLeak forwards scrub options, so it can audit a scrub that used them', () => {
170
+ const name = '0281-differential-audit-allowlist-symmetry.md';
171
+ const doc = `see docs/adr/${name} for the rationale`;
172
+ const knownDocNames = new Set([name]);
173
+ assert.equal(findSecretLeak(doc), true, 'without the allowlist the filename reads as a secret');
174
+ assert.equal(findSecretLeak(doc, [], { knownDocNames }), false, 'with it, a file is a file');
175
+ // …and the allowlist is not a smuggling channel: a real shape beside it still trips.
176
+ assert.equal(findSecretLeak(`ghp_${'a'.repeat(36)} in docs/adr/${name}`, [], { knownDocNames }), true);
177
+ });
164
178
 
165
179
  // ── vanilla instance (empty branding) — IP floor still works, no token/owner rules ────
166
180
  t('empty branding derives no tokens/owners but the IP floor still redacts', () => {
@@ -23,6 +23,11 @@ const PUBLISHED_SURFACE = [
23
23
  'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
24
24
  'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
25
25
  'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
26
+ // added by task 1003575 (ADR 0241 §3): the instance's PROJECT OWNER — its first
27
+ // admitted human builder. Kernel identity like getBuilderById beside it, and
28
+ // published because the artist gate's one escape hatch is the owner's alone, and
29
+ // a module cannot enforce that without being able to ask who the owner is.
30
+ 'getFoundingBuilder',
26
31
  'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
27
32
  'grantedScopeCoversRepos', // the create-route credential preflight's scope read (task 1002909); kernel (auth-config.js), unlike db/github-repos which the module reaches via `pool` + its own client
28
33
  'accountExistenceReadRateLimit', // task 1003339 (ADR 0209): the shared per-IP budget both account-existence probes spend from — one instance, not a factory, so the budget is genuinely shared
@@ -40,6 +45,7 @@ const PUBLISHED_SURFACE = [
40
45
  'resolveRotDays', 'projectSettings', // added by task 1003279 (ADR 0232; goal 1000072): the ROT timer + the live knob store behind it. A DIFFERENT question from the stale-claim timer above — days not hours, DB not env, work nobody picked up rather than a builder who went quiet, and it only ever ASKS a human where that one ACTS. Both ride the doorway so no module hardcodes either.
41
46
  'joinabilityMode', // added by task 1003044 (ADR 0194; the carrier after 1.19.374): the project's membership door, one reader for the sign-in gate AND the onboarding module's public POST
42
47
  'projectJoinDoor', // added by task 1002331 (ADR 0182 D6 / ADR 0247): the COMPOSED join/apply door — visibility × join grant × joinability, one answer, so a module reading a single knob cannot re-open a door the other two closed
48
+ 'artistGate', // added by task 1003575 (ADR 0241): the artist gate — off | advisory | strict, plus the pure verdict helpers over it. Two modules ask it two different questions ("file this review?" and "does an open one hold the deploy?") and both must resolve the knob through this one reader, never branding().project
43
49
  'readApplicantProfileFromHub', // added by task 1002972 (privacy spec D7): the reviewer queue's LIVE per-render read of one applicant's hub profile view. NARROW by design — the underlying call is authenticated with this instance's hub client secret, and loadIdpConfig stays OFF the doorway so no module ever holds the credential that speaks for the whole project
44
50
  'enabledDisciplines', // BV1.R81: instance offered-disciplines (1.9.0; onboarding restock backstop)
45
51
  'registerProvider', 'hasProvider', 'resolve', 'resolveOptional', 'listPorts', 'verifyPortsSatisfied',
@@ -61,6 +61,8 @@ ${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY=public
61
61
  ${NEUTRAL_PREFIX}_JOINABILITY=apply
62
62
  ${NEUTRAL_PREFIX}_VISIBILITY=public
63
63
  ${NEUTRAL_PREFIX}_JOIN_GRANT=full
64
+ ${NEUTRAL_PREFIX}_ARTIST_GATE=strict
65
+ ${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE=
64
66
  `;
65
67
 
66
68
  t('replaces an existing KEY= line where it stands; every other line survives byte-for-byte', () => {
@@ -113,7 +115,13 @@ t('is PURE: same input, same output; empty body yields just the lines', () => {
113
115
  t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at standup, under the pack prefix', () => {
114
116
  const inst = { slug: 'spike', port: 3010, db_name: 'spike', hosting_shape: 'standalone', target_ref: 'jaxri/spike', settings: { platform_visibility: 'gated' } };
115
117
  const vars = P.settingsEnvVarsFor(inst);
116
- assert.deepEqual(vars, { [`${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY`]: 'gated', [`${NEUTRAL_PREFIX}_JOINABILITY`]: 'apply', [`${NEUTRAL_PREFIX}_VISIBILITY`]: 'public', [`${NEUTRAL_PREFIX}_JOIN_GRANT`]: 'full' });
118
+ assert.deepEqual(vars, {
119
+ [`${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY`]: 'gated', [`${NEUTRAL_PREFIX}_JOINABILITY`]: 'apply',
120
+ [`${NEUTRAL_PREFIX}_VISIBILITY`]: 'public', [`${NEUTRAL_PREFIX}_JOIN_GRANT`]: 'full',
121
+ // the artist gate + its non-retroactivity stamp (task 1003575, ADR 0241) ride
122
+ // the same transport; the stamp is empty until the gate is actually RAISED
123
+ [`${NEUTRAL_PREFIX}_ARTIST_GATE`]: 'strict', [`${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE`]: '',
124
+ });
117
125
  const body = P.webEnvBody(inst, {});
118
126
  for (const [k, v] of Object.entries(vars)) assert.match(body, new RegExp(`^${k}=${v}$`, 'm'), `${k} at standup`);
119
127
  });
@@ -124,10 +132,10 @@ const GATED = { settings: { platform_visibility: 'gated', joinability: 'invite_o
124
132
  const manifestWith = (project) => JSON.stringify({ schema: 'instance-manifest/v1', brand: project === undefined ? {} : { project } });
125
133
 
126
134
  t('true when every pushed key matches brand.project (camelCase, the ENV_OVERRIDES targets)', () => {
127
- assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full' }), GATED), true);
135
+ assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), true);
128
136
  });
129
137
  t('false when the instance still reports a different value', () => {
130
- assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full' }), GATED), false);
138
+ assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), false);
131
139
  assert.equal(P.settingsConsumed(manifestWith({ joinability: 'invite_only' }), GATED), false, 'a missing key is a mismatch, not a pass');
132
140
  });
133
141
  t('null (unknown) when the manifest carries no project block — a core that predates task 1003139 — or does not parse', () => {
@@ -145,7 +153,7 @@ t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
145
153
  function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, healthzFails = false, privileged = false,
146
154
  manifest = 'match', restartThrows = false, writeThrows = false } = {}) {
147
155
  const cmds = [], writes = [], events = [], probes = [], cleared = [], logs = [];
148
- const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full' })
156
+ const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' })
149
157
  : manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public' })
150
158
  : manifest === 'none' ? manifestWith(undefined) : 'nope';
151
159
  const exec = (cmd, opts = {}) => {
@@ -214,7 +222,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
214
222
  assert.equal(r.ok, true);
215
223
  assert.equal(r.healthy, null, 'nothing ran, nothing observed');
216
224
  assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
217
- assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
225
+ assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
218
226
  assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
219
227
  assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
220
228
  });
@@ -164,7 +164,7 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
164
164
  reset();
165
165
  const r = await get('/provisioning/instances/18/settings');
166
166
  assert.equal(r.status, 200, r.raw);
167
- assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' });
167
+ assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
168
168
  // the planet rides beside the settings (task 1003347): no default — null = the platform's own pick
169
169
  assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null, card_backdrop: null });
170
170
  assert.deepEqual(r.body.options, {
@@ -175,6 +175,10 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
175
175
  // what a PUBLIC join grants (task 1002331, ADR 0247) — the fourth policy
176
176
  // axis, offered to the form the same way
177
177
  join_grant: ['view', 'apply', 'full'],
178
+ // the artist gate (task 1003575, ADR 0241) — the fifth policy axis. It is
179
+ // offered to the form like the rest; what makes it different is only its
180
+ // default, which is the owner's ruling rather than today's behaviour.
181
+ artist_gate: ['off', 'advisory', 'strict'],
178
182
  planet_template: [...provisioning.PLANET_TEMPLATES], planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
179
183
  // the card's backdrop (task 1003352) is the same kind of key: platform-drawn,
180
184
  // closed list, no default, no push — so it rides the same vocab
@@ -187,14 +191,14 @@ await ta('a stored planet reads back; a name that left the vocabulary reads null
187
191
  const r = await get('/provisioning/instances/18/settings');
188
192
  assert.equal(r.status, 200, r.raw);
189
193
  assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: null, card_backdrop: null });
190
- assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the planet is not a setting: the policy read is untouched');
194
+ assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the planet is not a setting: the policy read is untouched');
191
195
  });
192
196
 
193
197
  await ta('stored keys overlay the defaults', async () => {
194
198
  reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { joinability: 'invite_only' } } });
195
199
  const r = await get('/provisioning/instances/18/settings');
196
200
  assert.equal(r.status, 200, r.raw);
197
- assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full' });
201
+ assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
198
202
  });
199
203
 
200
204
  await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
@@ -222,7 +226,7 @@ await ta('owner changes one key — merged, returned effective, audited api:self
222
226
  reset();
223
227
  const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
224
228
  assert.equal(r.status, 200, r.raw);
225
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
229
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
226
230
  assert.equal(r.body.push.queued, true, 'an active co-tenant gets the live push (task 1003140)');
227
231
  assert.deepEqual(updateCalls, [{ id: 18, patch: { joinability: 'open' } }], 'exactly the sent key, nothing else');
228
232
  assert.equal(events.length, 1);
@@ -235,7 +239,7 @@ await ta('owner changes both keys in one call', async () => {
235
239
  reset();
236
240
  const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'invite_only' });
237
241
  assert.equal(r.status, 200, r.raw);
238
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full' });
242
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
239
243
  });
240
244
 
241
245
  await ta('the OWN-SCOPE WALL: another builder\'s PATCH → 404 and the write path is never reached', async () => {
@@ -274,7 +278,7 @@ await ta('the owner sets the door to stealth — stored, echoed effective, audit
274
278
  reset();
275
279
  const r = await patch('/provisioning/instances/18/settings', { visibility: 'stealth' });
276
280
  assert.equal(r.status, 200, r.raw);
277
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full' });
281
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full', artist_gate: 'strict' });
278
282
  assert.deepEqual(updateCalls, [{ id: 18, patch: { visibility: 'stealth' } }], 'exactly the sent key, nothing else');
279
283
  assert.equal(events[0].actor, 'api:self');
280
284
  assert.match(events[0].detail, /visibility=stealth/);
@@ -334,7 +338,7 @@ await ta('the owner picks a planet: saved, echoed as `planet`, audited — and N
334
338
  const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
335
339
  assert.equal(r.status, 200, r.raw);
336
340
  assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: 'sea', card_backdrop: null });
337
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the policy half is untouched');
341
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the policy half is untouched');
338
342
  assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }]);
339
343
  assert.equal(r.body.push.queued, false);
340
344
  assert.equal(r.body.push.restart, false, 'an active co-tenant would get a push for a POLICY change — never for the planet');
@@ -225,7 +225,7 @@ await ta('ACTIVE standalone: the row is written FIRST, then a settings-apply int
225
225
  reset();
226
226
  const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
227
227
  assert.equal(r.status, 200, r.raw);
228
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
228
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
229
229
  assert.equal(r.body.settings, undefined);
230
230
  assert.deepEqual(updateCalls.map((c) => c.patch), [{ platform_visibility: 'gated' }]);
231
231
  assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the runner reads the row — the write must precede the intent');
@@ -274,7 +274,7 @@ await ta('an enqueue that THROWS still answers 200 "saved" — never a 500 that
274
274
  enqueueThrows = new Error('db down');
275
275
  const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
276
276
  assert.equal(r.status, 200, r.raw);
277
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full' });
277
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
278
278
  assert.equal(updateCalls.length, 1, 'the write landed');
279
279
  receiptShape(r.body.push, { queued: false, restart: true, reachable: true });
280
280
  assert.match(r.body.push.message, /Saved, but the restart could not be queued/);
@@ -287,7 +287,7 @@ await ta('same values as already stored → no write, no event, no intent, "No c
287
287
  reset({ inst: { ...ACTIVE, settings: { platform_visibility: 'gated', joinability: 'open' } } });
288
288
  const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'open' });
289
289
  assert.equal(r.status, 200, r.raw);
290
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full' });
290
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
291
291
  assert.equal(updateCalls.length, 0, 'nothing written (no updated_at reshuffle)');
292
292
  assert.equal(events.length, 0, 'no "settings changed" event for a change that is not one');
293
293
  assert.equal(enqueueCalls.length, 0, 'a whole-form save must not bounce a live project');
@@ -404,7 +404,7 @@ await ta('save A queues → save B 409s while A RUNS → A settles → the UNCHA
404
404
  assert.equal(enqueueCalls.length, enqueuedBefore + 1, 'exactly one new intent');
405
405
  assert.equal(updateCalls.length, writesBefore, 'nothing re-written — the value was already stored in step 2');
406
406
  assert.deepEqual(owedCalls[owedCalls.length - 1], { id: 18, owed: false }, 'and the debt is settled');
407
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full' },
407
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' },
408
408
  'the value that finally ships is the one the owner saved');
409
409
  });
410
410
 
@@ -59,8 +59,17 @@ t('a row with NO stored settings emits today\'s behavior EXPLICITLY, never nothi
59
59
  assert.equal(v.CLOUDBONGOS_VISIBILITY, 'public');
60
60
  // what a public join grants rides it too (task 1002331, ADR 0182 D6)
61
61
  assert.equal(v.CLOUDBONGOS_JOIN_GRANT, 'full');
62
+ // The artist gate is the one key whose default is NOT today's behaviour — it is
63
+ // the owner's ruling (ADR 0241 §4), and it is safe as a default because strict
64
+ // holds only a pin move, never a builder's ship.
65
+ assert.equal(v.CLOUDBONGOS_ARTIST_GATE, 'strict');
66
+ // Its companion stamp is emitted EMPTY rather than omitted: an absent variable is
67
+ // a policy the instance would have to infer, and empty already means "no cutoff".
68
+ assert.equal(v.CLOUDBONGOS_ARTIST_GATE_SINCE, '', 'present and empty — never absent');
69
+ assert.ok(linesOf(provisioning.settingsEnvLines({}, 'X')).some((l) => l === 'X_ARTIST_GATE_SINCE='));
62
70
  assert.equal(linesOf(provisioning.settingsEnvLines({}, 'X')).length,
63
- Object.keys(provisioning.SETTINGS_VOCAB).length, 'every policy key always present');
71
+ Object.keys(provisioning.SETTINGS_VOCAB).length + Object.keys(provisioning.SETTINGS_COMPANION_ENV).length,
72
+ 'every policy key and every declared companion always present');
64
73
  });
65
74
 
66
75
  t('an off-vocabulary stored value falls back to the default, never reaches the instance', () => {
@@ -82,10 +91,14 @@ t('the planet never rides the env transport (task 1003347): it is the platform\'
82
91
  assert.doesNotMatch(provisioning.settingsEnvLines({ settings: { planet_template: 'bead-obsidian-tint' } }, 'CB'), /planet|PLANET/);
83
92
  });
84
93
 
85
- t('every emitted key is in SETTINGS_VOCAB — the two lists cannot drift', () => {
94
+ t('every emitted key is a vocabulary key or a DECLARED companion — the lists cannot drift', () => {
86
95
  const keys = linesOf(provisioning.settingsEnvLines({}, 'X')).map((l) => l.split('=')[0].replace(/^X_/, '').toLowerCase());
87
- assert.deepEqual(keys.sort(), Object.keys(provisioning.SETTINGS_VOCAB).sort(),
88
- 'a key added to the vocabulary must be carried, or the instance never learns it');
96
+ const expected = [
97
+ ...Object.keys(provisioning.SETTINGS_VOCAB),
98
+ ...Object.keys(provisioning.SETTINGS_COMPANION_ENV).map((k) => k.toLowerCase()),
99
+ ];
100
+ assert.deepEqual(keys.sort(), expected.sort(),
101
+ 'a key added to the vocabulary must be carried, or the instance never learns it — and an env var the instance reads must be a DECLARED companion, never an ad-hoc extra');
89
102
  });
90
103
 
91
104
  console.log('\nThe generated file tells the truth about what it is:');
@@ -99,11 +112,50 @@ t('it names the live push by the intent the runner actually dispatches (task 100
99
112
  });
100
113
 
101
114
  t('settingsEnvVars is the single source both legs derive from — same keys, same values as the lines', () => {
102
- const row = { settings: { platform_visibility: 'gated', joinability: 'open', visibility: 'stealth', join_grant: 'view' } };
115
+ const row = { settings: { platform_visibility: 'gated', joinability: 'open', visibility: 'stealth', join_grant: 'view', artist_gate: 'advisory' } };
103
116
  const vars = provisioning.settingsEnvVars(row, 'CLOUDBONGOS');
104
- assert.deepEqual(vars, { CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open', CLOUDBONGOS_VISIBILITY: 'stealth', CLOUDBONGOS_JOIN_GRANT: 'view' });
117
+ assert.deepEqual(vars, {
118
+ CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open', CLOUDBONGOS_VISIBILITY: 'stealth',
119
+ CLOUDBONGOS_JOIN_GRANT: 'view', CLOUDBONGOS_ARTIST_GATE: 'advisory', CLOUDBONGOS_ARTIST_GATE_SINCE: '',
120
+ });
121
+ // varsOf splits on '=', so an empty value reads as '' on both sides — the point
122
+ // of the comparison is that the standup lines carry exactly the same key set.
105
123
  assert.deepEqual(varsOf(provisioning.settingsEnvLines(row, 'CLOUDBONGOS')), vars, 'the standup lines ARE the vars');
106
- assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), { X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full' }, 'defaults, explicitly');
124
+ assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), {
125
+ X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full',
126
+ X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '',
127
+ }, 'defaults, explicitly');
128
+ });
129
+
130
+ console.log('\nThe artist gate rides the same transport (ADR 0241, task 1003575):');
131
+
132
+ t('the stamp is carried only when the gate was actually RAISED, and it is the row\'s own value', () => {
133
+ const stamped = { settings: { artist_gate: 'strict', artist_gate_strict_since: '2026-09-13T00:00:00.000Z' } };
134
+ assert.equal(provisioning.settingsEnvVars(stamped, 'X').X_ARTIST_GATE_SINCE, '2026-09-13T00:00:00.000Z');
135
+ // An unparseable stamp is NOT transmitted as a date nobody can read: it reads as
136
+ // absent, which is the safe direction — no cutoff, so every open review counts.
137
+ assert.equal(provisioning.settingsEnvVars({ settings: { artist_gate_strict_since: 'whenever' } }, 'X').X_ARTIST_GATE_SINCE, '');
138
+ });
139
+
140
+ t('artistGateStamp writes the cutoff on a RAISE only — no other save may move it (ADR 0241 §5)', () => {
141
+ const now = new Date('2026-09-13T12:00:00.000Z');
142
+ const key = provisioning.ARTIST_GATE_STRICT_SINCE_KEY;
143
+ // advisory → strict is the tightening the stamp exists for.
144
+ assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'advisory' } }, { artist_gate: 'strict' }, now),
145
+ { [key]: '2026-09-13T12:00:00.000Z' });
146
+ // A row with NO stored gate is already 'strict' by default — re-sending 'strict'
147
+ // is not a raise, and stamping it would silently un-hold reviews already held.
148
+ assert.deepEqual(provisioning.artistGateStamp({}, { artist_gate: 'strict' }, now), {});
149
+ // Lowering, and any save that does not touch the gate at all, move nothing.
150
+ assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'strict' } }, { artist_gate: 'off' }, now), {});
151
+ assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'advisory' } }, { joinability: 'open' }, now), {});
152
+ });
153
+
154
+ t('the stamp can neither leak as a setting nor be written by a client patch', () => {
155
+ const row = { settings: { artist_gate: 'strict', artist_gate_strict_since: '2026-09-13T00:00:00.000Z' } };
156
+ assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.SETTINGS_VOCAB));
157
+ assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.PLANET_VOCAB));
158
+ assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.effectiveSettings(row)));
107
159
  });
108
160
 
109
161
  console.log('\nwebEnvBody — the settings ride the real per-instance env file:');
@@ -103,6 +103,18 @@ t('redactBlob SCRUBS a secret shape from a text blob (handled, not leaked)', ()
103
103
  assert.ok(!r.buf.toString('utf8').includes('AKIAABCDEFGHIJKLMNOP'), 'the secret must be scrubbed from the published text');
104
104
  assert.equal(r.secret, false, 'a scrubbed text secret is handled, so it is not a residual leak');
105
105
  });
106
+ // Regression (task 1003873). The secret verdict audits THIS call's own scrub, so it has to
107
+ // share the scrub's knownDocNames allowlist. When it did not, the audit re-redacted the ADR
108
+ // filename the scrub had just spared, read the difference as a leak, and failed the packager
109
+ // closed on every doc in the tree — the publish lane stayed red for seven core releases.
110
+ t('redactBlob reports no secret for an allowlisted doc filename (the gate agrees with its own scrub)', () => {
111
+ const name = '0281-differential-audit-allowlist-symmetry.md';
112
+ const knownDocNames = new Set([name]);
113
+ const r = redactBlob({ buf: Buffer.from(`see [ADR 0281](docs/adr/${name}) for why`), rules, sensitiveLiterals, envSecrets: [], knownDocNames });
114
+ assert.ok(r.buf.toString('utf8').includes(name), 'the allowlisted filename must survive the scrub');
115
+ assert.equal(r.secret, false, 'and must not then be reported as a residual secret');
116
+ assert.deepEqual(r.leaks, []);
117
+ });
106
118
  t('redactBlob FAILS CLOSED on a secret inside a BINARY blob (cannot safely rewrite it)', () => {
107
119
  const bin = Buffer.concat([Buffer.from('BIN\x00'), Buffer.from('AKIA' + 'ABCDEFGHIJKLMNOP')]);
108
120
  const r = redactBlob({ buf: bin, rules, sensitiveLiterals, envSecrets: [] });