@bongos/core 1.19.715 → 1.19.725

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 (429) hide show
  1. package/.bongos-core.json +491 -421
  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 +36 -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/onboarding/approval-broadcast.js +27 -0
  348. package/modules/provisioning/provisioning.js +79 -3
  349. package/modules/provisioning/routes/provisioning.js +6 -1
  350. package/modules/provisioning/tests/provisioning.mjs +8 -8
  351. package/modules/security/secret-scrub.js +40 -4
  352. package/modules/status-ui/public/README.md +1 -1
  353. package/modules/ui-design/docs/design-contract.md +2 -2
  354. package/modules/ui-design/kit/README.md +1 -1
  355. package/modules/ui-design/kit/serve.js +1 -0
  356. package/modules/ui-design/skills/README.md +6 -6
  357. package/modules/ui-design/skills/brandkit/SKILL.md +1 -1
  358. package/modules/ui-design/skills/design-taste-frontend/SKILL.md +1 -1
  359. package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +1 -1
  360. package/modules/ui-design/skills/gpt-taste/SKILL.md +1 -1
  361. package/modules/ui-design/skills/high-end-visual-design/SKILL.md +1 -1
  362. package/modules/ui-design/skills/image-to-code/SKILL.md +1 -1
  363. package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +1 -1
  364. package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +1 -1
  365. package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
  366. package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +1 -1
  367. package/modules/ui-design/skills/minimalist-ui/SKILL.md +1 -1
  368. package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +1 -1
  369. package/modules/ui-design/skills/stitch-design-taste/SKILL.md +1 -1
  370. package/modules/ui-design/skills/style/SKILL.md +2 -2
  371. package/modules/ui-design/styles/README.md +3 -3
  372. package/modules/ui-design/styles/blueprint/DESIGN.md +1 -1
  373. package/modules/ui-design/styles/chrome-world/DESIGN.md +1 -1
  374. package/package-lock.json +2 -2
  375. package/package.json +1 -1
  376. package/scripts/gds/adopt-preflight.js +68 -2
  377. package/scripts/gds/audit-rank-roles.js +172 -0
  378. package/scripts/gds/build-cli-package.js +2 -0
  379. package/scripts/gds/claim.js +94 -9
  380. package/scripts/gds/claude-md-template.js +18 -0
  381. package/scripts/gds/device-flow-help.js +72 -0
  382. package/scripts/gds/fitness.js +8 -8
  383. package/scripts/gds/go-live.js +139 -5
  384. package/scripts/gds/init.js +64 -8
  385. package/scripts/gds/land-watch.js +13 -5
  386. package/scripts/gds/login.js +18 -0
  387. package/scripts/gds/mirror-redact.js +13 -2
  388. package/scripts/gds/package-core.js +11 -3
  389. package/scripts/gds/release.js +55 -0
  390. package/scripts/gds/setup.js +5 -39
  391. package/scripts/gds/ship-land.js +10 -2
  392. package/scripts/gds/ship-regen.js +41 -8
  393. package/scripts/gds/start.js +7 -4
  394. package/scripts/gds/worktree-claim-guard.js +27 -1
  395. package/scripts/public-mirror-export.js +17 -4
  396. package/src/bongos/api-errors.js +9 -0
  397. package/src/bongos/artist-gate.js +204 -0
  398. package/src/bongos/repo-info.js +37 -6
  399. package/src/branding.js +15 -0
  400. package/src/module-api.js +41 -1
  401. package/tests/adopt_preflight.mjs +72 -0
  402. package/tests/api_path_404.mjs +28 -0
  403. package/tests/artist_gate.mjs +234 -0
  404. package/tests/artist_gate_release.mjs +283 -0
  405. package/tests/artist_gate_route.mjs +174 -0
  406. package/tests/bongos_login.mjs +69 -2
  407. package/tests/box_scope_predicate.mjs +117 -0
  408. package/tests/claim_error_surface.mjs +163 -1
  409. package/tests/claude_materialize.mjs +9 -0
  410. package/tests/cli_exit_no_abort.mjs +256 -3
  411. package/tests/currency_label.mjs +11 -4
  412. package/tests/discord_approvals.mjs +45 -0
  413. package/tests/go_live.mjs +144 -0
  414. package/tests/init.mjs +52 -2
  415. package/tests/main_worktree_busy_reason.mjs +167 -0
  416. package/tests/membership_kind_lockstep.mjs +88 -0
  417. package/tests/mirror_redact.mjs +14 -0
  418. package/tests/module_api.mjs +7 -0
  419. package/tests/provision_settings_apply.mjs +13 -5
  420. package/tests/provisioning_settings.mjs +11 -7
  421. package/tests/provisioning_settings_apply.mjs +4 -4
  422. package/tests/provisioning_settings_env.mjs +59 -7
  423. package/tests/public_mirror_export.mjs +12 -0
  424. package/tests/rank_roles_audit.mjs +114 -0
  425. package/tests/repo_info_binding.mjs +82 -0
  426. package/tests/scrubber_corpus.mjs +70 -2
  427. package/tests/setup_device_flow_fallback.mjs +16 -0
  428. package/tests/start_rebase_warning.mjs +50 -0
  429. package/tests/task_field_bounds_match_db.mjs +140 -0
@@ -13,8 +13,10 @@
13
13
  import { strict as assert } from 'node:assert';
14
14
  import { createRequire } from 'node:module';
15
15
 
16
+ import { readFileSync } from 'node:fs';
17
+
16
18
  const require = createRequire(import.meta.url);
17
- const { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan } = require('../scripts/gds/claim.js');
19
+ const { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, worktreeDetectionNote } = require('../scripts/gds/claim.js');
18
20
 
19
21
  let passed = 0;
20
22
  let failed = 0;
@@ -29,6 +31,12 @@ const envelope = (code, details = {}, message = code, status = 409) => ({
29
31
  });
30
32
  const text = (r) => r.lines.join('\n');
31
33
 
34
+ // task 1002592 — populated by the guard test below, then checked against the
35
+ // codes the server can actually refuse a claim with. Declared here so the drift
36
+ // guard reads the SAME array the coverage test exercises rather than a second
37
+ // hand-typed copy that could itself drift.
38
+ const CLAIM_REFUSAL_CODES_UNDER_TEST = [];
39
+
32
40
  // The regression guard: no refusal may ever render a raw object.
33
41
  t('never dumps [object Object] for any known code', () => {
34
42
  const codes = [
@@ -37,8 +45,16 @@ t('never dumps [object Object] for any known code', () => {
37
45
  'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
38
46
  'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
39
47
  'OPEN_BLOCKER', 'GOAL_MEMBERSHIP_REQUIRED',
48
+ // task 1002592: REBASE_REQUIRED was missing from this list for its whole
49
+ // life, which is exactly why its rendering gap survived — the guard that
50
+ // looked like it covered every refusal only covered the ones someone
51
+ // remembered to type. The drift guard at the bottom of this file now checks
52
+ // this array against what the server can actually throw.
53
+ 'REBASE_REQUIRED', 'REWARD_REQUIRED', 'BUILDER_INACTIVE', 'INSUFFICIENT_RANK',
54
+ 'XENOS_RESTRICTED', 'rank_too_low_for_task',
40
55
  'some_unmapped_future_code',
41
56
  ];
57
+ CLAIM_REFUSAL_CODES_UNDER_TEST.push(...codes);
42
58
  for (const c of codes) {
43
59
  const out = text(formatClaimFailure(envelope(c, { active_task_id: 5 }, 'msg'), 12));
44
60
  assert.ok(!out.includes('[object Object]'), `${c} produced a raw object dump`);
@@ -191,5 +207,151 @@ t('claimErrorInfo: reads envelope and flat shapes identically', () => {
191
207
  assert.equal(flat.code, 'X'); assert.equal(flat.d.a, 1);
192
208
  });
193
209
 
210
+ // ---------------------------------------------------------------------------
211
+ // task 1002592 — three measured defects in this surface.
212
+ // ---------------------------------------------------------------------------
213
+
214
+ // 1. REBASE_REQUIRED. ADR 0120 part 3's gate refuses EVERY new claim, so it is
215
+ // the refusal a builder is most stuck behind, and it was the only one with no
216
+ // branch: it printed `Claim failed (409) [REBASE_REQUIRED]: REBASE_REQUIRED.`
217
+ // while a fully-formed remediation menu sat unread in the body.
218
+ const REBASE_HINT = 'Your queue is gated: 1 confirmed task(s) await your rebase (task 1003575). Clear it with /strand-fix N (works on any machine), or rebase the branch and re-ship — /merge-mode only if this machine has a GitHub push credential.';
219
+
220
+ t('REBASE_REQUIRED: names the owed tasks and relays the server hint', () => {
221
+ const out = text(formatClaimFailure(envelope('REBASE_REQUIRED', {
222
+ hint: REBASE_HINT,
223
+ rebaseTasks: [{ id: 1003575, title: 'The artist deploy gate', reason: 'strand:unresolvable_conflict:docs/module-api-changelog.md' }],
224
+ }), 1002592));
225
+ assert.match(out, /task 1003575/, 'must name the owed task id');
226
+ assert.match(out, /The artist deploy gate/, 'must name the owed task title');
227
+ assert.match(out, /strand:unresolvable_conflict/, 'must surface why it was flagged');
228
+ assert.match(out, /strand-fix/, 'must relay the hint, which leads with the universally-available exit');
229
+ // The regression proper: the old output was ONLY the bare code echo.
230
+ assert.ok(!/^Claim failed \(409\) \[REBASE_REQUIRED\]: REBASE_REQUIRED\.$/m.test(out),
231
+ 'must not fall through to the bare code echo');
232
+ });
233
+
234
+ t('REBASE_REQUIRED: still guides when the server sends no hint and no task list', () => {
235
+ const out = text(formatClaimFailure(envelope('REBASE_REQUIRED', {}), 5));
236
+ assert.match(out, /gated/i, 'must say the queue is gated');
237
+ assert.match(out, /strand-fix|builder-release/, 'must name a way out even with an empty body');
238
+ });
239
+
240
+ t('REBASE_REQUIRED: server prose is clipped, flattened and capped before it hits the terminal', () => {
241
+ const out = text(formatClaimFailure(envelope('REBASE_REQUIRED', {
242
+ hint: 'H'.repeat(2000),
243
+ rebaseTasks: Array.from({ length: 50 }, (_, i) => ({
244
+ id: 1000 + i,
245
+ title: `T${'i'.repeat(400)}`,
246
+ reason: 'line oneline two\nline three',
247
+ })),
248
+ }), 5));
249
+ assert.ok(!//.test(out), 'escape sequences must never reach the terminal verbatim');
250
+ assert.ok(!/line one.*\n.*line two/.test(out), 'a multi-line reason must be flattened onto its own single line');
251
+ for (const line of out.split('\n')) {
252
+ assert.ok(line.length <= 620, `a rendered line ran to ${line.length} chars — the cap is not holding`);
253
+ }
254
+ const listed = out.split('\n').filter((l) => /^ {2}- task /.test(l)).length;
255
+ assert.ok(listed > 0 && listed <= 20, `expected a bounded task list, got ${listed} rows`);
256
+ });
257
+
258
+ // 2. The fallback floor. Any code the server sends a hint for must print it,
259
+ // so the NEXT unmapped code cannot be mute the way REBASE_REQUIRED was.
260
+ t('fallback: an unmapped code still prints the server hint when one is supplied', () => {
261
+ const out = text(formatClaimFailure(
262
+ envelope('some_future_gate', { hint: 'Do the thing, then retry.' }, 'nope', 409), 3));
263
+ assert.match(out, /some_future_gate/, 'still names the code');
264
+ assert.match(out, /Do the thing, then retry\./, 'and prints the way forward');
265
+ });
266
+
267
+ t('fallback: no hint means no invented guidance', () => {
268
+ const out = text(formatClaimFailure(envelope('some_future_gate', {}, 'nope', 409), 3));
269
+ assert.match(out, /some_future_gate/);
270
+ assert.ok(!/undefined/.test(out), 'must not print a stray undefined when no hint exists');
271
+ });
272
+
273
+ // 3. The original bug report: a failed worktree DETECTION arrives disguised as a
274
+ // claim CONFLICT. The note must fire when null is WRONG and stay silent when
275
+ // null is the correct answer — that distinction is the whole fix.
276
+ t('worktreeDetectionNote: silent when the worktree WAS named (the refusal is genuine)', () => {
277
+ assert.deepEqual(worktreeDetectionNote({ worktree: 'task-42', isMainCheckout: false }, 42), []);
278
+ });
279
+
280
+ t('worktreeDetectionNote: silent in the MAIN CHECKOUT, where a null name is correct', () => {
281
+ assert.deepEqual(worktreeDetectionNote({ worktree: null, isMainCheckout: true }, 42), [],
282
+ 'blaming path detection here would send the reader down exactly the wrong path');
283
+ });
284
+
285
+ t('worktreeDetectionNote: fires on a LINKED worktree with no name, and offers --worktree', () => {
286
+ const out = worktreeDetectionNote({ worktree: null, isMainCheckout: false }, 42).join('\n');
287
+ assert.match(out, /could not name its worktree/i);
288
+ assert.match(out, /linked worktree, so a name was expected/i, 'must say a name was expected here');
289
+ assert.match(out, /--worktree/, 'must print the explicit fallback');
290
+ assert.match(out, /claim\.js 42 --worktree/, 'the fallback must be runnable as printed');
291
+ assert.match(out, /not a claim conflict/i, 'must correct the misreading the report describes');
292
+ });
293
+
294
+ t('worktreeDetectionNote: fires when the tree kind is UNKNOWN, and hedges rather than asserts', () => {
295
+ const out = worktreeDetectionNote({ worktree: null, isMainCheckout: null }, 42).join('\n');
296
+ assert.match(out, /--worktree/);
297
+ assert.match(out, /not recognisable as the main checkout/i);
298
+ assert.ok(!/linked worktree, so a name was expected/i.test(out),
299
+ 'must not claim this IS a linked worktree when the probe could not tell');
300
+ });
301
+
302
+ t('worktreeDetectionNote: a missing ctx is silent, never a crash', () => {
303
+ assert.deepEqual(worktreeDetectionNote(undefined, 42), []);
304
+ assert.deepEqual(worktreeDetectionNote({}, 42), [], 'an empty ctx knows nothing — say nothing');
305
+ });
306
+
307
+ t('session_already_has_active_claim: carries the detection note only when ctx warrants it', () => {
308
+ const e = envelope('session_already_has_active_claim', { active_task_id: 7, active_task_title: 'Other' });
309
+ const withNote = text(formatClaimFailure(e, 42, { worktree: null, isMainCheckout: false }));
310
+ assert.match(withNote, /--worktree/, 'an unnamed linked worktree must get the note');
311
+ const named = text(formatClaimFailure(e, 42, { worktree: 'task-42', isMainCheckout: false }));
312
+ assert.ok(!/--worktree/.test(named), 'a named worktree means the conflict is real — no note');
313
+ const mainCheckout = text(formatClaimFailure(e, 42, { worktree: null, isMainCheckout: true }));
314
+ assert.ok(!/--worktree/.test(mainCheckout), 'the main checkout must stay silent');
315
+ const noCtx = text(formatClaimFailure(e, 42));
316
+ assert.ok(!/--worktree/.test(noCtx), 'no ctx (every pre-existing caller) must behave as before');
317
+ assert.match(noCtx, /#7/, 'and must still render the original guidance');
318
+ });
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // The drift guard. REBASE_REQUIRED was thrown by the server and absent from the
322
+ // coverage list above for its whole life, which is how a mute refusal survived a
323
+ // test that looked like it covered every refusal. Derive the truth from the
324
+ // server source instead of trusting a hand-typed array.
325
+ //
326
+ // A static scan fails SILENT — finding nothing looks identical to finding no
327
+ // problem — so this asserts the extraction actually worked before it asserts
328
+ // anything about the result.
329
+ // ---------------------------------------------------------------------------
330
+ t('drift guard: every code claimTask can throw is covered above (or consciously excluded)', () => {
331
+ const src = readFileSync(new URL('../modules/lifecycle/db-claims.js', import.meta.url), 'utf8');
332
+ const start = src.indexOf('async function claimTask');
333
+ assert.ok(start !== -1, 'could not find claimTask in db-claims.js — the guard is reading the wrong file');
334
+ const thrown = new Set(
335
+ [...src.slice(start).matchAll(/throw \{\s*code: '([A-Za-z_]+)'/g)].map((m) => m[1])
336
+ );
337
+
338
+ // Fail loudly on a scan that found nothing, and on one that lost its canary.
339
+ assert.ok(thrown.size >= 8, `extracted only ${thrown.size} codes — the regex has drifted off the source shape`);
340
+ assert.ok(thrown.has('REBASE_REQUIRED'), 'canary missing: the extraction is not seeing real throws');
341
+ assert.ok(thrown.has('TASK_NOT_FOUND'), 'canary missing: the extraction is not seeing real throws');
342
+
343
+ // Codes that exist on the claim path but can never reach the single-claim CLI.
344
+ // Listed explicitly so adding a server code forces a decision rather than
345
+ // silently widening this exemption.
346
+ const NOT_A_SINGLE_CLAIM_REFUSAL = new Set([
347
+ 'BATCH_EMPTY', 'BATCH_DUPLICATE_IDS', 'BATCH_FAILED', // the hall's bulk-claim route
348
+ ]);
349
+
350
+ const covered = new Set(CLAIM_REFUSAL_CODES_UNDER_TEST);
351
+ const missing = [...thrown].filter((c) => !covered.has(c) && !NOT_A_SINGLE_CLAIM_REFUSAL.has(c));
352
+ assert.deepEqual(missing, [],
353
+ `these server codes are refusable but untested here — add them to the codes[] array above (or to NOT_A_SINGLE_CLAIM_REFUSAL with a reason): ${missing.join(', ')}`);
354
+ });
355
+
194
356
  console.log(`\nclaim_error_surface: ${passed} passed, ${failed} failed`);
195
357
  process.exit(failed === 0 ? 0 : 1);
@@ -510,6 +510,15 @@ t('renderInstanceClaudeMd: brand-substituted — names the instance + its hall/c
510
510
  assert.ok(!md.toLowerCase().includes('amazonprimea') && !md.includes('~/.config/otb'), 'no founder literal in the starter');
511
511
  });
512
512
 
513
+ t('renderInstanceClaudeMd: the first-run bootstrap precedes every bongos instruction', () => {
514
+ const md = tmpl.renderInstanceClaudeMd({ identity: { productName: 'Acme' } });
515
+ const iNpm = md.indexOf('npm install');
516
+ const iStart = md.indexOf('bongos start');
517
+ assert.ok(iNpm > 0, 'names npm install at all — without it a fresh clone has no bongos command');
518
+ assert.ok(iStart > iNpm, 'npm install comes BEFORE the first bongos instruction, or the agent hits command not found');
519
+ assert.ok(/@bongos\/core/.test(md), 'explains WHY: the CLI ships inside the dependency');
520
+ });
521
+
513
522
  t('renderInstanceClaudeMd: degrades cleanly on a minimal branding (no world dup, defaults currency)', () => {
514
523
  const md = tmpl.renderInstanceClaudeMd({ identity: { productName: 'Solo', worldName: 'Solo' } });
515
524
  assert.ok(md.startsWith('# Solo — Project Memory'));
@@ -32,6 +32,7 @@ import assert from 'node:assert/strict';
32
32
  import { test } from 'node:test';
33
33
  import { spawnSync, spawn } from 'node:child_process';
34
34
  import fs from 'node:fs';
35
+ import os from 'node:os';
35
36
  import path from 'node:path';
36
37
  import { createRequire } from 'node:module';
37
38
  import { fileURLToPath } from 'node:url';
@@ -40,6 +41,10 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
40
41
  const API = path.join(ROOT, 'scripts', 'gds', 'api.js');
41
42
  const MAIN_AUDIT = path.join(ROOT, 'scripts', 'gds', 'main-audit.js');
42
43
  const CLI_LIB = path.join(ROOT, 'scripts', 'gds', 'cli-lib.js');
44
+ // task 1002592 — the next two CLIs found ABORTING, converted under task 1002734's
45
+ // standing rule (evidence, not a blanket sweep).
46
+ const CLAIM = path.join(ROOT, 'scripts', 'gds', 'claim.js');
47
+ const LAND_WATCH = path.join(ROOT, 'scripts', 'gds', 'land-watch.js');
43
48
  const DEAD_BASE = 'http://127.0.0.1:9'; // discard port — connection refused, instantly
44
49
 
45
50
  // Point a CLI at a base URL. DERIVED, not hardcoded: instance-config resolves
@@ -47,8 +52,14 @@ const DEAD_BASE = 'http://127.0.0.1:9'; // discard port — connection refused,
47
52
  // literal here would (a) add to the legacy-GDS ratchet the rename is retiring and
48
53
  // (b) silently stop steering the CLI on any instance whose brand pack sets a
49
54
  // different prefix — the test would then hit the REAL API instead of the stub.
50
- const { envPrefix } = createRequire(import.meta.url)(path.join(ROOT, 'src', 'instance-config.js'));
55
+ const ic = createRequire(import.meta.url)(path.join(ROOT, 'src', 'instance-config.js'));
56
+ const { envPrefix } = ic;
51
57
  const API_BASE_ENV = `${envPrefix()}_API_BASE`;
58
+ // Derived for the same reason the base env is: a literal would stop steering the
59
+ // CLI on any instance whose brand pack renames the config dir, and the test would
60
+ // then read the operator's REAL session instead of the throwaway one.
61
+ const CONFIG_DIR_NAME = ic.configDirName();
62
+ const SESSION_FILENAME = ic.SESSION_FILENAMES[0];
52
63
  const atBase = (url) => ({ [API_BASE_ENV]: url });
53
64
 
54
65
  // The native abort prints this and nothing else can be relied on; it is NOT a
@@ -104,8 +115,79 @@ const s = http.createServer((q, r) => {
104
115
  s.listen(0, '127.0.0.1', () => console.log('PORT=' + s.address().port));
105
116
  `;
106
117
 
107
- function startStub() {
108
- const proc = spawn(process.execPath, ['-e', STUB_SRC], { stdio: ['ignore', 'pipe', 'ignore'] });
118
+ // task 1002592 — claim.js needs a stub that ROUTES: the claim POST decides which
119
+ // path is under test, while every other call it makes (learnings, recall, /me, the
120
+ // task fetch land-watch does) is best-effort and only has to answer something
121
+ // parseable. `status` picks the path: 409 serves the real REBASE_REQUIRED envelope
122
+ // so the refusal rendering is proved end-to-end, 201 serves a claim so the heavier
123
+ // success teardown runs.
124
+ const claimStubSrc = (status) => `
125
+ const http = require('node:http');
126
+ const REFUSAL = ${JSON.stringify({
127
+ error: {
128
+ code: 'REBASE_REQUIRED',
129
+ message: 'REBASE_REQUIRED',
130
+ details: {
131
+ hint: 'Your queue is gated: 1 confirmed task(s) await your rebase (task 1003575). Clear it with /strand-fix N (works on any machine).',
132
+ rebaseTasks: [{ id: 1003575, title: 'Stub owed task', reason: 'strand:unresolvable_conflict:stub.md' }],
133
+ },
134
+ },
135
+ })};
136
+ const CLAIMED = ${JSON.stringify({
137
+ claim: { id: 9001 },
138
+ task: {
139
+ id: 4242, title: 'Stub task', version_id: 'BONGOS-V2', touches: [],
140
+ credits_reward: 0, priority: 1, description: '', parallel_safe: false,
141
+ },
142
+ reward: { mode: 'cost-plus-only', perTaskCredit: false },
143
+ })};
144
+ const s = http.createServer((q, r) => {
145
+ let body = '';
146
+ q.on('data', (c) => { body += c; });
147
+ q.on('end', () => {
148
+ const isClaim = q.method === 'POST' && /\\/claims$/.test(q.url.split('?')[0]);
149
+ const code = isClaim ? ${status} : 200;
150
+ const payload = isClaim ? (${status} === 201 ? CLAIMED : REFUSAL) : {};
151
+ r.writeHead(code, { 'Content-Type': 'application/json' });
152
+ r.end(JSON.stringify(payload));
153
+ });
154
+ });
155
+ s.listen(0, '127.0.0.1', () => console.log('PORT=' + s.address().port));
156
+ `;
157
+
158
+ // task 1002592 — claim.js and land-watch.js read a SESSION and (for land-watch) a
159
+ // breadcrumb directory, both under the config home, which instance-config derives
160
+ // from os.homedir(). Node resolves that from HOME / USERPROFILE, so overriding both
161
+ // gives each test a throwaway config tree: the developer's real session is never
162
+ // read, their real breadcrumb dir is never written, and a test can plant exactly
163
+ // the state it needs. Without this, planting a breadcrumb would mean writing into
164
+ // the operator's own ~/.config and racing whatever land-watch is genuinely tracking.
165
+ function makeHome({ apiBase = DEAD_BASE, breadcrumbTaskId = null } = {}) {
166
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-exit-home-'));
167
+ const cfg = path.join(home, '.config', CONFIG_DIR_NAME);
168
+ fs.mkdirSync(cfg, { recursive: true });
169
+ fs.writeFileSync(
170
+ path.join(cfg, SESSION_FILENAME),
171
+ JSON.stringify({ token: 'stub-token-not-a-real-credential', api_base: apiBase }),
172
+ 'utf8'
173
+ );
174
+ if (breadcrumbTaskId) {
175
+ const pending = path.join(cfg, 'pending-land');
176
+ fs.mkdirSync(pending, { recursive: true });
177
+ fs.writeFileSync(
178
+ path.join(pending, `${breadcrumbTaskId}.json`),
179
+ JSON.stringify({ taskId: String(breadcrumbTaskId), at: new Date().toISOString() }),
180
+ 'utf8'
181
+ );
182
+ }
183
+ return home;
184
+ }
185
+
186
+ const isolatedHome = (home) => ({ HOME: home, USERPROFILE: home });
187
+
188
+ function startStub(opts = {}) {
189
+ const src = opts.claimStatus ? claimStubSrc(opts.claimStatus) : STUB_SRC;
190
+ const proc = spawn(process.execPath, ['-e', src], { stdio: ['ignore', 'pipe', 'ignore'] });
109
191
  return new Promise((resolve, reject) => {
110
192
  let buf = '';
111
193
  const timer = setTimeout(() => { proc.kill(); reject(new Error('stub server did not report a port')); }, 10_000);
@@ -152,6 +234,177 @@ test('api.js routes its post-request exits through cliExit, not process.exit', (
152
234
  assert.match(afterRequest, /cliExit\(/, 'the post-request paths must set the exit code via cliExit');
153
235
  });
154
236
 
237
+ // ---- claim.js + land-watch.js (task 1002592) -------------------------------
238
+ //
239
+ // WHAT EACH KIND OF TEST BELOW ACTUALLY GUARDS — measured, not assumed, because
240
+ // claiming otherwise is how a suite comes to look like coverage it does not have.
241
+ // With the hard exits deliberately restored in both scripts, the STATIC checks
242
+ // went red and every behavioural test below stayed GREEN, including the ones that
243
+ // make a real HTTP round trip to the local stub. That reproduces, for these two
244
+ // CLIs, what task 1003772 found for main-audit.js: a localhost stub tears down too
245
+ // little machinery to trip the abort. The aborts these tests exist for were all
246
+ // measured against the REAL API over TLS.
247
+ //
248
+ // So: the static checks are the net for the ABORT. The behavioural tests are still
249
+ // worth their runtime — they pin the exit CODES callers branch on (claim 0 vs 1,
250
+ // land-watch 0 on every path), they prove the scripts TERMINATE rather than hang,
251
+ // which is the documented conversion trap, and the refusal case proves the
252
+ // REBASE_REQUIRED rendering reaches a terminal end-to-end.
253
+ // Both were MEASURED aborting 3/3 on Windows 11 / Node v24.19.0 before the
254
+ // conversion, and 5/5 clean after — claim.js on its refusal path (exit 127 where
255
+ // it computed 1) and land-watch.js on its network path (exit 127 where it always
256
+ // intends 0). Task 1002734 deliberately left the wider CLI surface alone and set
257
+ // the bar at "found ABORTING, not merely fetching-then-exiting"; these two cleared
258
+ // it. Neither file arms a timer, so the documented conversion trap — a hard exit
259
+ // masking a leak, turning the crash into a HANG — does not apply here; the
260
+ // behavioural runs below still assert termination rather than assuming it.
261
+
262
+ test('claim.js routes its post-request exits through cliExit, not process.exit', () => {
263
+ const src = fs.readFileSync(CLAIM, 'utf8');
264
+ // The pre-request guards stay as hard exits, exactly as api.js kept its two:
265
+ // the worktree guard and the usage banner both run before any network work.
266
+ // Everything from the claim POST onward must drain instead.
267
+ // Anchor on the CLAIM request itself, not on `cliClient()` — the client is also
268
+ // built inside best-effort helpers defined near the top of the file, so that
269
+ // string would slice from the wrong place and quietly measure nothing.
270
+ const marker = 'let r = await postClaim();';
271
+ const i = src.indexOf(marker);
272
+ assert.ok(i !== -1, 'could not find the claim request site in claim.js — this guard is reading the wrong shape');
273
+ assert.equal(src.split(marker).length - 1, 1, 'the anchor must be unique or the slice is arbitrary');
274
+ const afterRequest = src.slice(i).replace(/\/\/[^\n]*/g, '');
275
+ assert.ok(
276
+ !/process\.exit\s*\(/.test(afterRequest),
277
+ 'no process.exit() may remain after the claim request is issued — that is what aborts node on Windows '
278
+ + 'and turns a refusal\'s exit 1 into 127'
279
+ );
280
+ assert.match(afterRequest, /cliExit\(/, 'the post-request paths must set the exit code via cliExit');
281
+ });
282
+
283
+ test('claim.js keeps its PRE-request guards as hard exits', () => {
284
+ const src = fs.readFileSync(CLAIM, 'utf8');
285
+ const beforeRequest = src.slice(0, src.indexOf('let r = await postClaim();'));
286
+ assert.match(
287
+ beforeRequest, /process\.exit\s*\(/,
288
+ 'the usage + worktree guards run before any network work; converting them buys nothing '
289
+ + 'and a cliExit there would let execution continue past a refusal'
290
+ );
291
+ });
292
+
293
+ test('land-watch.js routes every exit through cliExit, not process.exit', () => {
294
+ const src = fs.readFileSync(LAND_WATCH, 'utf8');
295
+ const stripped = src.replace(/\/\/[^\n]*/g, '');
296
+ assert.ok(
297
+ !/process\.exit\s*\(/.test(stripped),
298
+ 'no process.exit() may remain in land-watch.js — it is HOOK-INVOKED and exits 0 on every path '
299
+ + 'on purpose, so the abort made the one script written never to fail always report 127'
300
+ );
301
+ assert.match(src, /cliExit\(/, 'the exit paths must set the exit code via cliExit');
302
+ });
303
+
304
+ test('land-watch.js exits 0 and TERMINATES with no pending lands (fast path, no network)', () => {
305
+ const r = spawnSync(process.execPath, [LAND_WATCH], {
306
+ cwd: ROOT,
307
+ encoding: 'utf8',
308
+ timeout: 60_000, // dropping a hard exit trades a crash for a HANG if a handle leaks
309
+ env: { ...process.env, ...atBase(DEAD_BASE), ...isolatedHome(makeHome()) },
310
+ });
311
+ assert.notEqual(r.signal, 'SIGTERM', 'timed out — draining left a handle armed');
312
+ assert.doesNotMatch(r.stderr || '', ABORT_RE, `native abort on the land-watch path:\n${r.stderr}`);
313
+ assert.equal(r.status, 0, `land-watch must exit 0 on every path, got ${r.status} (127 = the abort)`);
314
+ });
315
+
316
+ // THE PATH THAT ACTUALLY ABORTED. The fast path above returns before any network
317
+ // work, so on its own it proves nothing about this bug: the measured 3/3 aborts
318
+ // were on the NETWORK path (breadcrumb present → watch() → fetchTask() → a real
319
+ // round trip → exit). Plant a breadcrumb in an isolated HOME and serve a real 200
320
+ // so the full teardown runs, exactly as api.js's stub-server test does.
321
+ test('land-watch.js exits 0 after a REAL request — the teardown that aborted 3/3', async () => {
322
+ const { proc, port } = await startStub();
323
+ try {
324
+ for (let i = 0; i < 3; i++) {
325
+ // classifyLand drops a non-shipped breadcrumb after one pass, so re-plant it.
326
+ const home = makeHome({ apiBase: `http://127.0.0.1:${port}`, breadcrumbTaskId: '4242' });
327
+ const r = spawnSync(process.execPath, [LAND_WATCH], {
328
+ cwd: ROOT,
329
+ encoding: 'utf8',
330
+ timeout: 60_000,
331
+ env: { ...process.env, ...atBase(`http://127.0.0.1:${port}`), ...isolatedHome(home) },
332
+ });
333
+ assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — draining left a handle armed`);
334
+ assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively on the network path:\n${r.stderr}`);
335
+ assert.equal(r.status, 0, `run ${i + 1} exited ${r.status} (127 = the abort); stderr: ${r.stderr}`);
336
+ // Prove the run really took the network path rather than the idle fast-path,
337
+ // or this test would silently degrade into the one above.
338
+ assert.doesNotMatch(r.stderr || '', /no pending lands/,
339
+ `run ${i + 1} took the idle fast path — the breadcrumb was not seen, so the network teardown was never exercised`);
340
+ }
341
+ } finally {
342
+ proc.kill();
343
+ }
344
+ });
345
+
346
+ // claim.js. Its exits are post-request and it was measured aborting 3/3 on the
347
+ // refusal path. Both paths below run against the stub in an isolated HOME with a
348
+ // throwaway session, so no real API is touched and the developer's own config is
349
+ // never read or written. `--here` because CI may check out the MAIN checkout,
350
+ // where the worktree guard would refuse at exit 2 before any request is made.
351
+ test('claim.js exits 1 without aborting on a REFUSED claim — measured 3/3 aborting before', async () => {
352
+ const { proc, port } = await startStub({ claimStatus: 409 });
353
+ try {
354
+ const home = makeHome({ apiBase: `http://127.0.0.1:${port}` });
355
+ for (let i = 0; i < 3; i++) {
356
+ const r = spawnSync(process.execPath, [CLAIM, '4242', '--here'], {
357
+ cwd: ROOT,
358
+ encoding: 'utf8',
359
+ timeout: 60_000,
360
+ env: { ...process.env, ...atBase(`http://127.0.0.1:${port}`), ...isolatedHome(home) },
361
+ });
362
+ assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — draining left a handle armed`);
363
+ assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively:\n${r.stderr}`);
364
+ assert.equal(r.status, 1, `a refused claim must exit 1, got ${r.status} (127 = the abort); stderr: ${r.stderr}`);
365
+ // End-to-end proof that the new REBASE_REQUIRED rendering reaches a terminal,
366
+ // not just the unit test: the stub refuses with that exact envelope.
367
+ //
368
+ // Assert on wording only the BRANCH produces. Matching the hint text alone
369
+ // would pass with the branch disabled, because the generic fallback also
370
+ // prints d.hint now — measured, not assumed: a probe that disabled the branch
371
+ // kept this test green until these two assertions replaced it.
372
+ assert.match(r.stderr, /queue is gated/i, 'the REBASE_REQUIRED branch must render, not the bare fallback');
373
+ assert.match(r.stderr, /flagged: strand:unresolvable_conflict/, 'the branch must surface why the task was flagged');
374
+ assert.match(r.stderr, /strand-fix/, 'the server hint must reach the builder, not be dropped on the floor');
375
+ assert.match(r.stderr, /task 1003575/, 'the owed task must be named');
376
+ }
377
+ } finally {
378
+ proc.kill();
379
+ }
380
+ });
381
+
382
+ // THE OPEN QUESTION THE TASK FLAGGED, now answered rather than assumed: a
383
+ // SUCCESSFUL claim leaves main() by `return` with no explicit exit, so it was never
384
+ // covered by the refusal measurement above. It is also the heavier teardown — the
385
+ // success path makes several further best-effort calls after the claim — which is
386
+ // exactly the shape that aborted for api.js. Measured here: clean, exit 0.
387
+ test('claim.js exits 0 without aborting on a SUCCESSFUL claim — the heavier teardown', async () => {
388
+ const { proc, port } = await startStub({ claimStatus: 201 });
389
+ try {
390
+ const home = makeHome({ apiBase: `http://127.0.0.1:${port}` });
391
+ for (let i = 0; i < 3; i++) {
392
+ const r = spawnSync(process.execPath, [CLAIM, '4242', '--here'], {
393
+ cwd: ROOT,
394
+ encoding: 'utf8',
395
+ timeout: 60_000,
396
+ env: { ...process.env, ...atBase(`http://127.0.0.1:${port}`), ...isolatedHome(home) },
397
+ });
398
+ assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — draining left a handle armed`);
399
+ assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively on the success path:\n${r.stderr}`);
400
+ assert.equal(r.status, 0, `a successful claim must exit 0, got ${r.status} (127 = the abort); stderr: ${r.stderr}`);
401
+ assert.match(r.stdout, /Claimed task/, 'the success output must still be produced');
402
+ }
403
+ } finally {
404
+ proc.kill();
405
+ }
406
+ });
407
+
155
408
  // ---- main-audit.js — the deploy gate (task 1003772) ------------------------
156
409
  // Ranges are always HEAD..HEAD: it resolves on every checkout including a shallow
157
410
  // CI clone, audits zero commits, and still runs the WHOLE pipeline — fetchRankMap
@@ -51,11 +51,14 @@ test('clientBranding exposes project policy — the settings read-back (task 100
51
51
  // `visibility` — the join door — joined the block in task 1002324 (ADR 0182 D1);
52
52
  // `joinGrant` — what a PUBLIC join grants — in task 1002331 (ADR 0247), defaulting
53
53
  // to `full`, the value that narrows nothing, so an un-pushed instance is unchanged
54
+ // `artistGate` joined in task 1003575 (ADR 0241) and is the ONE key here whose
55
+ // fallback is not today's behaviour: 'strict' is the owner's ruling, safe as a
56
+ // default because it can only ever delay a pin move, never a builder's ship.
54
57
  assert.deepEqual(clientBranding({}).project,
55
- { platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full' });
58
+ { platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict' });
56
59
  assert.deepEqual(
57
- clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view' } }).project,
58
- { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view' },
60
+ clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' } }).project,
61
+ { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' },
59
62
  'what the instance actually resolved (env override included) is what it publishes');
60
63
  });
61
64
 
@@ -74,7 +77,11 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
74
77
  // `project` joined the allow-list in task 1003140: the per-project policy is public
75
78
  // by nature and is the settings read-back a manage page needs (see clientBranding).
76
79
  assert.deepEqual(Object.keys(projected).sort(), ['copy', 'currency', 'identity', 'project', 'rankLabels', 'reward', 'theme']);
77
- assert.deepEqual(Object.keys(projected.project).sort(), ['joinGrant', 'joinability', 'platformVisibility', 'visibility']);
80
+ // `artistGate` joined in task 1003575 (ADR 0241) the project's artist strictness,
81
+ // public for the same read-back reason as its neighbours. Its companion stamp
82
+ // (artistGateSince) deliberately does NOT: that is platform bookkeeping the server
83
+ // reads, and no browser surface has a question it answers.
84
+ assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'visibility']);
78
85
  for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
79
86
  assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
80
87
  }
@@ -170,5 +170,50 @@ await t('a DB error returns {error}, never throws', async () => {
170
170
  assert.equal(r.outcome, 'error');
171
171
  });
172
172
 
173
+ // ---- the outbound half must not outlive the inbound half (task 1003400) ----
174
+ //
175
+ // The 👍 this line invites is applied ONLY by discord-approvals.js, which lives in the
176
+ // `discord` module. onboarding (which posts) is default-ON; discord is default-OFF; and
177
+ // nothing gated one on the other. A default instance therefore posted a working-looking
178
+ // approve/decline control into a channel where no handler was listening — an Archon
179
+ // reacts, sees it land, and believes they admitted a builder who is still locked out.
180
+
181
+ await t('discord module OFF → the control is NOT posted', async () => {
182
+ let posted = false;
183
+ const r = await bcast.postMemberApprovalRequest(
184
+ { id: 7, github_login: 'carol' },
185
+ { postWebhook: async () => { posted = true; }, log: () => {}, isModuleEnabled: () => false },
186
+ );
187
+ assert.equal(posted, false, 'a 👍 nobody applies must never be invited');
188
+ assert.equal(r.skipped, 'discord_module_off');
189
+ assert.equal(r.posted, false);
190
+ });
191
+
192
+ await t('discord module ON → the control posts exactly as before', async () => {
193
+ let line = null;
194
+ const r = await bcast.postMemberApprovalRequest(
195
+ { id: 7, github_login: 'carol' },
196
+ { postWebhook: async (_ch, l) => { line = l; }, log: () => {}, isModuleEnabled: () => true },
197
+ );
198
+ assert.equal(r.posted, true, 'the working path is untouched');
199
+ assert.deepEqual(ap.parseApprovalMarker(line), { kind: 'member', id: 7 }, 'and still round-trips');
200
+ });
201
+
202
+ await t('enablement unresolvable → fails CLOSED (no post)', async () => {
203
+ let posted = false;
204
+ const r = await bcast.postMemberApprovalRequest(
205
+ { id: 7, github_login: 'carol' },
206
+ { postWebhook: async () => { posted = true; }, log: () => {}, isModuleEnabled: () => { throw new Error('config unreadable'); } },
207
+ );
208
+ assert.equal(posted, false, 'a state we cannot vouch for is not a state we post into');
209
+ assert.equal(r.skipped, 'discord_module_off');
210
+ });
211
+
212
+ await t('the gate is reachable through the published doorway, not a direct core require', async () => {
213
+ const doorway = require('../src/module-api.js');
214
+ assert.equal(typeof doorway.isModuleEnabled, 'function', 'ADR 0083: modules read enablement here');
215
+ assert.throws(() => doorway.isModuleEnabled('no-such-module'), /unknown module/, 'a typo must be a bug, not a silent false');
216
+ });
217
+
173
218
  console.log(`\n${passed} passed, ${failed} failed`);
174
219
  process.exit(failed ? 1 : 0);