@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.
- package/.bongos-core.json +488 -413
- 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/copy-inventory.md +15 -13
- package/docs/copy-registry.json +35 -17
- 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/hall-ui/public/watch.css +6 -0
- package/modules/hall-ui/public/watch.js +52 -6
- 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/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/audit-rank-roles.js +172 -0
- package/scripts/gds/backfill-task-rewards.js +9 -4
- package/scripts/gds/build-cli-package.js +1 -0
- package/scripts/gds/claim.js +94 -9
- package/scripts/gds/fitness.js +8 -8
- package/scripts/gds/go-live.js +139 -5
- package/scripts/gds/land-watch.js +13 -5
- 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/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/branding.js +15 -0
- package/src/module-api.js +34 -1
- 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/box_scope_predicate.mjs +117 -0
- package/tests/claim_error_surface.mjs +163 -1
- package/tests/cli_exit_no_abort.mjs +256 -3
- package/tests/credit_log_single_writer.mjs +94 -0
- package/tests/currency_label.mjs +11 -4
- package/tests/go_live.mjs +144 -0
- package/tests/helpers.mjs +8 -0
- 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 +6 -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/scrubber_corpus.mjs +70 -2
- package/tests/start_rebase_warning.mjs +50 -0
- package/tests/task_field_bounds_match_db.mjs +140 -0
- package/tests/watch_roster_self_row.mjs +289 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// tests/artist_gate_route.mjs
|
|
2
|
+
//
|
|
3
|
+
// GET /artist-gate — the one read the deploy consults (ADR 0241, task 1003575).
|
|
4
|
+
//
|
|
5
|
+
// The pure verdict logic is pinned in tests/artist_gate.mjs; what is worth pinning
|
|
6
|
+
// HERE is the wiring, and specifically the three things a route can get wrong in a
|
|
7
|
+
// way no pure test would see:
|
|
8
|
+
//
|
|
9
|
+
// • 'off' must not even ASK the board. At `off` no reviews are filed, so a query
|
|
10
|
+
// would be a per-deploy read that can only ever return rows the gate ignores.
|
|
11
|
+
// • the DB read must be handed the CASCADE's own (source, ref) spelling, not a
|
|
12
|
+
// second copy of it — a drifted prefix silently matches nothing, and a gate
|
|
13
|
+
// that finds nothing looks exactly like a gate that is satisfied.
|
|
14
|
+
// • `holds` and `halt` must agree. Two fields describing one fact is two chances
|
|
15
|
+
// to disagree, and the caller trusts whichever it read.
|
|
16
|
+
//
|
|
17
|
+
// DB-free: the data-access fn is stubbed on the cached module object (the
|
|
18
|
+
// provisioning_settings.mjs pattern).
|
|
19
|
+
//
|
|
20
|
+
// Run: node --test tests/artist_gate_route.mjs
|
|
21
|
+
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import { strict as assert } from 'node:assert';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import http from 'node:http';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
|
|
29
|
+
const db = require('../modules/lifecycle/db.js');
|
|
30
|
+
const api = require('../src/module-api.js');
|
|
31
|
+
const gateModule = require('../src/bongos/artist-gate.js');
|
|
32
|
+
|
|
33
|
+
let rows = [];
|
|
34
|
+
let scanTruncated = false;
|
|
35
|
+
let queries = [];
|
|
36
|
+
db.listOpenArtistReviews = async (opts) => { queries.push(opts); return { reviews: rows, truncated: scanTruncated }; };
|
|
37
|
+
|
|
38
|
+
let mode = 'strict';
|
|
39
|
+
let since = null;
|
|
40
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, rank: 'thetes' }; next(); };
|
|
41
|
+
// The route must resolve the level through the doorway's ONE reader; stubbing it
|
|
42
|
+
// here is also what proves it does not read branding().project behind its back.
|
|
43
|
+
api.artistGate = { ...gateModule, artistGateMode: () => mode, artistGateSince: () => since };
|
|
44
|
+
|
|
45
|
+
const express = require('express');
|
|
46
|
+
const buildRouter = require('../modules/lifecycle/routes/artist-gate.js');
|
|
47
|
+
const app = express();
|
|
48
|
+
app.use((_req, res, next) => {
|
|
49
|
+
res.fail = (code, opts) => res.status((opts && opts.status) || 400).json({ error: { code } });
|
|
50
|
+
next();
|
|
51
|
+
});
|
|
52
|
+
app.use('/api/bongos', buildRouter());
|
|
53
|
+
|
|
54
|
+
const server = app.listen(0);
|
|
55
|
+
await new Promise((r) => server.once('listening', r));
|
|
56
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
57
|
+
|
|
58
|
+
function get(path) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
http.get(`${base}${path}`, (res) => {
|
|
61
|
+
let d = '';
|
|
62
|
+
res.on('data', (c) => { d += c; });
|
|
63
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, headers: res.headers, raw: d }); });
|
|
64
|
+
}).on('error', reject);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const review = (over = {}) => ({
|
|
69
|
+
id: '9001', title: 'Artist review: the copy and visuals that shipped in task 7001',
|
|
70
|
+
status: 'backlog', source: 'cascade', source_ref: 'cascade-artist-review-on-ship-7001',
|
|
71
|
+
created_at: '2026-09-12T00:00:00.000Z', merged_into_task_id: null, ...over,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function reset({ level = 'strict', cutoff = null, open = [], truncated = false } = {}) {
|
|
75
|
+
mode = level; since = cutoff; rows = open; scanTruncated = truncated; queries = [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("at 'off' the board is never asked — nothing is filed, so there is nothing to read", async () => {
|
|
79
|
+
reset({ level: 'off', open: [review()] });
|
|
80
|
+
const r = await get('/artist-gate');
|
|
81
|
+
assert.equal(r.status, 200, r.raw);
|
|
82
|
+
assert.equal(r.body.mode, 'off');
|
|
83
|
+
assert.equal(r.body.holds, false);
|
|
84
|
+
assert.deepEqual(r.body.reviews, []);
|
|
85
|
+
assert.equal(queries.length, 0, 'a per-deploy query whose rows are ignored is a query that should not run');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('the DB read is handed the cascade\'s OWN source and ref prefix, never a second copy', async () => {
|
|
89
|
+
reset({ open: [] });
|
|
90
|
+
await get('/artist-gate');
|
|
91
|
+
assert.equal(queries.length, 1);
|
|
92
|
+
assert.equal(queries[0].source, gateModule.ARTIST_REVIEW_SOURCE);
|
|
93
|
+
assert.equal(queries[0].refPrefix, gateModule.ARTIST_REVIEW_REF_PREFIX);
|
|
94
|
+
assert.deepEqual(queries[0].resolvedStatuses, gateModule.RESOLVED_REVIEW_STATUSES);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("at 'strict' an open review is reported as holding, with the refusal already composed", async () => {
|
|
98
|
+
reset({ open: [review()] });
|
|
99
|
+
const r = await get('/artist-gate');
|
|
100
|
+
assert.equal(r.body.mode, 'strict');
|
|
101
|
+
assert.equal(r.body.holds, true);
|
|
102
|
+
assert.deepEqual(r.body.reviews.map((t) => t.id), ['9001']);
|
|
103
|
+
assert.equal(r.body.halt.code, 'ARTIST_REVIEW_UNRESOLVED');
|
|
104
|
+
assert.match(r.body.halt.detail, /override-request/, 'the caller must not have to invent the way out');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("at 'advisory' the same open review is reported but holds nothing", async () => {
|
|
108
|
+
reset({ level: 'advisory', open: [review()] });
|
|
109
|
+
const r = await get('/artist-gate');
|
|
110
|
+
assert.equal(r.body.holds, false);
|
|
111
|
+
assert.equal(r.body.halt, null);
|
|
112
|
+
assert.deepEqual(r.body.reviews.map((t) => t.id), ['9001'], 'advisory still SHOWS the queue — it just does not stop');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('the cutoff is applied to what the board returned (ADR 0241 §5)', async () => {
|
|
116
|
+
reset({ cutoff: '2026-09-11T00:00:00.000Z', open: [review({ id: '9001', created_at: '2026-09-01T00:00:00.000Z' }), review({ id: '9002' })] });
|
|
117
|
+
const r = await get('/artist-gate');
|
|
118
|
+
assert.deepEqual(r.body.reviews.map((t) => t.id), ['9002'], 'the pre-raise review is not this gate\'s business');
|
|
119
|
+
assert.equal(r.body.since, '2026-09-11T00:00:00.000Z', 'and the cutoff itself is reported, so the answer is checkable');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('`holds` and `halt` can never disagree', async () => {
|
|
123
|
+
for (const open of [[], [review()]]) {
|
|
124
|
+
for (const level of ['off', 'advisory', 'strict']) {
|
|
125
|
+
reset({ level, open });
|
|
126
|
+
const r = await get('/artist-gate');
|
|
127
|
+
assert.equal(r.body.holds, r.body.halt !== null, `${level} / ${open.length} open`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the payload carries NO task title — the one untrusted string it could have carried', async () => {
|
|
133
|
+
reset({ open: [review()] });
|
|
134
|
+
const r = await get('/artist-gate');
|
|
135
|
+
assert.deepEqual(Object.keys(r.body.reviews[0]).sort(), ['created_at', 'id']);
|
|
136
|
+
assert.doesNotMatch(r.raw, /Artist review: the copy/, 'builder-written board text never reaches this wire');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('a capped scan reports a FLOOR, not a census — the verdict is unchanged either way', async () => {
|
|
140
|
+
reset({ open: [review({ id: '9001' }), review({ id: '9002' })], truncated: true });
|
|
141
|
+
const r = await get('/artist-gate');
|
|
142
|
+
assert.equal(r.body.truncated, true, 'the caller must never read the list as "and that is all"');
|
|
143
|
+
assert.equal(r.body.holds, true, 'truncation cannot weaken the gate — every row returned is a holder');
|
|
144
|
+
assert.match(r.body.halt.message, /^2\+ unresolved artist reviews/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('truncation is DROPPED when the cutoff shortened the page — "200+" over three rows is the worse lie', async () => {
|
|
148
|
+
reset({
|
|
149
|
+
cutoff: '2026-09-11T00:00:00.000Z',
|
|
150
|
+
open: [review({ id: '9001', created_at: '2026-09-01T00:00:00.000Z' }), review({ id: '9002' })],
|
|
151
|
+
truncated: true,
|
|
152
|
+
});
|
|
153
|
+
const r = await get('/artist-gate');
|
|
154
|
+
assert.equal(r.body.truncated, false);
|
|
155
|
+
assert.match(r.body.halt.message, /^1 unresolved artist review /);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('the read is never cached — a dismissed review must free the deploy immediately', async () => {
|
|
159
|
+
reset({ open: [review()] });
|
|
160
|
+
const r = await get('/artist-gate');
|
|
161
|
+
assert.equal(r.headers['cache-control'], 'no-store');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('a database failure is a 500 with no internals, never a quiet "nothing holds"', async () => {
|
|
165
|
+
reset({ open: [] });
|
|
166
|
+
db.listOpenArtistReviews = async () => { throw new Error('relation "tasks" does not exist'); };
|
|
167
|
+
const r = await get('/artist-gate');
|
|
168
|
+
assert.equal(r.status, 500);
|
|
169
|
+
assert.equal(r.body.error.code, 'artist_gate_read_failed');
|
|
170
|
+
assert.doesNotMatch(r.raw, /relation/, 'the error text never reaches the wire');
|
|
171
|
+
db.listOpenArtistReviews = async (opts) => { queries.push(opts); return { reviews: rows, truncated: scanTruncated }; };
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test.after(() => server.close());
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// tests/box_scope_predicate.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002593 — "is this a box-scope refusal?" must have exactly ONE definition,
|
|
4
|
+
// and it must survive the error-envelope shape.
|
|
5
|
+
//
|
|
6
|
+
// THE BUG THIS PINS. A box-scoped session (ADR 0053) may call only the dev-box
|
|
7
|
+
// bootstrap endpoints; everything else answers 403 box_scope. Callers branched on
|
|
8
|
+
// `res.data.error === 'box_scope'`. Then routes moved to the object error envelope
|
|
9
|
+
// (ADR 0116) — `data.error` became `{ code, message, details }` — and every one of
|
|
10
|
+
// those comparisons silently became dead code. Nothing failed; the branch just
|
|
11
|
+
// stopped firing, and builders got the generic 403 path instead of the guided
|
|
12
|
+
// re-auth the branch existed to give them. Task 1002162 fixed it in cli-lib by
|
|
13
|
+
// adding isBoxScope(); scripts/gds/ship-land.js was the last site still deciding
|
|
14
|
+
// for itself, and task 1002593 repointed it.
|
|
15
|
+
//
|
|
16
|
+
// WHY A DRIFT GUARD AND NOT JUST UNIT CASES. The original defect was not a wrong
|
|
17
|
+
// answer — it was a comparison that could never be true. A unit test of the good
|
|
18
|
+
// predicate cannot see a second, dead one somewhere else in the tree; only a scan
|
|
19
|
+
// can. So the last test here is the one that actually protects against a repeat.
|
|
20
|
+
//
|
|
21
|
+
// Run: node tests/box_scope_predicate.mjs
|
|
22
|
+
|
|
23
|
+
import { strict as assert } from 'node:assert';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const CLI_DIR = path.join(ROOT, 'scripts', 'gds');
|
|
32
|
+
const { isBoxScope } = require(path.join(CLI_DIR, 'cli-lib.js'));
|
|
33
|
+
|
|
34
|
+
let passed = 0;
|
|
35
|
+
let failed = 0;
|
|
36
|
+
function t(name, fn) {
|
|
37
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
38
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ENVELOPE = { status: 403, data: { error: { code: 'box_scope', message: 'box-scoped session' } } };
|
|
42
|
+
const LEGACY = { status: 403, data: { error: 'box_scope' } };
|
|
43
|
+
|
|
44
|
+
t('fires for the CURRENT object error envelope — the shape that killed the old comparison', () => {
|
|
45
|
+
assert.equal(isBoxScope(ENVELOPE), true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
t('the old hand-rolled comparison is provably dead against that same shape', () => {
|
|
49
|
+
// Not a tautology: this is the regression itself. If someone reintroduces
|
|
50
|
+
// `data.error === 'box_scope'` anywhere, this is what it evaluates to.
|
|
51
|
+
assert.equal(ENVELOPE.data.error === 'box_scope', false,
|
|
52
|
+
'the envelope shape must NOT satisfy a bare string comparison — that is the whole defect');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
t('still fires for the LEGACY flat shape, so a pinned older core keeps working', () => {
|
|
56
|
+
assert.equal(isBoxScope(LEGACY), true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
t('does not fire on a different code at 403', () => {
|
|
60
|
+
assert.equal(isBoxScope({ status: 403, data: { error: { code: 'rank_too_low_for_task' } } }), false);
|
|
61
|
+
assert.equal(isBoxScope({ status: 403, data: { error: 'forbidden' } }), false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
t('does not fire on a non-403, even carrying the code', () => {
|
|
65
|
+
assert.equal(isBoxScope({ status: 200, data: { error: { code: 'box_scope' } } }), false,
|
|
66
|
+
'status is part of the contract — a 200 mentioning box_scope is not a refusal');
|
|
67
|
+
assert.equal(isBoxScope({ status: 500, data: { error: { code: 'box_scope' } } }), false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
t('tolerates missing/!odd bodies instead of throwing', () => {
|
|
71
|
+
for (const bad of [undefined, null, {}, { status: 403 }, { status: 403, data: null }, { status: 403, data: {} }]) {
|
|
72
|
+
assert.equal(isBoxScope(bad), false, `${JSON.stringify(bad)} must be a quiet false, not a throw`);
|
|
73
|
+
}
|
|
74
|
+
assert.equal(isBoxScope({ status: 403, data: { error: null } }), false,
|
|
75
|
+
'a null error must not be read as an envelope');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// The drift guard. One definition, and no second opinion anywhere in the CLI.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
t('no CLI script decides box-scope for itself — cli-lib owns the predicate', () => {
|
|
82
|
+
const files = fs.readdirSync(CLI_DIR).filter((f) => f.endsWith('.js'));
|
|
83
|
+
// Fail loudly on a scan that found nothing: an empty sweep looks exactly like a
|
|
84
|
+
// clean one, and a guard that silently stops scanning protects nothing.
|
|
85
|
+
assert.ok(files.length > 20, `only ${files.length} scripts scanned — the sweep is not reading the CLI directory`);
|
|
86
|
+
|
|
87
|
+
const offenders = [];
|
|
88
|
+
let sawTheCanary = false;
|
|
89
|
+
for (const f of files) {
|
|
90
|
+
const src = fs.readFileSync(path.join(CLI_DIR, f), 'utf8');
|
|
91
|
+
if (f === 'cli-lib.js') {
|
|
92
|
+
// The one legitimate definition. Confirms the scan can actually see the
|
|
93
|
+
// pattern it is looking for, so a regex that has drifted off the source
|
|
94
|
+
// shape reports as a failure here rather than as a clean sweep everywhere.
|
|
95
|
+
sawTheCanary = /===\s*'box_scope'/.test(src);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Strip comments: several files DISCUSS the old comparison on purpose.
|
|
99
|
+
const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
|
|
100
|
+
for (const m of code.matchAll(/[^\n]*===\s*'box_scope'[^\n]*/g)) {
|
|
101
|
+
offenders.push(`${f}: ${m[0].trim()}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
assert.ok(sawTheCanary, 'cli-lib.js no longer contains the comparison — this guard is looking for the wrong pattern');
|
|
106
|
+
assert.deepEqual(offenders, [],
|
|
107
|
+
`these sites decide box-scope themselves instead of calling isBoxScope(); that is how the comparison died last time:\n ${offenders.join('\n ')}`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
t('ship-land.js — the site task 1002593 repointed — uses the shared predicate', () => {
|
|
111
|
+
const src = fs.readFileSync(path.join(CLI_DIR, 'ship-land.js'), 'utf8');
|
|
112
|
+
assert.match(src, /isBoxScope\s*\(/, 'the box-scope branch must call the shared predicate');
|
|
113
|
+
assert.match(src, /require\('\.\/cli-lib'\)/, 'and import it from cli-lib rather than redefining it');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(`\nbox_scope_predicate: ${passed} passed, ${failed} failed`);
|
|
117
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
import { strict as assert } from 'node:assert';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
16
18
|
const require = createRequire(import.meta.url);
|
|
17
|
-
const { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan } = require('../scripts/gds/claim.js');
|
|
19
|
+
const { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan, worktreeDetectionNote } = require('../scripts/gds/claim.js');
|
|
18
20
|
|
|
19
21
|
let passed = 0;
|
|
20
22
|
let failed = 0;
|
|
@@ -29,6 +31,12 @@ const envelope = (code, details = {}, message = code, status = 409) => ({
|
|
|
29
31
|
});
|
|
30
32
|
const text = (r) => r.lines.join('\n');
|
|
31
33
|
|
|
34
|
+
// task 1002592 — populated by the guard test below, then checked against the
|
|
35
|
+
// codes the server can actually refuse a claim with. Declared here so the drift
|
|
36
|
+
// guard reads the SAME array the coverage test exercises rather than a second
|
|
37
|
+
// hand-typed copy that could itself drift.
|
|
38
|
+
const CLAIM_REFUSAL_CODES_UNDER_TEST = [];
|
|
39
|
+
|
|
32
40
|
// The regression guard: no refusal may ever render a raw object.
|
|
33
41
|
t('never dumps [object Object] for any known code', () => {
|
|
34
42
|
const codes = [
|
|
@@ -37,8 +45,16 @@ t('never dumps [object Object] for any known code', () => {
|
|
|
37
45
|
'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
|
|
38
46
|
'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
|
|
39
47
|
'OPEN_BLOCKER', 'GOAL_MEMBERSHIP_REQUIRED',
|
|
48
|
+
// task 1002592: REBASE_REQUIRED was missing from this list for its whole
|
|
49
|
+
// life, which is exactly why its rendering gap survived — the guard that
|
|
50
|
+
// looked like it covered every refusal only covered the ones someone
|
|
51
|
+
// remembered to type. The drift guard at the bottom of this file now checks
|
|
52
|
+
// this array against what the server can actually throw.
|
|
53
|
+
'REBASE_REQUIRED', 'REWARD_REQUIRED', 'BUILDER_INACTIVE', 'INSUFFICIENT_RANK',
|
|
54
|
+
'XENOS_RESTRICTED', 'rank_too_low_for_task',
|
|
40
55
|
'some_unmapped_future_code',
|
|
41
56
|
];
|
|
57
|
+
CLAIM_REFUSAL_CODES_UNDER_TEST.push(...codes);
|
|
42
58
|
for (const c of codes) {
|
|
43
59
|
const out = text(formatClaimFailure(envelope(c, { active_task_id: 5 }, 'msg'), 12));
|
|
44
60
|
assert.ok(!out.includes('[object Object]'), `${c} produced a raw object dump`);
|
|
@@ -191,5 +207,151 @@ t('claimErrorInfo: reads envelope and flat shapes identically', () => {
|
|
|
191
207
|
assert.equal(flat.code, 'X'); assert.equal(flat.d.a, 1);
|
|
192
208
|
});
|
|
193
209
|
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// task 1002592 — three measured defects in this surface.
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
// 1. REBASE_REQUIRED. ADR 0120 part 3's gate refuses EVERY new claim, so it is
|
|
215
|
+
// the refusal a builder is most stuck behind, and it was the only one with no
|
|
216
|
+
// branch: it printed `Claim failed (409) [REBASE_REQUIRED]: REBASE_REQUIRED.`
|
|
217
|
+
// while a fully-formed remediation menu sat unread in the body.
|
|
218
|
+
const REBASE_HINT = 'Your queue is gated: 1 confirmed task(s) await your rebase (task 1003575). Clear it with /strand-fix N (works on any machine), or rebase the branch and re-ship — /merge-mode only if this machine has a GitHub push credential.';
|
|
219
|
+
|
|
220
|
+
t('REBASE_REQUIRED: names the owed tasks and relays the server hint', () => {
|
|
221
|
+
const out = text(formatClaimFailure(envelope('REBASE_REQUIRED', {
|
|
222
|
+
hint: REBASE_HINT,
|
|
223
|
+
rebaseTasks: [{ id: 1003575, title: 'The artist deploy gate', reason: 'strand:unresolvable_conflict:docs/module-api-changelog.md' }],
|
|
224
|
+
}), 1002592));
|
|
225
|
+
assert.match(out, /task 1003575/, 'must name the owed task id');
|
|
226
|
+
assert.match(out, /The artist deploy gate/, 'must name the owed task title');
|
|
227
|
+
assert.match(out, /strand:unresolvable_conflict/, 'must surface why it was flagged');
|
|
228
|
+
assert.match(out, /strand-fix/, 'must relay the hint, which leads with the universally-available exit');
|
|
229
|
+
// The regression proper: the old output was ONLY the bare code echo.
|
|
230
|
+
assert.ok(!/^Claim failed \(409\) \[REBASE_REQUIRED\]: REBASE_REQUIRED\.$/m.test(out),
|
|
231
|
+
'must not fall through to the bare code echo');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
t('REBASE_REQUIRED: still guides when the server sends no hint and no task list', () => {
|
|
235
|
+
const out = text(formatClaimFailure(envelope('REBASE_REQUIRED', {}), 5));
|
|
236
|
+
assert.match(out, /gated/i, 'must say the queue is gated');
|
|
237
|
+
assert.match(out, /strand-fix|builder-release/, 'must name a way out even with an empty body');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
t('REBASE_REQUIRED: server prose is clipped, flattened and capped before it hits the terminal', () => {
|
|
241
|
+
const out = text(formatClaimFailure(envelope('REBASE_REQUIRED', {
|
|
242
|
+
hint: 'H'.repeat(2000),
|
|
243
|
+
rebaseTasks: Array.from({ length: 50 }, (_, i) => ({
|
|
244
|
+
id: 1000 + i,
|
|
245
|
+
title: `T${'i'.repeat(400)}`,
|
|
246
|
+
reason: 'line one[2Jline two\nline three',
|
|
247
|
+
})),
|
|
248
|
+
}), 5));
|
|
249
|
+
assert.ok(!//.test(out), 'escape sequences must never reach the terminal verbatim');
|
|
250
|
+
assert.ok(!/line one.*\n.*line two/.test(out), 'a multi-line reason must be flattened onto its own single line');
|
|
251
|
+
for (const line of out.split('\n')) {
|
|
252
|
+
assert.ok(line.length <= 620, `a rendered line ran to ${line.length} chars — the cap is not holding`);
|
|
253
|
+
}
|
|
254
|
+
const listed = out.split('\n').filter((l) => /^ {2}- task /.test(l)).length;
|
|
255
|
+
assert.ok(listed > 0 && listed <= 20, `expected a bounded task list, got ${listed} rows`);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// 2. The fallback floor. Any code the server sends a hint for must print it,
|
|
259
|
+
// so the NEXT unmapped code cannot be mute the way REBASE_REQUIRED was.
|
|
260
|
+
t('fallback: an unmapped code still prints the server hint when one is supplied', () => {
|
|
261
|
+
const out = text(formatClaimFailure(
|
|
262
|
+
envelope('some_future_gate', { hint: 'Do the thing, then retry.' }, 'nope', 409), 3));
|
|
263
|
+
assert.match(out, /some_future_gate/, 'still names the code');
|
|
264
|
+
assert.match(out, /Do the thing, then retry\./, 'and prints the way forward');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
t('fallback: no hint means no invented guidance', () => {
|
|
268
|
+
const out = text(formatClaimFailure(envelope('some_future_gate', {}, 'nope', 409), 3));
|
|
269
|
+
assert.match(out, /some_future_gate/);
|
|
270
|
+
assert.ok(!/undefined/.test(out), 'must not print a stray undefined when no hint exists');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 3. The original bug report: a failed worktree DETECTION arrives disguised as a
|
|
274
|
+
// claim CONFLICT. The note must fire when null is WRONG and stay silent when
|
|
275
|
+
// null is the correct answer — that distinction is the whole fix.
|
|
276
|
+
t('worktreeDetectionNote: silent when the worktree WAS named (the refusal is genuine)', () => {
|
|
277
|
+
assert.deepEqual(worktreeDetectionNote({ worktree: 'task-42', isMainCheckout: false }, 42), []);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
t('worktreeDetectionNote: silent in the MAIN CHECKOUT, where a null name is correct', () => {
|
|
281
|
+
assert.deepEqual(worktreeDetectionNote({ worktree: null, isMainCheckout: true }, 42), [],
|
|
282
|
+
'blaming path detection here would send the reader down exactly the wrong path');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
t('worktreeDetectionNote: fires on a LINKED worktree with no name, and offers --worktree', () => {
|
|
286
|
+
const out = worktreeDetectionNote({ worktree: null, isMainCheckout: false }, 42).join('\n');
|
|
287
|
+
assert.match(out, /could not name its worktree/i);
|
|
288
|
+
assert.match(out, /linked worktree, so a name was expected/i, 'must say a name was expected here');
|
|
289
|
+
assert.match(out, /--worktree/, 'must print the explicit fallback');
|
|
290
|
+
assert.match(out, /claim\.js 42 --worktree/, 'the fallback must be runnable as printed');
|
|
291
|
+
assert.match(out, /not a claim conflict/i, 'must correct the misreading the report describes');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
t('worktreeDetectionNote: fires when the tree kind is UNKNOWN, and hedges rather than asserts', () => {
|
|
295
|
+
const out = worktreeDetectionNote({ worktree: null, isMainCheckout: null }, 42).join('\n');
|
|
296
|
+
assert.match(out, /--worktree/);
|
|
297
|
+
assert.match(out, /not recognisable as the main checkout/i);
|
|
298
|
+
assert.ok(!/linked worktree, so a name was expected/i.test(out),
|
|
299
|
+
'must not claim this IS a linked worktree when the probe could not tell');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
t('worktreeDetectionNote: a missing ctx is silent, never a crash', () => {
|
|
303
|
+
assert.deepEqual(worktreeDetectionNote(undefined, 42), []);
|
|
304
|
+
assert.deepEqual(worktreeDetectionNote({}, 42), [], 'an empty ctx knows nothing — say nothing');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
t('session_already_has_active_claim: carries the detection note only when ctx warrants it', () => {
|
|
308
|
+
const e = envelope('session_already_has_active_claim', { active_task_id: 7, active_task_title: 'Other' });
|
|
309
|
+
const withNote = text(formatClaimFailure(e, 42, { worktree: null, isMainCheckout: false }));
|
|
310
|
+
assert.match(withNote, /--worktree/, 'an unnamed linked worktree must get the note');
|
|
311
|
+
const named = text(formatClaimFailure(e, 42, { worktree: 'task-42', isMainCheckout: false }));
|
|
312
|
+
assert.ok(!/--worktree/.test(named), 'a named worktree means the conflict is real — no note');
|
|
313
|
+
const mainCheckout = text(formatClaimFailure(e, 42, { worktree: null, isMainCheckout: true }));
|
|
314
|
+
assert.ok(!/--worktree/.test(mainCheckout), 'the main checkout must stay silent');
|
|
315
|
+
const noCtx = text(formatClaimFailure(e, 42));
|
|
316
|
+
assert.ok(!/--worktree/.test(noCtx), 'no ctx (every pre-existing caller) must behave as before');
|
|
317
|
+
assert.match(noCtx, /#7/, 'and must still render the original guidance');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// The drift guard. REBASE_REQUIRED was thrown by the server and absent from the
|
|
322
|
+
// coverage list above for its whole life, which is how a mute refusal survived a
|
|
323
|
+
// test that looked like it covered every refusal. Derive the truth from the
|
|
324
|
+
// server source instead of trusting a hand-typed array.
|
|
325
|
+
//
|
|
326
|
+
// A static scan fails SILENT — finding nothing looks identical to finding no
|
|
327
|
+
// problem — so this asserts the extraction actually worked before it asserts
|
|
328
|
+
// anything about the result.
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
t('drift guard: every code claimTask can throw is covered above (or consciously excluded)', () => {
|
|
331
|
+
const src = readFileSync(new URL('../modules/lifecycle/db-claims.js', import.meta.url), 'utf8');
|
|
332
|
+
const start = src.indexOf('async function claimTask');
|
|
333
|
+
assert.ok(start !== -1, 'could not find claimTask in db-claims.js — the guard is reading the wrong file');
|
|
334
|
+
const thrown = new Set(
|
|
335
|
+
[...src.slice(start).matchAll(/throw \{\s*code: '([A-Za-z_]+)'/g)].map((m) => m[1])
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Fail loudly on a scan that found nothing, and on one that lost its canary.
|
|
339
|
+
assert.ok(thrown.size >= 8, `extracted only ${thrown.size} codes — the regex has drifted off the source shape`);
|
|
340
|
+
assert.ok(thrown.has('REBASE_REQUIRED'), 'canary missing: the extraction is not seeing real throws');
|
|
341
|
+
assert.ok(thrown.has('TASK_NOT_FOUND'), 'canary missing: the extraction is not seeing real throws');
|
|
342
|
+
|
|
343
|
+
// Codes that exist on the claim path but can never reach the single-claim CLI.
|
|
344
|
+
// Listed explicitly so adding a server code forces a decision rather than
|
|
345
|
+
// silently widening this exemption.
|
|
346
|
+
const NOT_A_SINGLE_CLAIM_REFUSAL = new Set([
|
|
347
|
+
'BATCH_EMPTY', 'BATCH_DUPLICATE_IDS', 'BATCH_FAILED', // the hall's bulk-claim route
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const covered = new Set(CLAIM_REFUSAL_CODES_UNDER_TEST);
|
|
351
|
+
const missing = [...thrown].filter((c) => !covered.has(c) && !NOT_A_SINGLE_CLAIM_REFUSAL.has(c));
|
|
352
|
+
assert.deepEqual(missing, [],
|
|
353
|
+
`these server codes are refusable but untested here — add them to the codes[] array above (or to NOT_A_SINGLE_CLAIM_REFUSAL with a reason): ${missing.join(', ')}`);
|
|
354
|
+
});
|
|
355
|
+
|
|
194
356
|
console.log(`\nclaim_error_surface: ${passed} passed, ${failed} failed`);
|
|
195
357
|
process.exit(failed === 0 ? 0 : 1);
|