@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
@@ -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
@@ -0,0 +1,94 @@
1
+ // tests/credit_log_single_writer.mjs
2
+ //
3
+ // Task 1003641 — the balance is moved by the TRIGGER, and by nothing else.
4
+ //
5
+ // THE BUG. scripts/gds/backfill-task-rewards.js applyPlan() did both: INSERT INTO
6
+ // credit_log (which fires trg_credit_log_apply AFTER INSERT) *and* a separate
7
+ // `UPDATE builders SET total_credits = total_credits + $1`. The trigger had already
8
+ // applied that delta, so every credit the script minted landed twice. A real
9
+ // financial-ledger bug, and the kind that is invisible in review because each half
10
+ // looks correct on its own.
11
+ //
12
+ // WHY THIS FILE IS NOT JUST A TEST OF THAT ONE LINE. The invariant is repo-wide:
13
+ // exactly one place applies a credit_log delta to builders.total_credits. Every
14
+ // other writer already relied on the trigger; only the backfill script double-wrote.
15
+ // So the durable guard is "no credit_log writer also updates the balance", which
16
+ // catches the NEXT script someone writes as well as this one.
17
+ //
18
+ // The existing tests/backfill_task_rewards.mjs never referenced applyPlan,
19
+ // total_credits or the trigger at all, which is how this survived. Source
20
+ // assertions, because the DB-free gate cannot run SQL; runtime behaviour is proven
21
+ // against real Postgres at deploy.
22
+ //
23
+ // Run: node tests/credit_log_single_writer.mjs
24
+
25
+ import { strict as assert } from 'node:assert';
26
+ import { test } from 'node:test';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
33
+ // Comments carry the rationale; strip them before asserting on the executable body
34
+ // so a comment ABOUT the bug can never satisfy an assertion against it.
35
+ const code = (src) => src
36
+ .replace(/^\s*\/\/.*$/gm, '')
37
+ .replace(/\/\*[\s\S]*?\*\//g, '');
38
+
39
+ // Every file in the repo that inserts a credit_log row.
40
+ const WRITERS = [
41
+ ['scripts', 'gds', 'backfill-task-rewards.js'],
42
+ ['scripts', 'gds', 'backfill-subagent-rewards.js'],
43
+ ['modules', 'economy', 'achievements.js'],
44
+ ['modules', 'economy', 'credits.js'],
45
+ ['modules', 'lifecycle', 'db-rank-authz.js'],
46
+ ['src', 'bongos', 'db.js'],
47
+ ];
48
+
49
+ test('the trigger is the ONE place a credit_log delta reaches total_credits', () => {
50
+ const mig = read('migrations', '003_pms.sql');
51
+ assert.match(mig, /CREATE OR REPLACE FUNCTION pms_apply_credit\(\)/);
52
+ assert.match(mig, /SET total_credits = total_credits \+ NEW\.delta/,
53
+ 'the trigger applies the delta');
54
+ assert.match(mig, /AFTER INSERT ON credit_log/,
55
+ 'on insert, so any INSERT has already moved the balance');
56
+ });
57
+
58
+ test('no credit_log writer ALSO updates builders.total_credits', () => {
59
+ // The invariant. A writer that does both double-credits, silently.
60
+ const offenders = [];
61
+ for (const parts of WRITERS) {
62
+ const src = code(read(...parts));
63
+ if (!/INSERT INTO credit_log/i.test(src)) continue; // moved on; not our business
64
+ // Match an ASSIGNMENT, not a mention. `UPDATE builders SET rank = $2 ...
65
+ // RETURNING id, ..., total_credits, ...` READS the column and must not trip
66
+ // this — two files do exactly that, and an earlier draft of this test flagged
67
+ // both as double-writers. The two shapes that actually move a balance are the
68
+ // self-referential increment and a direct set.
69
+ const writesBalance = /\btotal_credits\s*=\s*total_credits/i.test(src)
70
+ || /SET\s+total_credits\s*=/i.test(src);
71
+ if (writesBalance) offenders.push(parts.join('/'));
72
+ }
73
+ assert.deepEqual(offenders, [],
74
+ `these insert a credit_log row AND move the balance themselves, so every credit lands twice: ${offenders.join(', ')}`);
75
+ });
76
+
77
+ test('backfill-task-rewards.js specifically — the regression that cost real credits', () => {
78
+ const src = read('scripts', 'gds', 'backfill-task-rewards.js');
79
+ assert.match(src, /INSERT INTO credit_log/, 'it still mints the audit row');
80
+ assert.doesNotMatch(code(src), /\btotal_credits\s*=/i,
81
+ 'and must never assign total_credits — the trigger owns the balance');
82
+ // The reason WHY must survive too, or the next reader re-adds it.
83
+ assert.match(src, /trg_credit_log_apply applies the delta/,
84
+ 'the call site explains why there is no balance update here');
85
+ });
86
+
87
+ test('the idempotency key still guards re-runs', () => {
88
+ // Removing the double-write must not disturb the other half of this script's
89
+ // safety: a second run of the same backfill must mint nothing.
90
+ const src = code(read('scripts', 'gds', 'backfill-task-rewards.js'));
91
+ assert.match(src, /SELECT 1 FROM credit_log WHERE task_id = \$1 AND reason = \$2/,
92
+ 'the per-task idempotency re-check inside the transaction');
93
+ assert.match(src, /BACKFILL_REASON/, 'keyed on the backfill reason');
94
+ });
@@ -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
  }
package/tests/go_live.mjs CHANGED
@@ -25,6 +25,7 @@
25
25
  import { test } from 'node:test';
26
26
  import assert from 'node:assert/strict';
27
27
  import { createRequire } from 'node:module';
28
+ import fs from 'node:fs';
28
29
  import path from 'node:path';
29
30
  import { fileURLToPath } from 'node:url';
30
31
 
@@ -109,6 +110,149 @@ test('an undetermined divergence halts and does not fall through to a bump', ()
109
110
  assert.equal(p.from, null, 'must not invent a from-version');
110
111
  });
111
112
 
113
+ // ---------------------------------------------------------------------------
114
+ // The artist gate (ADR 0241, task 1003575)
115
+ //
116
+ // This is the one halt that comes from a PERSON'S queue rather than from git, so
117
+ // what these pin is the boundary around it: it stops a PIN MOVE, it names its own
118
+ // remedy, and it never fires on anything but a definite 'strict'-with-open-reviews
119
+ // answer. A gate that halted on "I could not tell" would brick deploys on an
120
+ // unrelated outage with no task id to release against — the blocked-state-that-
121
+ // does-not-explain-itself failure ADR 0241 §4 was written against.
122
+ // ---------------------------------------------------------------------------
123
+
124
+ // The shape readArtistGate() resolves to, as the live instance reports it.
125
+ const gate = (over = {}) => ({ mode: 'strict', since: null, holds: false, reviews: [], halt: null, ...over });
126
+ const heldGate = (n = 1) => gate({
127
+ holds: true,
128
+ reviews: Array.from({ length: n }, (_, i) => ({ id: String(9000 + i), title: 'Artist review: …' })),
129
+ halt: {
130
+ code: 'ARTIST_REVIEW_UNRESOLVED',
131
+ message: `${n} unresolved artist review${n === 1 ? '' : 's'} hold this deploy — the project runs artist_gate 'strict'`,
132
+ detail: 'Held by task 9000. Two ways out … POST /tasks/:id/override-request …',
133
+ },
134
+ });
135
+
136
+ test('an unresolved artist review HALTS the pin move', () => {
137
+ const p = G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306', artistGate: heldGate(2) });
138
+ const h = p.halts.find((x) => x.code === 'ARTIST_REVIEW_UNRESOLVED');
139
+ assert.ok(h, 'expected ARTIST_REVIEW_UNRESOLVED');
140
+ // The refusal has to carry its own way out, or it is the stall ADR 0162 retired.
141
+ assert.match(h.detail, /override-request/);
142
+ });
143
+
144
+ test('a gate that holds nothing halts nothing — at any level', () => {
145
+ for (const mode of ['off', 'advisory', 'strict']) {
146
+ const p = G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306', artistGate: gate({ mode }) });
147
+ assert.equal(p.halts.length, 0, `${mode}: ${JSON.stringify(p.halts)}`);
148
+ }
149
+ });
150
+
151
+ test('an UNREAD gate is a note, never a halt — and never renders as "nothing holds"', () => {
152
+ // No session, or an instance too old to serve the route: we do not know the
153
+ // gate applies at all, and halting every deploy on that would brick the pin
154
+ // with no task id to release against.
155
+ const p = G.planGoLive({
156
+ div: div(), target: okTarget, targetVersion: '1.19.306',
157
+ artistGate: { unread: 'this instance does not serve /artist-gate — it predates ADR 0241' },
158
+ });
159
+ assert.equal(p.halts.length, 0, 'not knowing must not brick the pin');
160
+ const n = p.notes.find((x) => x.code === 'ARTIST_GATE_UNREAD');
161
+ assert.ok(n, 'but it must be REPORTED — "could not tell" is not "nothing holds"');
162
+ assert.match(n.message, /may be holding/, 'the wording must not claim the gate is clear');
163
+ });
164
+
165
+ test('a gate that is THERE and BROKEN halts — the fail-closed half', () => {
166
+ // The other side of the line: the route exists and 500s. We know the gate
167
+ // applies and cannot get an answer from it, so deploying past would be a
168
+ // quieter way through than the owner's own override — the asymmetry to refuse.
169
+ const p = G.planGoLive({
170
+ div: div(), target: okTarget, targetVersion: '1.19.306',
171
+ artistGate: { broken: 'GET /artist-gate returned 500' },
172
+ });
173
+ const h = p.halts.find((x) => x.code === 'ARTIST_GATE_UNREADABLE');
174
+ assert.ok(h, 'expected ARTIST_GATE_UNREADABLE');
175
+ assert.match(h.detail, /fault on the instance/, 'and it must point at the instance, not at an artist');
176
+ assert.equal(p.notes.find((x) => x.code === 'ARTIST_GATE_UNREAD'), undefined, 'one verdict, not two');
177
+ });
178
+
179
+ test('artistGateEntries is the shared composer, so the gitless lane cannot skip the gate', () => {
180
+ // The on-box lane builds its plan without planGoLive; it reaches the same halt
181
+ // through this, which is what stops "no git checkout" from meaning "no gate".
182
+ assert.equal(G.artistGateEntries(heldGate()).halts[0].code, 'ARTIST_REVIEW_UNRESOLVED');
183
+ assert.deepEqual(G.artistGateEntries(null), { halts: [], notes: [] });
184
+ assert.equal(G.artistGateEntries({ unread: 'no session' }).notes[0].code, 'ARTIST_GATE_UNREAD');
185
+ });
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // readArtistGate — the network leg.
189
+ //
190
+ // This is tested rather than left to the file's ssh/spawnSync boundary because a
191
+ // mistake here is INVISIBLE: cli-lib's apiCall composes `${origin}${path}`, so a
192
+ // path missing its `/api/bongos` prefix lands on the origin's catch-all and the
193
+ // gate reports itself unread on every instance — including the ones that fully
194
+ // support it — while the plan reads exactly like a clean run. It shipped that way
195
+ // once. The URL is pinned against the route's REAL mount point in the generated
196
+ // OpenAPI spec, so a rename on either side reds this rather than going quiet.
197
+ // ---------------------------------------------------------------------------
198
+
199
+ test('the path it requests is the one the server actually mounts', () => {
200
+ // The spec's paths are relative to its server base, and apiCall prepends
201
+ // nothing — so the URL go-live builds must be base + path, assembled from the
202
+ // spec rather than retyped. A rename on either side reds this.
203
+ const spec = JSON.parse(fs.readFileSync(new URL('../docs/api/openapi.json', import.meta.url), 'utf8'));
204
+ const bases = (spec.servers || []).map((s) => s.url);
205
+ const full = new Set(bases.flatMap((b) => Object.keys(spec.paths || {}).map((p) => `${b}${p}`)));
206
+ assert.ok(full.has(G.ARTIST_GATE_PATH),
207
+ `go-live requests ${G.ARTIST_GATE_PATH}, which the API spec does not serve — it would hit the origin's catch-all and read as "unread" forever`);
208
+ assert.match(G.ARTIST_GATE_PATH, /^\/api\//, 'apiCall does not prepend an API prefix — the path must carry it');
209
+ });
210
+
211
+ test('a real answer is parsed; a 200 that is NOT this route reads as unread, never as "nothing holds"', async () => {
212
+ const seen = [];
213
+ const call = async (method, path, body, opts) => { seen.push({ method, path, opts }); return call._res; };
214
+
215
+ call._res = { status: 200, ok: true, data: { mode: 'strict', since: null, holds: true, reviews: [{ id: '9001' }], halt: { message: 'm', detail: 'd' } } };
216
+ const got = await G.readArtistGate(call);
217
+ assert.deepEqual(seen[0].method, 'GET');
218
+ assert.equal(seen[0].path, G.ARTIST_GATE_PATH);
219
+ assert.equal(got.mode, 'strict');
220
+ assert.ok(got.halt, 'a real halt survives the read');
221
+
222
+ // An SPA index.html from a catch-all, or a proxy's own page: 200, no gate. The
223
+ // one failure that would wave a deploy through in silence.
224
+ call._res = { status: 200, ok: true, data: { raw: '<!doctype html><html>…' } };
225
+ const spa = await G.readArtistGate(call);
226
+ assert.ok(spa.unread, 'a 200 from something else is NOT a gate that holds nothing');
227
+ assert.equal(spa.halt, undefined);
228
+ });
229
+
230
+ test('every failure shape resolves — 404 and a lost connection are notes, a 5xx is the halt', async () => {
231
+ const res = (r) => async () => r;
232
+ assert.match((await G.readArtistGate(res({ status: 404, ok: false, data: {} }))).unread, /predates ADR 0241/);
233
+ assert.match((await G.readArtistGate(res({ status: 401, ok: false, data: {} }))).unread, /401/);
234
+ assert.match((await G.readArtistGate(res({ status: 500, ok: false, data: {} }))).broken, /500/);
235
+ assert.match((await G.readArtistGate(res({ status: 503, ok: false, data: {} }))).broken, /503/);
236
+ // It must never throw: a go-live that crashes on a gate read is worse than one
237
+ // that halts on it.
238
+ const thrower = async () => { throw new Error('ECONNREFUSED'); };
239
+ assert.match((await G.readArtistGate(thrower)).unread, /ECONNREFUSED/);
240
+ });
241
+
242
+ test('the level vocabulary go-live checks against is the gate\'s own', async () => {
243
+ const gate = await import('../src/bongos/artist-gate.js');
244
+ assert.deepEqual([...G.ARTIST_GATE_LEVELS].sort(), [...gate.ARTIST_GATE_LEVELS].sort(),
245
+ 'the mirrored list must not drift from src/bongos/artist-gate.js');
246
+ });
247
+
248
+ test('the plan SHOWS the gate level even when nothing is held', () => {
249
+ const out = G.renderPlan(
250
+ { ...G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306', artistGate: gate({ mode: 'advisory' }) }), artistGate: gate({ mode: 'advisory' }) },
251
+ { targetName: 'web' }
252
+ );
253
+ assert.match(out, /artist gate\s+advisory/, '"why did nothing stop?" must be answerable from the plan');
254
+ });
255
+
112
256
  // ---------------------------------------------------------------------------
113
257
  // Shell safety — this builds a command that runs as root over ssh
114
258
  // ---------------------------------------------------------------------------
package/tests/helpers.mjs CHANGED
@@ -330,6 +330,14 @@ export function makeEl(tagName, attrs = {}) {
330
330
  while (node) { if (matches(node, sel)) return node; node = node.parentNode; }
331
331
  return null;
332
332
  },
333
+ // Node.contains — true for the node itself and any descendant. A DELEGATED
334
+ // listener guards on it (`!root.contains(el)`), so without this a test that
335
+ // dispatches on the section throws instead of exercising the handler.
336
+ contains(node) {
337
+ let n = node;
338
+ while (n) { if (n === this) return true; n = n.parentNode; }
339
+ return false;
340
+ },
333
341
  addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); },
334
342
  focus() {},
335
343
  fire(type, event) { (this.listeners[type] || []).forEach((fn) => fn(event)); },