@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.
- package/.bongos-core.json +491 -421
- package/.claude/skills/backlog-review/SKILL.md +3 -3
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/blocker-solve/SKILL.md +2 -2
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +2 -2
- package/.claude/skills/feedback/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/goal-review/SKILL.md +2 -2
- package/.claude/skills/idea-triage/SKILL.md +5 -5
- package/.claude/skills/merge-mode/SKILL.md +4 -4
- package/.claude/skills/new-project/SKILL.md +17 -17
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/recall/SKILL.md +2 -2
- package/.claude/skills/strand-fix/SKILL.md +1 -1
- package/.devcontainer/README.md +2 -2
- package/CONTRIBUTING.md +2 -2
- package/GOVERNANCE.md +4 -4
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/config/branding.neutral.json +4 -2
- package/docs/adr/0001-stack-choice-single-vps.md +2 -2
- package/docs/adr/0002-digitalocean-over-hetzner.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0009-autotile-architecture.md +1 -1
- package/docs/adr/0011-terrain-id-rendering-model.md +3 -3
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +1 -1
- package/docs/adr/0014-families-canonical-over-vocabulary.md +1 -1
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +1 -1
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +1 -1
- package/docs/adr/0018-three-rank-model-goes-live.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +9 -9
- package/docs/adr/0021-per-builder-skill-model-preferences.md +2 -2
- package/docs/adr/0022-secrets-policy.md +5 -5
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +3 -3
- package/docs/adr/0024-multi-agent-system-architecture.md +10 -10
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +1 -1
- package/docs/adr/0025-structured-criterion-task-link.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +13 -13
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +14 -14
- package/docs/adr/0029-status-page-outage-resilience.md +1 -1
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +7 -7
- package/docs/adr/0032-discord-mirror-of-gds.md +3 -3
- package/docs/adr/0033-discord-bot-service-principal.md +4 -4
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0035-builder-onboarding-three-paths.md +4 -4
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +1 -1
- package/docs/adr/0037-discord-channel-management.md +1 -1
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +5 -5
- package/docs/adr/0039-setup-first-onboarding-ux.md +6 -6
- package/docs/adr/0040-remote-control-default-browser-onramp.md +3 -3
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +2 -2
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +5 -5
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +2 -2
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0046-sandbox-first-review-gate.md +1 -1
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +1 -1
- package/docs/adr/0049-split-parallel-safety-contract.md +1 -1
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +2 -2
- package/docs/adr/0051-full-session-transcript-corpus.md +2 -2
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +1 -1
- package/docs/adr/0053-scoped-dev-box-session.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +7 -7
- package/docs/adr/0055-server-mediated-branch-publish.md +2 -2
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +3 -3
- package/docs/adr/0057-container-cost-ledger.md +1 -1
- package/docs/adr/0057-discord-archon-approval-channels.md +2 -2
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +6 -6
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +11 -11
- package/docs/adr/0060-gds-retrieval-layer.md +3 -3
- package/docs/adr/0061-context-layer-decomposition.md +2 -2
- package/docs/adr/0062-medusa-instance-model-decoupling.md +5 -5
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +2 -2
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +3 -3
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +5 -5
- package/docs/adr/0066-generated-file-map-sections.md +2 -2
- package/docs/adr/0067-session-start-fast-forward-pull.md +1 -1
- package/docs/adr/0068-shared-art-key-for-newcomers.md +1 -1
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +3 -3
- package/docs/adr/0070-deterministic-session-card-delivery.md +1 -1
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +2 -2
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +2 -2
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +2 -2
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +3 -3
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +2 -2
- package/docs/adr/0076-in-session-deterministic-compaction.md +2 -2
- package/docs/adr/0077-content-addressed-llm-cache.md +2 -2
- package/docs/adr/0078-deterministic-first-cron.md +3 -3
- package/docs/adr/0079-value-gated-autonomous-engagement.md +2 -2
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +3 -3
- package/docs/adr/0081-tool-agnostic-design-layer.md +3 -3
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +2 -2
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +3 -3
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +2 -2
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +15 -15
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +5 -5
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +1 -1
- package/docs/adr/0088-consistent-character-generative-animation.md +5 -5
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -15
- package/docs/adr/0090-metic-task-authoring.md +8 -8
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +3 -3
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +11 -11
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +5 -5
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +1 -1
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +6 -6
- package/docs/adr/0095-cross-agent-context-management.md +5 -5
- package/docs/adr/0096-require-reward-before-workable.md +5 -5
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +2 -2
- package/docs/adr/0097-retroactive-reward-backfill.md +4 -4
- package/docs/adr/0098-public-mirror-publish-manifest.md +3 -3
- package/docs/adr/0099-delayed-redacted-mirror-export.md +1 -1
- package/docs/adr/0100-two-repo-instance-model.md +6 -6
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +9 -9
- package/docs/adr/0102-finish-bounding-the-kernel.md +1 -1
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +6 -6
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +3 -3
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +3 -3
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +2 -2
- package/docs/adr/0106-goal-owner-manager-authority.md +2 -2
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +6 -6
- package/docs/adr/0108-instance-composition-configurable-root.md +6 -6
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +3 -3
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +1 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +18 -18
- package/docs/adr/0112-private-goals-invitations-join-requests.md +7 -7
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +1 -1
- package/docs/adr/0114-instance-docs-discovery-contract.md +4 -4
- package/docs/adr/0115-scheduled-routines-portability.md +1 -1
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +1 -1
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +2 -2
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +1 -1
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +3 -3
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +1 -1
- package/docs/adr/0124-typed-api-response-bodies.md +2 -2
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +2 -2
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +3 -3
- package/docs/adr/0127-cross-box-fleet-observability.md +1 -1
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +3 -3
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +4 -4
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +4 -4
- package/docs/adr/0131-rank-scoped-skill-visibility.md +2 -2
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +2 -2
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +1 -1
- package/docs/adr/0134-private-first-npm-distribution.md +3 -3
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +1 -1
- package/docs/adr/0136-update-channel-subscription-policy.md +2 -2
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +6 -6
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +4 -4
- package/docs/adr/0142-project-context-is-prose-only.md +2 -2
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +2 -2
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +2 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +2 -2
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +4 -4
- package/docs/adr/0147-standalone-live-docs-regen.md +1 -1
- package/docs/adr/0149-bongos-dev-local-launcher.md +1 -1
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +3 -3
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +20 -20
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +2 -2
- package/docs/adr/0152-metic-task-abandonment.md +7 -7
- package/docs/adr/0153-sequential-chain-runner.md +1 -1
- package/docs/adr/0154-goal-owner-criterion-authoring.md +7 -7
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +2 -2
- package/docs/adr/0156-user-set-display-name-wins.md +1 -1
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +10 -10
- package/docs/adr/0158-choke-point-verdict-taxonomy.md +1 -1
- package/docs/adr/0159-merge-gate-reconciliation.md +1 -1
- package/docs/adr/0160-main-audit-repo-verifiable-caller.md +1 -1
- package/docs/adr/0161-publish-on-merge.md +2 -2
- package/docs/adr/0163-legacy-display-name-resolution.md +1 -1
- package/docs/adr/0165-instances-hookless-by-default.md +1 -1
- package/docs/adr/0166-module-maintenance-and-deprecation-policy.md +3 -3
- package/docs/adr/0167-gate-trust-link-agpl-default.md +2 -2
- package/docs/adr/0167-module-catalog-source-vs-provenance.md +3 -3
- package/docs/adr/0169-bongos-serve-server-bring-up.md +1 -1
- package/docs/adr/0171-profile-rollup-join-and-consent.md +1 -1
- package/docs/adr/0172-editable-rank-roles-substrate-axis.md +2 -2
- package/docs/adr/0172-per-craft-compensation-ideator-credit-lane.md +4 -4
- package/docs/adr/0173-legacy-multi-ship-shortfall-not-backfilled.md +1 -1
- package/docs/adr/0174-government-terminology-ranks-not-roles.md +1 -1
- package/docs/adr/0177-goal-membership-kind.md +3 -3
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +1 -1
- package/docs/adr/0179-board-membership-predicate-grammar.md +1 -1
- package/docs/adr/0181-teardown-state-gate-and-address-retention.md +1 -1
- package/docs/adr/0182-project-visibility-two-axes.md +1 -1
- package/docs/adr/0183-criteria-close-themselves.md +1 -1
- package/docs/adr/0184-ship-requires-an-assurance.md +1 -1
- package/docs/adr/0185-spark-handoff-credit-split.md +1 -1
- package/docs/adr/0186-per-project-idea-templates.md +1 -1
- package/docs/adr/0187-collab-four-decisions.md +1 -1
- package/docs/adr/0187-oauth-handshake-cookie-lifetime.md +1 -1
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +2 -2
- package/docs/adr/0191-majority-and-the-author-who-may-not-decide-alone.md +1 -1
- package/docs/adr/0192-a-category-orients-and-authorises-nothing.md +2 -2
- package/docs/adr/0193-pause-task-scoped-box-slices.md +1 -1
- package/docs/adr/0195-adr-numbers-are-checked-like-migration-numbers.md +2 -2
- package/docs/adr/0196-chrome-world-token-contract-and-the-landing-sheets.md +2 -2
- package/docs/adr/0197-the-ui-design-module.md +1 -1
- package/docs/adr/0198-third-party-skill-vendoring-policy.md +1 -1
- package/docs/adr/0203-doorway-changelog-out-of-module-api.md +2 -2
- package/docs/adr/0204-the-apex-is-dark-only.md +2 -2
- package/docs/adr/0205-federated-checkin-needs-the-hubs-own-signin-witness.md +1 -1
- package/docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md +1 -1
- package/docs/adr/0207-connections-implementation-departures-and-honest-limits.md +1 -1
- package/docs/adr/0208-application-lifecycle-the-handle-waits-and-the-vouch-has-no-minter.md +2 -2
- package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +1 -1
- package/docs/adr/0210-scouting-graduates-on-an-instance-asserted-owner-claim.md +1 -1
- package/docs/adr/0212-connections-on-the-profile-surface-and-addressing.md +1 -1
- package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +2 -2
- package/docs/adr/0215-the-recruiter-sliver-is-a-ceiling-not-a-step.md +6 -6
- package/docs/adr/0216-a-fake-pool-interpreter-must-fail-loudly-not-silently-ignore.md +2 -2
- package/docs/adr/0217-rank-gates-inviting-not-viewing.md +1 -1
- package/docs/adr/0218-the-apex-root-web-surface.md +2 -2
- package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +1 -1
- package/docs/adr/0220-an-in-house-rebuilt-skill-is-a-first-party-skill.md +1 -1
- package/docs/adr/0221-the-impeccable-method-is-one-skill-with-sub-command-playbooks.md +2 -2
- package/docs/adr/0222-the-image-family-under-the-hero-only-rule.md +2 -2
- package/docs/adr/0223-the-detector-is-two-tiers-in-the-kit.md +3 -3
- package/docs/adr/0224-module-skills-reach-the-core-checkout-as-untracked-self-ignored-copies.md +1 -1
- package/docs/adr/0225-privacy-suppresses-cross-project-surfaces-only.md +1 -1
- package/docs/adr/0226-one-stale-timer-reporting-degrades-enforcement-refuses.md +1 -1
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +1 -1
- package/docs/adr/0230-a-look-skill-is-a-composition-grammar-the-palette-is-the-pack.md +1 -1
- package/docs/adr/0231-the-style-session-an-owner-authors-a-look-inside-the-contract.md +2 -2
- package/docs/adr/0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md +2 -2
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +2 -2
- package/docs/adr/0234-idea-routing-capture-time-promotion-landing-matrix-homeless-inbox.md +2 -2
- package/docs/adr/0235-a-fix-category-is-kindbug-under-a-goal-no-column-required.md +11 -11
- package/docs/adr/0236-many-ideas-one-task-the-lineage-key-is-the-pair.md +5 -5
- package/docs/adr/0237-a-starter-bundle-is-a-preset-over-the-always-on-core-keyed-by-declared-type.md +5 -5
- package/docs/adr/0238-the-join-door-is-a-provisioning-settings-policy-key.md +1 -1
- package/docs/adr/0239-a-recommendation-is-a-table-that-carries-its-own-reason.md +4 -4
- package/docs/adr/0240-a-skipped-module-picker-stores-nothing-and-resolves-the-bundle-on-read.md +4 -4
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +1 -1
- package/docs/adr/0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md +2 -2
- package/docs/adr/0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md +4 -4
- package/docs/adr/0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md +3 -3
- package/docs/adr/0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md +7 -7
- package/docs/adr/0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md +1 -1
- package/docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md +2 -2
- package/docs/adr/0248-a-hub-project-s-owner-is-the-provisioning-row-carried-on-a-port.md +3 -3
- package/docs/adr/0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md +3 -3
- package/docs/adr/0250-strict-versioning-the-version-boundary-is-the-scope-gate.md +6 -6
- package/docs/adr/0251-a-suggestion-of-who-to-invite-is-a-recruiting-surface-and-owes-the-opt-out.md +5 -5
- package/docs/adr/0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md +19 -19
- package/docs/adr/0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md +2 -2
- package/docs/adr/0254-a-relayed-link-is-contained-downward-or-it-is-not-published.md +6 -6
- package/docs/adr/0255-a-public-lists-ordering-is-part-of-its-payload.md +2 -2
- package/docs/adr/0256-the-visibility-ceiling-is-the-sign-in-gate-too-and-the-reader-moves-to-the-composer.md +3 -3
- package/docs/adr/0257-auth-resolves-before-the-hall-mounts-anything.md +1 -1
- package/docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md +1 -1
- package/docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md +7 -7
- package/docs/adr/0260-the-application-is-the-consent-and-the-echo-is-the-gate.md +4 -4
- package/docs/adr/0261-a-preselect-always-carries-a-reason-the-bundle-summary-is-the-floor.md +1 -1
- package/docs/adr/0262-a-bug-never-lands-in-the-inbox.md +1 -1
- package/docs/adr/0263-how-a-version-closes.md +3 -3
- package/docs/adr/0264-the-ten-working-areas.md +3 -3
- package/docs/adr/0265-work-with-no-goal-and-the-order-the-cut-needs.md +5 -5
- package/docs/adr/0266-the-board-room-is-its-own-surface-reachable-by-whoever-may-vote.md +2 -2
- package/docs/adr/0267-unanimity-and-the-revise-and-re-sit-loop.md +1 -1
- package/docs/adr/0268-the-constitution-comes-from-two-roots.md +3 -3
- package/docs/adr/0269-the-cli-session-store-is-host-keyed-at-a-fixed-anchor.md +1 -1
- package/docs/adr/0270-rank-floor-derives-without-a-prediction.md +2 -2
- package/docs/adr/0271-the-ideator-surface-is-two-halves.md +3 -3
- package/docs/adr/0272-a-craft-is-an-identity-not-a-toolchain.md +4 -4
- package/docs/adr/0273-hall-nav-can-gate-on-a-permission.md +1 -1
- package/docs/adr/0274-one-kernel-three-role-packs.md +1 -1
- package/docs/adr/0275-one-source-for-a-written-role-responsibility.md +1 -1
- package/docs/adr/0276-the-skill-listing-budget-cannot-hold-every-trigger.md +1 -1
- package/docs/adr/0277-a-box-is-in-use-only-while-a-human-is-attached.md +1 -1
- package/docs/adr/0278-a-gated-project-still-takes-applications.md +2 -2
- package/docs/adr/0279-an-upgrade-is-proven-by-the-served-version-not-the-health-check.md +1 -1
- package/docs/adr/0280-the-unattended-lane-may-drive-a-co-tenant-because-the-roster-is-the-decision.md +1 -1
- package/docs/adr/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +2 -2
- package/docs/adr/README.md +294 -294
- package/docs/api/openapi.json +72 -5
- package/docs/api-reference.md +8 -2
- package/docs/architecture.md +22 -22
- package/docs/branding-contract.md +5 -5
- package/docs/canonical-permissions.md +22 -22
- package/docs/design/hall-direction-v2.md +3 -3
- package/docs/design/landing-page-direction-v2.md +1 -1
- package/docs/design/projects-hub-direction-v2.md +1 -1
- package/docs/file-map.md +3 -1
- package/docs/module-api-changelog.md +36 -1
- package/docs/modules-contract.md +25 -25
- package/docs/onboarding/browser-terminal-guide.md +1 -1
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/drachmae-and-karma.md +13 -13
- package/docs/onboarding/primer.md +7 -7
- package/docs/packs/artist.md +1 -1
- package/docs/packs/ideator.md +1 -1
- package/docs/project-context.template.md +3 -3
- package/docs/recipes/builder-box-lifecycle.md +5 -5
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/core-release-pipeline.md +55 -14
- package/docs/recipes/figma-design-loop.md +1 -1
- package/docs/recipes/gds-db-backup.md +2 -2
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/instance-service-restart.md +3 -3
- package/docs/recipes/local-dev.md +2 -2
- package/docs/recipes/managed-settings-remote-control.md +3 -3
- package/docs/recipes/memory-backup.md +4 -4
- package/docs/recipes/memory-sync-conflicts.md +2 -2
- package/docs/recipes/multi-builder-merge.md +2 -2
- package/docs/recipes/ops-gotchas.md +4 -4
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +6 -6
- package/docs/recipes/private-npm-distribution.md +5 -5
- package/docs/recipes/search-index-ingestion.md +3 -3
- package/docs/recipes/self-host.md +5 -5
- package/docs/recipes/server-bring-up.md +1 -1
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +1 -1
- package/docs/recipes/ui-look-before-you-ship.md +1 -1
- package/docs/recipes/upgrading-the-core.md +2 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +6 -6
- package/docs/research/cursor-competitive-analysis.md +3 -3
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +7 -7
- package/migrations/core_240_artist_gate_indexes.sql +48 -0
- package/migrations/core_240_goal_members_drop_legacy_role.sql +79 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/lifecycle/cascade.js +37 -9
- package/modules/lifecycle/db-claims.js +5 -2
- package/modules/lifecycle/db-goals.js +4 -3
- package/modules/lifecycle/db-overrides.js +117 -4
- package/modules/lifecycle/db-tasks.js +63 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +12 -4
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/pg-constraint-fail.js +49 -0
- package/modules/lifecycle/routes/artist-gate.js +101 -0
- package/modules/lifecycle/routes/claims.js +13 -1
- package/modules/lifecycle/routes/lifecycle.js +6 -0
- package/modules/lifecycle/routes/task-write-routes.js +7 -0
- package/modules/lifecycle/routes/tasks.js +24 -0
- package/modules/lifecycle/task-classifier.js +10 -1
- package/modules/lifecycle/tests/cascade.mjs +43 -0
- package/modules/onboarding/approval-broadcast.js +27 -0
- package/modules/provisioning/provisioning.js +79 -3
- package/modules/provisioning/routes/provisioning.js +6 -1
- package/modules/provisioning/tests/provisioning.mjs +8 -8
- package/modules/security/secret-scrub.js +40 -4
- package/modules/status-ui/public/README.md +1 -1
- package/modules/ui-design/docs/design-contract.md +2 -2
- package/modules/ui-design/kit/README.md +1 -1
- package/modules/ui-design/kit/serve.js +1 -0
- package/modules/ui-design/skills/README.md +6 -6
- package/modules/ui-design/skills/brandkit/SKILL.md +1 -1
- package/modules/ui-design/skills/design-taste-frontend/SKILL.md +1 -1
- package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +1 -1
- package/modules/ui-design/skills/gpt-taste/SKILL.md +1 -1
- package/modules/ui-design/skills/high-end-visual-design/SKILL.md +1 -1
- package/modules/ui-design/skills/image-to-code/SKILL.md +1 -1
- package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +1 -1
- package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +1 -1
- package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
- package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +1 -1
- package/modules/ui-design/skills/minimalist-ui/SKILL.md +1 -1
- package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +1 -1
- package/modules/ui-design/skills/stitch-design-taste/SKILL.md +1 -1
- package/modules/ui-design/skills/style/SKILL.md +2 -2
- package/modules/ui-design/styles/README.md +3 -3
- package/modules/ui-design/styles/blueprint/DESIGN.md +1 -1
- package/modules/ui-design/styles/chrome-world/DESIGN.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/adopt-preflight.js +68 -2
- package/scripts/gds/audit-rank-roles.js +172 -0
- package/scripts/gds/build-cli-package.js +2 -0
- package/scripts/gds/claim.js +94 -9
- package/scripts/gds/claude-md-template.js +18 -0
- package/scripts/gds/device-flow-help.js +72 -0
- package/scripts/gds/fitness.js +8 -8
- package/scripts/gds/go-live.js +139 -5
- package/scripts/gds/init.js +64 -8
- package/scripts/gds/land-watch.js +13 -5
- package/scripts/gds/login.js +18 -0
- package/scripts/gds/mirror-redact.js +13 -2
- package/scripts/gds/package-core.js +11 -3
- package/scripts/gds/release.js +55 -0
- package/scripts/gds/setup.js +5 -39
- package/scripts/gds/ship-land.js +10 -2
- package/scripts/gds/ship-regen.js +41 -8
- package/scripts/gds/start.js +7 -4
- package/scripts/gds/worktree-claim-guard.js +27 -1
- package/scripts/public-mirror-export.js +17 -4
- package/src/bongos/api-errors.js +9 -0
- package/src/bongos/artist-gate.js +204 -0
- package/src/bongos/repo-info.js +37 -6
- package/src/branding.js +15 -0
- package/src/module-api.js +41 -1
- package/tests/adopt_preflight.mjs +72 -0
- package/tests/api_path_404.mjs +28 -0
- package/tests/artist_gate.mjs +234 -0
- package/tests/artist_gate_release.mjs +283 -0
- package/tests/artist_gate_route.mjs +174 -0
- package/tests/bongos_login.mjs +69 -2
- package/tests/box_scope_predicate.mjs +117 -0
- package/tests/claim_error_surface.mjs +163 -1
- package/tests/claude_materialize.mjs +9 -0
- package/tests/cli_exit_no_abort.mjs +256 -3
- package/tests/currency_label.mjs +11 -4
- package/tests/discord_approvals.mjs +45 -0
- package/tests/go_live.mjs +144 -0
- package/tests/init.mjs +52 -2
- package/tests/main_worktree_busy_reason.mjs +167 -0
- package/tests/membership_kind_lockstep.mjs +88 -0
- package/tests/mirror_redact.mjs +14 -0
- package/tests/module_api.mjs +7 -0
- package/tests/provision_settings_apply.mjs +13 -5
- package/tests/provisioning_settings.mjs +11 -7
- package/tests/provisioning_settings_apply.mjs +4 -4
- package/tests/provisioning_settings_env.mjs +59 -7
- package/tests/public_mirror_export.mjs +12 -0
- package/tests/rank_roles_audit.mjs +114 -0
- package/tests/repo_info_binding.mjs +82 -0
- package/tests/scrubber_corpus.mjs +70 -2
- package/tests/setup_device_flow_fallback.mjs +16 -0
- package/tests/start_rebase_warning.mjs +50 -0
- package/tests/task_field_bounds_match_db.mjs +140 -0
|
@@ -164,7 +164,7 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
164
164
|
reset();
|
|
165
165
|
const r = await get('/provisioning/instances/18/settings');
|
|
166
166
|
assert.equal(r.status, 200, r.raw);
|
|
167
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' });
|
|
167
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
168
168
|
// the planet rides beside the settings (task 1003347): no default — null = the platform's own pick
|
|
169
169
|
assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null, card_backdrop: null });
|
|
170
170
|
assert.deepEqual(r.body.options, {
|
|
@@ -175,6 +175,10 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
|
|
|
175
175
|
// what a PUBLIC join grants (task 1002331, ADR 0247) — the fourth policy
|
|
176
176
|
// axis, offered to the form the same way
|
|
177
177
|
join_grant: ['view', 'apply', 'full'],
|
|
178
|
+
// the artist gate (task 1003575, ADR 0241) — the fifth policy axis. It is
|
|
179
|
+
// offered to the form like the rest; what makes it different is only its
|
|
180
|
+
// default, which is the owner's ruling rather than today's behaviour.
|
|
181
|
+
artist_gate: ['off', 'advisory', 'strict'],
|
|
178
182
|
planet_template: [...provisioning.PLANET_TEMPLATES], planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
|
|
179
183
|
// the card's backdrop (task 1003352) is the same kind of key: platform-drawn,
|
|
180
184
|
// closed list, no default, no push — so it rides the same vocab
|
|
@@ -187,14 +191,14 @@ await ta('a stored planet reads back; a name that left the vocabulary reads null
|
|
|
187
191
|
const r = await get('/provisioning/instances/18/settings');
|
|
188
192
|
assert.equal(r.status, 200, r.raw);
|
|
189
193
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: null, card_backdrop: null });
|
|
190
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the planet is not a setting: the policy read is untouched');
|
|
194
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the planet is not a setting: the policy read is untouched');
|
|
191
195
|
});
|
|
192
196
|
|
|
193
197
|
await ta('stored keys overlay the defaults', async () => {
|
|
194
198
|
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { joinability: 'invite_only' } } });
|
|
195
199
|
const r = await get('/provisioning/instances/18/settings');
|
|
196
200
|
assert.equal(r.status, 200, r.raw);
|
|
197
|
-
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full' });
|
|
201
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
198
202
|
});
|
|
199
203
|
|
|
200
204
|
await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
|
|
@@ -222,7 +226,7 @@ await ta('owner changes one key — merged, returned effective, audited api:self
|
|
|
222
226
|
reset();
|
|
223
227
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
224
228
|
assert.equal(r.status, 200, r.raw);
|
|
225
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
229
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
|
|
226
230
|
assert.equal(r.body.push.queued, true, 'an active co-tenant gets the live push (task 1003140)');
|
|
227
231
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { joinability: 'open' } }], 'exactly the sent key, nothing else');
|
|
228
232
|
assert.equal(events.length, 1);
|
|
@@ -235,7 +239,7 @@ await ta('owner changes both keys in one call', async () => {
|
|
|
235
239
|
reset();
|
|
236
240
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'invite_only' });
|
|
237
241
|
assert.equal(r.status, 200, r.raw);
|
|
238
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full' });
|
|
242
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
239
243
|
});
|
|
240
244
|
|
|
241
245
|
await ta('the OWN-SCOPE WALL: another builder\'s PATCH → 404 and the write path is never reached', async () => {
|
|
@@ -274,7 +278,7 @@ await ta('the owner sets the door to stealth — stored, echoed effective, audit
|
|
|
274
278
|
reset();
|
|
275
279
|
const r = await patch('/provisioning/instances/18/settings', { visibility: 'stealth' });
|
|
276
280
|
assert.equal(r.status, 200, r.raw);
|
|
277
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full' });
|
|
281
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full', artist_gate: 'strict' });
|
|
278
282
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { visibility: 'stealth' } }], 'exactly the sent key, nothing else');
|
|
279
283
|
assert.equal(events[0].actor, 'api:self');
|
|
280
284
|
assert.match(events[0].detail, /visibility=stealth/);
|
|
@@ -334,7 +338,7 @@ await ta('the owner picks a planet: saved, echoed as `planet`, audited — and N
|
|
|
334
338
|
const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
|
|
335
339
|
assert.equal(r.status, 200, r.raw);
|
|
336
340
|
assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: 'sea', card_backdrop: null });
|
|
337
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the policy half is untouched');
|
|
341
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the policy half is untouched');
|
|
338
342
|
assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }]);
|
|
339
343
|
assert.equal(r.body.push.queued, false);
|
|
340
344
|
assert.equal(r.body.push.restart, false, 'an active co-tenant would get a push for a POLICY change — never for the planet');
|
|
@@ -225,7 +225,7 @@ await ta('ACTIVE standalone: the row is written FIRST, then a settings-apply int
|
|
|
225
225
|
reset();
|
|
226
226
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
|
|
227
227
|
assert.equal(r.status, 200, r.raw);
|
|
228
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
228
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
|
|
229
229
|
assert.equal(r.body.settings, undefined);
|
|
230
230
|
assert.deepEqual(updateCalls.map((c) => c.patch), [{ platform_visibility: 'gated' }]);
|
|
231
231
|
assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the runner reads the row — the write must precede the intent');
|
|
@@ -274,7 +274,7 @@ await ta('an enqueue that THROWS still answers 200 "saved" — never a 500 that
|
|
|
274
274
|
enqueueThrows = new Error('db down');
|
|
275
275
|
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
276
276
|
assert.equal(r.status, 200, r.raw);
|
|
277
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full' });
|
|
277
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
278
278
|
assert.equal(updateCalls.length, 1, 'the write landed');
|
|
279
279
|
receiptShape(r.body.push, { queued: false, restart: true, reachable: true });
|
|
280
280
|
assert.match(r.body.push.message, /Saved, but the restart could not be queued/);
|
|
@@ -287,7 +287,7 @@ await ta('same values as already stored → no write, no event, no intent, "No c
|
|
|
287
287
|
reset({ inst: { ...ACTIVE, settings: { platform_visibility: 'gated', joinability: 'open' } } });
|
|
288
288
|
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'open' });
|
|
289
289
|
assert.equal(r.status, 200, r.raw);
|
|
290
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full' });
|
|
290
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
|
|
291
291
|
assert.equal(updateCalls.length, 0, 'nothing written (no updated_at reshuffle)');
|
|
292
292
|
assert.equal(events.length, 0, 'no "settings changed" event for a change that is not one');
|
|
293
293
|
assert.equal(enqueueCalls.length, 0, 'a whole-form save must not bounce a live project');
|
|
@@ -404,7 +404,7 @@ await ta('save A queues → save B 409s while A RUNS → A settles → the UNCHA
|
|
|
404
404
|
assert.equal(enqueueCalls.length, enqueuedBefore + 1, 'exactly one new intent');
|
|
405
405
|
assert.equal(updateCalls.length, writesBefore, 'nothing re-written — the value was already stored in step 2');
|
|
406
406
|
assert.deepEqual(owedCalls[owedCalls.length - 1], { id: 18, owed: false }, 'and the debt is settled');
|
|
407
|
-
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full' },
|
|
407
|
+
assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' },
|
|
408
408
|
'the value that finally ships is the one the owner saved');
|
|
409
409
|
});
|
|
410
410
|
|
|
@@ -59,8 +59,17 @@ t('a row with NO stored settings emits today\'s behavior EXPLICITLY, never nothi
|
|
|
59
59
|
assert.equal(v.CLOUDBONGOS_VISIBILITY, 'public');
|
|
60
60
|
// what a public join grants rides it too (task 1002331, ADR 0182 D6)
|
|
61
61
|
assert.equal(v.CLOUDBONGOS_JOIN_GRANT, 'full');
|
|
62
|
+
// The artist gate is the one key whose default is NOT today's behaviour — it is
|
|
63
|
+
// the owner's ruling (ADR 0241 §4), and it is safe as a default because strict
|
|
64
|
+
// holds only a pin move, never a builder's ship.
|
|
65
|
+
assert.equal(v.CLOUDBONGOS_ARTIST_GATE, 'strict');
|
|
66
|
+
// Its companion stamp is emitted EMPTY rather than omitted: an absent variable is
|
|
67
|
+
// a policy the instance would have to infer, and empty already means "no cutoff".
|
|
68
|
+
assert.equal(v.CLOUDBONGOS_ARTIST_GATE_SINCE, '', 'present and empty — never absent');
|
|
69
|
+
assert.ok(linesOf(provisioning.settingsEnvLines({}, 'X')).some((l) => l === 'X_ARTIST_GATE_SINCE='));
|
|
62
70
|
assert.equal(linesOf(provisioning.settingsEnvLines({}, 'X')).length,
|
|
63
|
-
Object.keys(provisioning.SETTINGS_VOCAB).length
|
|
71
|
+
Object.keys(provisioning.SETTINGS_VOCAB).length + Object.keys(provisioning.SETTINGS_COMPANION_ENV).length,
|
|
72
|
+
'every policy key and every declared companion always present');
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
t('an off-vocabulary stored value falls back to the default, never reaches the instance', () => {
|
|
@@ -82,10 +91,14 @@ t('the planet never rides the env transport (task 1003347): it is the platform\'
|
|
|
82
91
|
assert.doesNotMatch(provisioning.settingsEnvLines({ settings: { planet_template: 'bead-obsidian-tint' } }, 'CB'), /planet|PLANET/);
|
|
83
92
|
});
|
|
84
93
|
|
|
85
|
-
t('every emitted key is
|
|
94
|
+
t('every emitted key is a vocabulary key or a DECLARED companion — the lists cannot drift', () => {
|
|
86
95
|
const keys = linesOf(provisioning.settingsEnvLines({}, 'X')).map((l) => l.split('=')[0].replace(/^X_/, '').toLowerCase());
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const expected = [
|
|
97
|
+
...Object.keys(provisioning.SETTINGS_VOCAB),
|
|
98
|
+
...Object.keys(provisioning.SETTINGS_COMPANION_ENV).map((k) => k.toLowerCase()),
|
|
99
|
+
];
|
|
100
|
+
assert.deepEqual(keys.sort(), expected.sort(),
|
|
101
|
+
'a key added to the vocabulary must be carried, or the instance never learns it — and an env var the instance reads must be a DECLARED companion, never an ad-hoc extra');
|
|
89
102
|
});
|
|
90
103
|
|
|
91
104
|
console.log('\nThe generated file tells the truth about what it is:');
|
|
@@ -99,11 +112,50 @@ t('it names the live push by the intent the runner actually dispatches (task 100
|
|
|
99
112
|
});
|
|
100
113
|
|
|
101
114
|
t('settingsEnvVars is the single source both legs derive from — same keys, same values as the lines', () => {
|
|
102
|
-
const row = { settings: { platform_visibility: 'gated', joinability: 'open', visibility: 'stealth', join_grant: 'view' } };
|
|
115
|
+
const row = { settings: { platform_visibility: 'gated', joinability: 'open', visibility: 'stealth', join_grant: 'view', artist_gate: 'advisory' } };
|
|
103
116
|
const vars = provisioning.settingsEnvVars(row, 'CLOUDBONGOS');
|
|
104
|
-
assert.deepEqual(vars, {
|
|
117
|
+
assert.deepEqual(vars, {
|
|
118
|
+
CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open', CLOUDBONGOS_VISIBILITY: 'stealth',
|
|
119
|
+
CLOUDBONGOS_JOIN_GRANT: 'view', CLOUDBONGOS_ARTIST_GATE: 'advisory', CLOUDBONGOS_ARTIST_GATE_SINCE: '',
|
|
120
|
+
});
|
|
121
|
+
// varsOf splits on '=', so an empty value reads as '' on both sides — the point
|
|
122
|
+
// of the comparison is that the standup lines carry exactly the same key set.
|
|
105
123
|
assert.deepEqual(varsOf(provisioning.settingsEnvLines(row, 'CLOUDBONGOS')), vars, 'the standup lines ARE the vars');
|
|
106
|
-
assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), {
|
|
124
|
+
assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), {
|
|
125
|
+
X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full',
|
|
126
|
+
X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '',
|
|
127
|
+
}, 'defaults, explicitly');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.log('\nThe artist gate rides the same transport (ADR 0241, task 1003575):');
|
|
131
|
+
|
|
132
|
+
t('the stamp is carried only when the gate was actually RAISED, and it is the row\'s own value', () => {
|
|
133
|
+
const stamped = { settings: { artist_gate: 'strict', artist_gate_strict_since: '2026-09-13T00:00:00.000Z' } };
|
|
134
|
+
assert.equal(provisioning.settingsEnvVars(stamped, 'X').X_ARTIST_GATE_SINCE, '2026-09-13T00:00:00.000Z');
|
|
135
|
+
// An unparseable stamp is NOT transmitted as a date nobody can read: it reads as
|
|
136
|
+
// absent, which is the safe direction — no cutoff, so every open review counts.
|
|
137
|
+
assert.equal(provisioning.settingsEnvVars({ settings: { artist_gate_strict_since: 'whenever' } }, 'X').X_ARTIST_GATE_SINCE, '');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
t('artistGateStamp writes the cutoff on a RAISE only — no other save may move it (ADR 0241 §5)', () => {
|
|
141
|
+
const now = new Date('2026-09-13T12:00:00.000Z');
|
|
142
|
+
const key = provisioning.ARTIST_GATE_STRICT_SINCE_KEY;
|
|
143
|
+
// advisory → strict is the tightening the stamp exists for.
|
|
144
|
+
assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'advisory' } }, { artist_gate: 'strict' }, now),
|
|
145
|
+
{ [key]: '2026-09-13T12:00:00.000Z' });
|
|
146
|
+
// A row with NO stored gate is already 'strict' by default — re-sending 'strict'
|
|
147
|
+
// is not a raise, and stamping it would silently un-hold reviews already held.
|
|
148
|
+
assert.deepEqual(provisioning.artistGateStamp({}, { artist_gate: 'strict' }, now), {});
|
|
149
|
+
// Lowering, and any save that does not touch the gate at all, move nothing.
|
|
150
|
+
assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'strict' } }, { artist_gate: 'off' }, now), {});
|
|
151
|
+
assert.deepEqual(provisioning.artistGateStamp({ settings: { artist_gate: 'advisory' } }, { joinability: 'open' }, now), {});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
t('the stamp can neither leak as a setting nor be written by a client patch', () => {
|
|
155
|
+
const row = { settings: { artist_gate: 'strict', artist_gate_strict_since: '2026-09-13T00:00:00.000Z' } };
|
|
156
|
+
assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.SETTINGS_VOCAB));
|
|
157
|
+
assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.PLANET_VOCAB));
|
|
158
|
+
assert.ok(!(provisioning.ARTIST_GATE_STRICT_SINCE_KEY in provisioning.effectiveSettings(row)));
|
|
107
159
|
});
|
|
108
160
|
|
|
109
161
|
console.log('\nwebEnvBody — the settings ride the real per-instance env file:');
|
|
@@ -103,6 +103,18 @@ t('redactBlob SCRUBS a secret shape from a text blob (handled, not leaked)', ()
|
|
|
103
103
|
assert.ok(!r.buf.toString('utf8').includes('AKIAABCDEFGHIJKLMNOP'), 'the secret must be scrubbed from the published text');
|
|
104
104
|
assert.equal(r.secret, false, 'a scrubbed text secret is handled, so it is not a residual leak');
|
|
105
105
|
});
|
|
106
|
+
// Regression (task 1003873). The secret verdict audits THIS call's own scrub, so it has to
|
|
107
|
+
// share the scrub's knownDocNames allowlist. When it did not, the audit re-redacted the ADR
|
|
108
|
+
// filename the scrub had just spared, read the difference as a leak, and failed the packager
|
|
109
|
+
// closed on every doc in the tree — the publish lane stayed red for seven core releases.
|
|
110
|
+
t('redactBlob reports no secret for an allowlisted doc filename (the gate agrees with its own scrub)', () => {
|
|
111
|
+
const name = '0281-differential-audit-allowlist-symmetry.md';
|
|
112
|
+
const knownDocNames = new Set([name]);
|
|
113
|
+
const r = redactBlob({ buf: Buffer.from(`see [ADR 0281](docs/adr/${name}) for why`), rules, sensitiveLiterals, envSecrets: [], knownDocNames });
|
|
114
|
+
assert.ok(r.buf.toString('utf8').includes(name), 'the allowlisted filename must survive the scrub');
|
|
115
|
+
assert.equal(r.secret, false, 'and must not then be reported as a residual secret');
|
|
116
|
+
assert.deepEqual(r.leaks, []);
|
|
117
|
+
});
|
|
106
118
|
t('redactBlob FAILS CLOSED on a secret inside a BINARY blob (cannot safely rewrite it)', () => {
|
|
107
119
|
const bin = Buffer.concat([Buffer.from('BIN\x00'), Buffer.from('AKIA' + 'ABCDEFGHIJKLMNOP')]);
|
|
108
120
|
const r = redactBlob({ buf: bin, rules, sensitiveLiterals, envSecrets: [] });
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// tests/rank_roles_audit.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002873 — the re-runnable half of "does the live roster hold only the four
|
|
4
|
+
// live ranks?".
|
|
5
|
+
//
|
|
6
|
+
// THE GAP THIS CLOSES. migrations/023's CHECK on builders.rank still admits twelve
|
|
7
|
+
// legacy values. governance_002's backfill INNER JOINs rank → the four seeded
|
|
8
|
+
// governance_roles, so a builder on a legacy rank gets no role row and resolves to
|
|
9
|
+
// ZERO permissions — admitted by threshold under requireRank, refused by the
|
|
10
|
+
// resolver, and told nothing. ADR 0151 chose that fail-closed direction
|
|
11
|
+
// deliberately; what was missing was anyone CHECKING.
|
|
12
|
+
//
|
|
13
|
+
// Three things had looked like the check and were not:
|
|
14
|
+
// - governance_002's own comment ("none exist today — verified") is a
|
|
15
|
+
// point-in-time manual note that nothing re-runs.
|
|
16
|
+
// - tests/governance_parity.mjs is DB-free by necessity (the unit gate has no
|
|
17
|
+
// Postgres) and says in the repo that it defers the roster half.
|
|
18
|
+
// - R114's live proof ran on a scratch DB holding only the four seeded
|
|
19
|
+
// rank-roles; the production roster was never queried.
|
|
20
|
+
//
|
|
21
|
+
// So the decision is a PURE function and this is a DB-free test of it. That is not
|
|
22
|
+
// a compromise — no-Postgres-in-CI is precisely why the gap survived, and a
|
|
23
|
+
// decision that can be tested without a database is the only kind that gets tested
|
|
24
|
+
// at all here. The live read is the script's thin shell around it.
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/rank_roles_audit.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
35
|
+
const { findRanksWithoutRoles, summarize } = require(path.join(ROOT, 'scripts', 'gds', 'audit-rank-roles.js'));
|
|
36
|
+
const { LIVE_RANK_LADDER } = require(path.join(ROOT, 'src', 'bongos', 'db-kernel.js'));
|
|
37
|
+
|
|
38
|
+
let passed = 0;
|
|
39
|
+
let failed = 0;
|
|
40
|
+
function t(name, fn) {
|
|
41
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
42
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const live = (rank, id = 1) => ({ id, github_login: `u${id}`, rank, status: 'active' });
|
|
46
|
+
|
|
47
|
+
t('the live ladder is the four ranks this audit is about', () => {
|
|
48
|
+
// Derived, not retyped: if the ladder ever gains a rank, this audit must be
|
|
49
|
+
// re-reasoned rather than silently widened.
|
|
50
|
+
assert.deepEqual([...LIVE_RANK_LADDER].sort(), ['archon', 'metic', 'thetes', 'xenos']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
t('a roster of only live ranks has no offenders', () => {
|
|
54
|
+
const rows = LIVE_RANK_LADDER.map((r, i) => live(r, i + 1));
|
|
55
|
+
assert.deepEqual(findRanksWithoutRoles(rows), []);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
t('a LEGACY rank is caught — the whole point', () => {
|
|
59
|
+
const out = findRanksWithoutRoles([live('archon', 1), { id: 2, github_login: 'old', rank: 'ephor', status: 'active' }]);
|
|
60
|
+
assert.equal(out.length, 1);
|
|
61
|
+
assert.equal(out[0].rank, 'ephor');
|
|
62
|
+
assert.equal(out[0].id, '2', 'the id must be reported as a string an operator can paste');
|
|
63
|
+
assert.equal(out[0].github_login, 'old', 'and the login, which is what identifies the account');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
t('a NULL or missing rank is caught too — it fails the same INNER JOIN', () => {
|
|
67
|
+
for (const row of [
|
|
68
|
+
{ id: 3, rank: null, status: 'active' },
|
|
69
|
+
{ id: 4, status: 'active' },
|
|
70
|
+
{ id: 5, rank: '', status: 'active' },
|
|
71
|
+
]) {
|
|
72
|
+
const out = findRanksWithoutRoles([row]);
|
|
73
|
+
assert.equal(out.length, 1, `${JSON.stringify(row)} must be reported`);
|
|
74
|
+
assert.equal(out[0].rank, row.rank === undefined ? null : row.rank);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
t('an INACTIVE builder on a legacy rank is still reported', () => {
|
|
79
|
+
// The roster query has no WHERE on `builders`, and this is the row a future
|
|
80
|
+
// CHECK-narrowing migration would fail to apply over. Skipping it would make a
|
|
81
|
+
// clean audit a false assurance for exactly the case that matters.
|
|
82
|
+
const out = findRanksWithoutRoles([{ id: 9, github_login: 'gone', rank: 'hoplite', status: 'inactive' }]);
|
|
83
|
+
assert.equal(out.length, 1);
|
|
84
|
+
assert.equal(out[0].status, 'inactive', 'and the status is carried so the operator can judge urgency');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
t('case matters — the backfill JOIN is exact', () => {
|
|
88
|
+
const out = findRanksWithoutRoles([{ id: 6, rank: 'Archon', status: 'active' }]);
|
|
89
|
+
assert.equal(out.length, 1, '`Archon` does not equal `archon` in SQL; a near-miss is still zero permissions');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
t('an empty or junk roster reports nothing and never throws', () => {
|
|
93
|
+
for (const bad of [[], null, undefined, 'nope', {}]) {
|
|
94
|
+
assert.deepEqual(findRanksWithoutRoles(bad), [], `${JSON.stringify(bad)} must be a quiet empty result`);
|
|
95
|
+
}
|
|
96
|
+
assert.deepEqual(findRanksWithoutRoles([null, undefined]), [], 'null rows are skipped, not dereferenced');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
t('the live-rank set is injectable, so the test is not asserting against itself', () => {
|
|
100
|
+
// Pass a deliberately different ladder: the function must obey its argument
|
|
101
|
+
// rather than a hardcoded list, or this whole file would be tautological.
|
|
102
|
+
assert.equal(findRanksWithoutRoles([live('archon')], ['xenos']).length, 1);
|
|
103
|
+
assert.equal(findRanksWithoutRoles([live('xenos')], ['xenos']).length, 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
t('summarize counts every rank, including the missing one', () => {
|
|
107
|
+
const h = summarize([live('xenos', 1), live('xenos', 2), live('archon', 3), { id: 4, rank: null }]);
|
|
108
|
+
assert.deepEqual(h, [['xenos', 2], ['archon', 1], ['(none)', 1]],
|
|
109
|
+
'a clean run must still say what it looked at — a check that prints nothing is '
|
|
110
|
+
+ 'indistinguishable from one that read nothing, which is the failure this task is about');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
console.log(`\nrank_roles_audit: ${passed} passed, ${failed} failed`);
|
|
114
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -208,6 +208,88 @@ test('a good bind still produces the blob/tree bases callers rely on', () => {
|
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
// ---- loadFromBranding: the source a standalone instance actually has -------
|
|
212
|
+
//
|
|
213
|
+
// An installed core sits in node_modules/@bongos/core, so loadFromGit correctly refuses
|
|
214
|
+
// and nothing sets the env pin — which meant EVERY standalone instance served a 503 from
|
|
215
|
+
// /public/repo-info while `config/branding.json` held the answer. These cases pin the new
|
|
216
|
+
// source AND, just as importantly, pin that it did not disturb the two above it.
|
|
217
|
+
|
|
218
|
+
const PACK = { repo: { owner: FIXTURE_OWNER, name: FIXTURE_REPO } };
|
|
219
|
+
|
|
220
|
+
test('a branding pack with owner + name binds (repo.name maps to repo)', () => {
|
|
221
|
+
assert.deepEqual(ri.loadFromBranding({ branding: () => PACK }), {
|
|
222
|
+
owner: FIXTURE_OWNER,
|
|
223
|
+
repo: FIXTURE_REPO,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('the neutral pack (empty strings) is NOT an answer', () => {
|
|
228
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ repo: { owner: '', name: '' } }) }), null);
|
|
229
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ repo: { owner: ' ', name: FIXTURE_REPO } }) }), null);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a pack with no repo key at all → null', () => {
|
|
233
|
+
assert.equal(ri.loadFromBranding({ branding: () => ({ identity: { productName: 'x' } }) }), null);
|
|
234
|
+
assert.equal(ri.loadFromBranding({ branding: () => null }), null);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('a THROWING branding loader degrades to null, never a crash', () => {
|
|
238
|
+
assert.equal(ri.loadFromBranding({ branding: () => { throw new Error('malformed pack'); } }), null);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('THE FIX: packaged instance, no env, no git → branding answers instead of 503', () => {
|
|
242
|
+
ri._resetCacheForTests();
|
|
243
|
+
try {
|
|
244
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
245
|
+
const info = ri.loadRepoInfo({
|
|
246
|
+
execSync: mkExec({ toplevel: path.resolve('/srv/instance') }), // git walks up → refused
|
|
247
|
+
packageRoot: root,
|
|
248
|
+
branding: () => PACK,
|
|
249
|
+
});
|
|
250
|
+
assert.equal(info.error, undefined, 'the instance declares its own repo — that is not an error case');
|
|
251
|
+
assert.equal(info.owner, FIXTURE_OWNER);
|
|
252
|
+
assert.equal(info.repo, FIXTURE_REPO);
|
|
253
|
+
assert.equal(info.blob_base, `https://github.com/${FIXTURE_OWNER}/${FIXTURE_REPO}/blob/main`);
|
|
254
|
+
} finally {
|
|
255
|
+
ri._resetCacheForTests();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('git still WINS over branding (the core dev checkout is unchanged)', () => {
|
|
260
|
+
ri._resetCacheForTests();
|
|
261
|
+
try {
|
|
262
|
+
const root = path.resolve('/srv/cloud-bongos');
|
|
263
|
+
const info = ri.loadRepoInfo({
|
|
264
|
+
execSync: mkExec({ toplevel: root }), // the remote resolves → git answers
|
|
265
|
+
packageRoot: root,
|
|
266
|
+
branding: () => ({ repo: { owner: 'packowner', name: 'packrepo' } }),
|
|
267
|
+
});
|
|
268
|
+
assert.equal(info.owner, FIXTURE_OWNER, 'branding must never override a resolved git remote');
|
|
269
|
+
} finally {
|
|
270
|
+
ri._resetCacheForTests();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('the env pin still WINS over branding (the operator override stays on top)', () => {
|
|
275
|
+
ri._resetCacheForTests();
|
|
276
|
+
process.env.OTB_GITHUB_OWNER = 'envowner';
|
|
277
|
+
process.env.OTB_GITHUB_REPO = 'envrepo';
|
|
278
|
+
try {
|
|
279
|
+
const root = path.resolve('/srv/instance/node_modules/@bongos/core');
|
|
280
|
+
const info = ri.loadRepoInfo({
|
|
281
|
+
execSync: mkExec({ toplevel: path.resolve('/srv/instance') }),
|
|
282
|
+
packageRoot: root,
|
|
283
|
+
branding: () => PACK,
|
|
284
|
+
});
|
|
285
|
+
assert.equal(info.owner, 'envowner');
|
|
286
|
+
} finally {
|
|
287
|
+
delete process.env.OTB_GITHUB_OWNER;
|
|
288
|
+
delete process.env.OTB_GITHUB_REPO;
|
|
289
|
+
ri._resetCacheForTests();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
211
293
|
// ---- end-to-end canary: the regression this must never cause ---------------
|
|
212
294
|
//
|
|
213
295
|
// The only case that touches real git. It earns its place — an anchor rule that is too
|
|
@@ -23,8 +23,9 @@ import { strict as assert } from 'node:assert';
|
|
|
23
23
|
import { createRequire } from 'node:module';
|
|
24
24
|
|
|
25
25
|
const require = createRequire(import.meta.url);
|
|
26
|
-
const {
|
|
27
|
-
|
|
26
|
+
const {
|
|
27
|
+
scrubSecrets, shannonEntropy, looksHighEntropySecret, collectEnvSecrets,
|
|
28
|
+
} = require('../scripts/gds/ship.js');
|
|
28
29
|
|
|
29
30
|
let passed = 0;
|
|
30
31
|
let failed = 0;
|
|
@@ -190,6 +191,73 @@ test('benign prose and short identifiers survive untouched', () => {
|
|
|
190
191
|
}
|
|
191
192
|
});
|
|
192
193
|
|
|
194
|
+
// ============================================================
|
|
195
|
+
// task 1002460 — the doc-path exemption is an ALLOWLIST, so recall is provable.
|
|
196
|
+
//
|
|
197
|
+
// Three shape-based rules were tried first and each leaked: anything built on what
|
|
198
|
+
// a filename LOOKS like also admits a secret dressed that way. The exemption now
|
|
199
|
+
// spares a candidate only when the caller says that exact name is a file it is
|
|
200
|
+
// publishing. A secret is not a file in the tree.
|
|
201
|
+
// ============================================================
|
|
202
|
+
|
|
203
|
+
const DOC_NAMES = new Set(["0016-trust-boundary-server-enforced-permissions.md","0083-module-system-plugin-api.md","core_231_project_settings_and_attention_water.sql"]);
|
|
204
|
+
|
|
205
|
+
test('1002460 with NO allowlist there is no exemption at all — recall byte-identical', () => {
|
|
206
|
+
// The guarantee that matters. Every caller that does not opt in — including every
|
|
207
|
+
// transcript scrub — behaves exactly as it did before this task existed.
|
|
208
|
+
for (const name of DOC_NAMES) {
|
|
209
|
+
const link = `[x](${name})`;
|
|
210
|
+
assert.ok(scrubSecrets(link).includes(REDACTED),
|
|
211
|
+
`without an allowlist a long filename is still redacted, as before: ${name}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('1002460 with the allowlist, real doc links survive', () => {
|
|
216
|
+
const opts = { knownDocNames: DOC_NAMES };
|
|
217
|
+
for (const name of DOC_NAMES) {
|
|
218
|
+
const link = `[x](${name})`;
|
|
219
|
+
assert.equal(scrubSecrets(link, [], opts), link, `over-redacted a real doc link: ${name}`);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('1002460 a secret is never in the allowlist, so it is never spared', () => {
|
|
224
|
+
// The smuggling cases from rounds 1 and 2, now structurally impossible: each is
|
|
225
|
+
// offered WITH the allowlist active, and none of them is a name in it.
|
|
226
|
+
const opts = { knownDocNames: DOC_NAMES };
|
|
227
|
+
const dressed = [
|
|
228
|
+
'[x](k3f9d2m7-q8w1e4r6-t5y0u3i9-p2a7s4d1.md)',
|
|
229
|
+
'[x](qxzjvbwk-mrtplghd-nfyscuae-bvkxwqzj.md)',
|
|
230
|
+
'[x](qxjvbwkzptrmnfyaeuicohgdslx12-a.md)', // the unbounded-run case round 2 missed
|
|
231
|
+
// NOTE on all-numeric strings: a long digit run joined by a hyphen is NOT
|
|
232
|
+
// caught by the scrubber with or without an allowlist — the hyphen breaks the
|
|
233
|
+
// all-hex rule C and H=3.30 is under rules A and B. That is a PRE-EXISTING gap
|
|
234
|
+
// in the entropy heuristic, not something this exemption opens, so it is noted
|
|
235
|
+
// here rather than asserted: a case that fails identically both ways would
|
|
236
|
+
// prove nothing about the allowlist.
|
|
237
|
+
'[x](a3f5c9e12b47d8016fa2c4e7b9d03518aa.md)',
|
|
238
|
+
];
|
|
239
|
+
const leaked = dressed.filter((d) => !scrubSecrets(d, [], opts).includes(REDACTED));
|
|
240
|
+
assert.deepEqual(leaked, [], `these slipped the scrubber: ${leaked.join(" | ")}`);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('1002460 the allowlist cannot spare a planted secret (recall unchanged, proven)', () => {
|
|
244
|
+
// The differential proof, now trivial: offer the WHOLE planted corpus with an
|
|
245
|
+
// allowlist active. Nothing in it is a filename, so nothing can be exempted.
|
|
246
|
+
const opts = { knownDocNames: DOC_NAMES };
|
|
247
|
+
for (const { name, secret, context } of positives()) {
|
|
248
|
+
const out = scrubSecrets(context, collectEnvSecrets({}), opts);
|
|
249
|
+
assert.ok(!out.includes(secret), `LEAK with allowlist active: "${name}" survived: ${out}`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('1002460 an allowlisted name only counts at a real doc extension boundary', () => {
|
|
254
|
+
// The name must be followed by its own extension. A secret that merely CONTAINS
|
|
255
|
+
// an allowlisted stem, or sits before a different extension, is not spared.
|
|
256
|
+
const opts = { knownDocNames: DOC_NAMES };
|
|
257
|
+
const notAName = '[x](0016-trust-boundary-server-enforced-permissions.env)';
|
|
258
|
+
assert.ok(scrubSecrets(notAName, [], opts).includes(REDACTED),
|
|
259
|
+
'the same stem before a NON-doc extension is not an allowlisted file');
|
|
260
|
+
});
|
|
193
261
|
test('empty / non-string / short-literal inputs are safe', () => {
|
|
194
262
|
assert.equal(scrubSecrets(''), '');
|
|
195
263
|
assert.equal(scrubSecrets(null), null);
|
|
@@ -91,6 +91,22 @@ t('names the Settings page, CLI Access, and the owner checkbox repair', () => {
|
|
|
91
91
|
assert.match(text, /Web sign-in is unaffected/i, 'says what still works');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
t('the closing step adapts to the caller — setup pastes, login runs a one-liner', () => {
|
|
95
|
+
const forSetup = setup.deviceFlowFallbackLines('https://spike.cloudbongos.com').join('\n');
|
|
96
|
+
const forLogin = setup.deviceFlowFallbackLines('https://spike.cloudbongos.com', { finish: 'login' }).join('\n');
|
|
97
|
+
|
|
98
|
+
// The default must be byte-stable — setup's own prompt still says "paste it below".
|
|
99
|
+
assert.match(forSetup, /paste it below/, 'setup keeps its inline-paste wording');
|
|
100
|
+
assert.doesNotMatch(forLogin, /paste it below/, '`bongos login` has no paste prompt to point at');
|
|
101
|
+
assert.match(forLogin, /run it in this shell/, 'login tells you what actually works there');
|
|
102
|
+
|
|
103
|
+
// Everything that is genuinely shared stays shared — one source of truth.
|
|
104
|
+
for (const shared of [/Enable Device Flow/, /CLI Access/, /builders\/settings/]) {
|
|
105
|
+
assert.match(forSetup, shared);
|
|
106
|
+
assert.match(forLogin, shared);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
94
110
|
// ── redeemCliToken in a hermetic child against a stubbed instance API ────────
|
|
95
111
|
// A child process because API_BASE + the session path resolve at module load:
|
|
96
112
|
// the child gets GDS_API_BASE → the stub and HOME/USERPROFILE → a throwaway
|
|
@@ -99,5 +99,55 @@ t('widget HTML-escapes the refs (no raw angle brackets from ids)', () => {
|
|
|
99
99
|
assert.ok(html.includes('<b>'), 'the id is HTML-escaped');
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// task 1002788 — this banner must not send a builder into a deadlock.
|
|
104
|
+
//
|
|
105
|
+
// The branch is ALREADY PUBLISHED when the gate fires, and server-mediated
|
|
106
|
+
// publish is append-only by deliberate design (github-push.js: "Refuse
|
|
107
|
+
// non-fast-forward implicitly (no --force, ever)"). So "rebase + re-ship" —
|
|
108
|
+
// what this banner used to say — rewrites history the server already holds, and
|
|
109
|
+
// every later publish is rejected non-fast-forward. The builder is then gated
|
|
110
|
+
// from new claims AND unable to land the one they hold, which is exactly the
|
|
111
|
+
// loop reproduced end-to-end on task 1002485. `git merge origin/main` reaches
|
|
112
|
+
// the same up-to-date branch and stays a fast-forward.
|
|
113
|
+
//
|
|
114
|
+
// The negative assertion is deliberately narrow: it bans INSTRUCTING a rebase,
|
|
115
|
+
// not the word. "Merge, never rebase" must keep working — that sentence is the
|
|
116
|
+
// point.
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
const INSTRUCTS_A_REBASE = [
|
|
119
|
+
/\brebase\s*\+\s*re-ship\b/i,
|
|
120
|
+
/\brebase (the|its|your) branch\b/i,
|
|
121
|
+
/\brebases it\b/i,
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
t('markdown prescribes MERGE, and never instructs a rebase', () => {
|
|
125
|
+
const md = rebaseWarningMarkdown([{ id: 123 }]);
|
|
126
|
+
assert.ok(md.includes('git merge origin/main'), 'must name the fast-forward-safe command');
|
|
127
|
+
for (const re of INSTRUCTS_A_REBASE) {
|
|
128
|
+
assert.ok(!re.test(md), `must not instruct a rebase of a published branch (matched ${re}): ${md}`);
|
|
129
|
+
}
|
|
130
|
+
assert.match(md, /never rebase/i, 'and must say why the other move is wrong, not merely omit it');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
t('widget prescribes MERGE, and never instructs a rebase', () => {
|
|
134
|
+
const html = rebaseWarningWidget([{ id: 123 }]);
|
|
135
|
+
assert.ok(html.includes('git merge origin/main'), 'must name the fast-forward-safe command');
|
|
136
|
+
for (const re of INSTRUCTS_A_REBASE) {
|
|
137
|
+
assert.ok(!re.test(html), `must not instruct a rebase of a published branch (matched ${re}): ${html}`);
|
|
138
|
+
}
|
|
139
|
+
assert.match(html, /never rebase/i);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
t('the two renderings give the SAME advice — a builder must not get different instructions per surface', () => {
|
|
143
|
+
const md = rebaseWarningMarkdown([{ id: 123 }]);
|
|
144
|
+
const html = rebaseWarningWidget([{ id: 123 }]);
|
|
145
|
+
for (const [name, text] of [['markdown', md], ['widget', html]]) {
|
|
146
|
+
assert.ok(text.includes('git merge origin/main'), `${name} lost the command`);
|
|
147
|
+
assert.match(text, /never rebase/i, `${name} lost the warning`);
|
|
148
|
+
assert.ok(text.includes('/builder-release'), `${name} lost the give-up exit`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
102
152
|
console.log(`\nstart_rebase_warning: ${passed} passed, ${failed} failed`);
|
|
103
153
|
if (failed > 0) process.exitCode = 1;
|