@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
|
@@ -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);
|
|
@@ -510,6 +510,15 @@ t('renderInstanceClaudeMd: brand-substituted — names the instance + its hall/c
|
|
|
510
510
|
assert.ok(!md.toLowerCase().includes('amazonprimea') && !md.includes('~/.config/otb'), 'no founder literal in the starter');
|
|
511
511
|
});
|
|
512
512
|
|
|
513
|
+
t('renderInstanceClaudeMd: the first-run bootstrap precedes every bongos instruction', () => {
|
|
514
|
+
const md = tmpl.renderInstanceClaudeMd({ identity: { productName: 'Acme' } });
|
|
515
|
+
const iNpm = md.indexOf('npm install');
|
|
516
|
+
const iStart = md.indexOf('bongos start');
|
|
517
|
+
assert.ok(iNpm > 0, 'names npm install at all — without it a fresh clone has no bongos command');
|
|
518
|
+
assert.ok(iStart > iNpm, 'npm install comes BEFORE the first bongos instruction, or the agent hits command not found');
|
|
519
|
+
assert.ok(/@bongos\/core/.test(md), 'explains WHY: the CLI ships inside the dependency');
|
|
520
|
+
});
|
|
521
|
+
|
|
513
522
|
t('renderInstanceClaudeMd: degrades cleanly on a minimal branding (no world dup, defaults currency)', () => {
|
|
514
523
|
const md = tmpl.renderInstanceClaudeMd({ identity: { productName: 'Solo', worldName: 'Solo' } });
|
|
515
524
|
assert.ok(md.startsWith('# Solo — Project Memory'));
|
|
@@ -32,6 +32,7 @@ import assert from 'node:assert/strict';
|
|
|
32
32
|
import { test } from 'node:test';
|
|
33
33
|
import { spawnSync, spawn } from 'node:child_process';
|
|
34
34
|
import fs from 'node:fs';
|
|
35
|
+
import os from 'node:os';
|
|
35
36
|
import path from 'node:path';
|
|
36
37
|
import { createRequire } from 'node:module';
|
|
37
38
|
import { fileURLToPath } from 'node:url';
|
|
@@ -40,6 +41,10 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
40
41
|
const API = path.join(ROOT, 'scripts', 'gds', 'api.js');
|
|
41
42
|
const MAIN_AUDIT = path.join(ROOT, 'scripts', 'gds', 'main-audit.js');
|
|
42
43
|
const CLI_LIB = path.join(ROOT, 'scripts', 'gds', 'cli-lib.js');
|
|
44
|
+
// task 1002592 — the next two CLIs found ABORTING, converted under task 1002734's
|
|
45
|
+
// standing rule (evidence, not a blanket sweep).
|
|
46
|
+
const CLAIM = path.join(ROOT, 'scripts', 'gds', 'claim.js');
|
|
47
|
+
const LAND_WATCH = path.join(ROOT, 'scripts', 'gds', 'land-watch.js');
|
|
43
48
|
const DEAD_BASE = 'http://127.0.0.1:9'; // discard port — connection refused, instantly
|
|
44
49
|
|
|
45
50
|
// Point a CLI at a base URL. DERIVED, not hardcoded: instance-config resolves
|
|
@@ -47,8 +52,14 @@ const DEAD_BASE = 'http://127.0.0.1:9'; // discard port — connection refused,
|
|
|
47
52
|
// literal here would (a) add to the legacy-GDS ratchet the rename is retiring and
|
|
48
53
|
// (b) silently stop steering the CLI on any instance whose brand pack sets a
|
|
49
54
|
// different prefix — the test would then hit the REAL API instead of the stub.
|
|
50
|
-
const
|
|
55
|
+
const ic = createRequire(import.meta.url)(path.join(ROOT, 'src', 'instance-config.js'));
|
|
56
|
+
const { envPrefix } = ic;
|
|
51
57
|
const API_BASE_ENV = `${envPrefix()}_API_BASE`;
|
|
58
|
+
// Derived for the same reason the base env is: a literal would stop steering the
|
|
59
|
+
// CLI on any instance whose brand pack renames the config dir, and the test would
|
|
60
|
+
// then read the operator's REAL session instead of the throwaway one.
|
|
61
|
+
const CONFIG_DIR_NAME = ic.configDirName();
|
|
62
|
+
const SESSION_FILENAME = ic.SESSION_FILENAMES[0];
|
|
52
63
|
const atBase = (url) => ({ [API_BASE_ENV]: url });
|
|
53
64
|
|
|
54
65
|
// The native abort prints this and nothing else can be relied on; it is NOT a
|
|
@@ -104,8 +115,79 @@ const s = http.createServer((q, r) => {
|
|
|
104
115
|
s.listen(0, '127.0.0.1', () => console.log('PORT=' + s.address().port));
|
|
105
116
|
`;
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
// task 1002592 — claim.js needs a stub that ROUTES: the claim POST decides which
|
|
119
|
+
// path is under test, while every other call it makes (learnings, recall, /me, the
|
|
120
|
+
// task fetch land-watch does) is best-effort and only has to answer something
|
|
121
|
+
// parseable. `status` picks the path: 409 serves the real REBASE_REQUIRED envelope
|
|
122
|
+
// so the refusal rendering is proved end-to-end, 201 serves a claim so the heavier
|
|
123
|
+
// success teardown runs.
|
|
124
|
+
const claimStubSrc = (status) => `
|
|
125
|
+
const http = require('node:http');
|
|
126
|
+
const REFUSAL = ${JSON.stringify({
|
|
127
|
+
error: {
|
|
128
|
+
code: 'REBASE_REQUIRED',
|
|
129
|
+
message: 'REBASE_REQUIRED',
|
|
130
|
+
details: {
|
|
131
|
+
hint: 'Your queue is gated: 1 confirmed task(s) await your rebase (task 1003575). Clear it with /strand-fix N (works on any machine).',
|
|
132
|
+
rebaseTasks: [{ id: 1003575, title: 'Stub owed task', reason: 'strand:unresolvable_conflict:stub.md' }],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
})};
|
|
136
|
+
const CLAIMED = ${JSON.stringify({
|
|
137
|
+
claim: { id: 9001 },
|
|
138
|
+
task: {
|
|
139
|
+
id: 4242, title: 'Stub task', version_id: 'BONGOS-V2', touches: [],
|
|
140
|
+
credits_reward: 0, priority: 1, description: '', parallel_safe: false,
|
|
141
|
+
},
|
|
142
|
+
reward: { mode: 'cost-plus-only', perTaskCredit: false },
|
|
143
|
+
})};
|
|
144
|
+
const s = http.createServer((q, r) => {
|
|
145
|
+
let body = '';
|
|
146
|
+
q.on('data', (c) => { body += c; });
|
|
147
|
+
q.on('end', () => {
|
|
148
|
+
const isClaim = q.method === 'POST' && /\\/claims$/.test(q.url.split('?')[0]);
|
|
149
|
+
const code = isClaim ? ${status} : 200;
|
|
150
|
+
const payload = isClaim ? (${status} === 201 ? CLAIMED : REFUSAL) : {};
|
|
151
|
+
r.writeHead(code, { 'Content-Type': 'application/json' });
|
|
152
|
+
r.end(JSON.stringify(payload));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
s.listen(0, '127.0.0.1', () => console.log('PORT=' + s.address().port));
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
// task 1002592 — claim.js and land-watch.js read a SESSION and (for land-watch) a
|
|
159
|
+
// breadcrumb directory, both under the config home, which instance-config derives
|
|
160
|
+
// from os.homedir(). Node resolves that from HOME / USERPROFILE, so overriding both
|
|
161
|
+
// gives each test a throwaway config tree: the developer's real session is never
|
|
162
|
+
// read, their real breadcrumb dir is never written, and a test can plant exactly
|
|
163
|
+
// the state it needs. Without this, planting a breadcrumb would mean writing into
|
|
164
|
+
// the operator's own ~/.config and racing whatever land-watch is genuinely tracking.
|
|
165
|
+
function makeHome({ apiBase = DEAD_BASE, breadcrumbTaskId = null } = {}) {
|
|
166
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-exit-home-'));
|
|
167
|
+
const cfg = path.join(home, '.config', CONFIG_DIR_NAME);
|
|
168
|
+
fs.mkdirSync(cfg, { recursive: true });
|
|
169
|
+
fs.writeFileSync(
|
|
170
|
+
path.join(cfg, SESSION_FILENAME),
|
|
171
|
+
JSON.stringify({ token: 'stub-token-not-a-real-credential', api_base: apiBase }),
|
|
172
|
+
'utf8'
|
|
173
|
+
);
|
|
174
|
+
if (breadcrumbTaskId) {
|
|
175
|
+
const pending = path.join(cfg, 'pending-land');
|
|
176
|
+
fs.mkdirSync(pending, { recursive: true });
|
|
177
|
+
fs.writeFileSync(
|
|
178
|
+
path.join(pending, `${breadcrumbTaskId}.json`),
|
|
179
|
+
JSON.stringify({ taskId: String(breadcrumbTaskId), at: new Date().toISOString() }),
|
|
180
|
+
'utf8'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return home;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const isolatedHome = (home) => ({ HOME: home, USERPROFILE: home });
|
|
187
|
+
|
|
188
|
+
function startStub(opts = {}) {
|
|
189
|
+
const src = opts.claimStatus ? claimStubSrc(opts.claimStatus) : STUB_SRC;
|
|
190
|
+
const proc = spawn(process.execPath, ['-e', src], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
109
191
|
return new Promise((resolve, reject) => {
|
|
110
192
|
let buf = '';
|
|
111
193
|
const timer = setTimeout(() => { proc.kill(); reject(new Error('stub server did not report a port')); }, 10_000);
|
|
@@ -152,6 +234,177 @@ test('api.js routes its post-request exits through cliExit, not process.exit', (
|
|
|
152
234
|
assert.match(afterRequest, /cliExit\(/, 'the post-request paths must set the exit code via cliExit');
|
|
153
235
|
});
|
|
154
236
|
|
|
237
|
+
// ---- claim.js + land-watch.js (task 1002592) -------------------------------
|
|
238
|
+
//
|
|
239
|
+
// WHAT EACH KIND OF TEST BELOW ACTUALLY GUARDS — measured, not assumed, because
|
|
240
|
+
// claiming otherwise is how a suite comes to look like coverage it does not have.
|
|
241
|
+
// With the hard exits deliberately restored in both scripts, the STATIC checks
|
|
242
|
+
// went red and every behavioural test below stayed GREEN, including the ones that
|
|
243
|
+
// make a real HTTP round trip to the local stub. That reproduces, for these two
|
|
244
|
+
// CLIs, what task 1003772 found for main-audit.js: a localhost stub tears down too
|
|
245
|
+
// little machinery to trip the abort. The aborts these tests exist for were all
|
|
246
|
+
// measured against the REAL API over TLS.
|
|
247
|
+
//
|
|
248
|
+
// So: the static checks are the net for the ABORT. The behavioural tests are still
|
|
249
|
+
// worth their runtime — they pin the exit CODES callers branch on (claim 0 vs 1,
|
|
250
|
+
// land-watch 0 on every path), they prove the scripts TERMINATE rather than hang,
|
|
251
|
+
// which is the documented conversion trap, and the refusal case proves the
|
|
252
|
+
// REBASE_REQUIRED rendering reaches a terminal end-to-end.
|
|
253
|
+
// Both were MEASURED aborting 3/3 on Windows 11 / Node v24.19.0 before the
|
|
254
|
+
// conversion, and 5/5 clean after — claim.js on its refusal path (exit 127 where
|
|
255
|
+
// it computed 1) and land-watch.js on its network path (exit 127 where it always
|
|
256
|
+
// intends 0). Task 1002734 deliberately left the wider CLI surface alone and set
|
|
257
|
+
// the bar at "found ABORTING, not merely fetching-then-exiting"; these two cleared
|
|
258
|
+
// it. Neither file arms a timer, so the documented conversion trap — a hard exit
|
|
259
|
+
// masking a leak, turning the crash into a HANG — does not apply here; the
|
|
260
|
+
// behavioural runs below still assert termination rather than assuming it.
|
|
261
|
+
|
|
262
|
+
test('claim.js routes its post-request exits through cliExit, not process.exit', () => {
|
|
263
|
+
const src = fs.readFileSync(CLAIM, 'utf8');
|
|
264
|
+
// The pre-request guards stay as hard exits, exactly as api.js kept its two:
|
|
265
|
+
// the worktree guard and the usage banner both run before any network work.
|
|
266
|
+
// Everything from the claim POST onward must drain instead.
|
|
267
|
+
// Anchor on the CLAIM request itself, not on `cliClient()` — the client is also
|
|
268
|
+
// built inside best-effort helpers defined near the top of the file, so that
|
|
269
|
+
// string would slice from the wrong place and quietly measure nothing.
|
|
270
|
+
const marker = 'let r = await postClaim();';
|
|
271
|
+
const i = src.indexOf(marker);
|
|
272
|
+
assert.ok(i !== -1, 'could not find the claim request site in claim.js — this guard is reading the wrong shape');
|
|
273
|
+
assert.equal(src.split(marker).length - 1, 1, 'the anchor must be unique or the slice is arbitrary');
|
|
274
|
+
const afterRequest = src.slice(i).replace(/\/\/[^\n]*/g, '');
|
|
275
|
+
assert.ok(
|
|
276
|
+
!/process\.exit\s*\(/.test(afterRequest),
|
|
277
|
+
'no process.exit() may remain after the claim request is issued — that is what aborts node on Windows '
|
|
278
|
+
+ 'and turns a refusal\'s exit 1 into 127'
|
|
279
|
+
);
|
|
280
|
+
assert.match(afterRequest, /cliExit\(/, 'the post-request paths must set the exit code via cliExit');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('claim.js keeps its PRE-request guards as hard exits', () => {
|
|
284
|
+
const src = fs.readFileSync(CLAIM, 'utf8');
|
|
285
|
+
const beforeRequest = src.slice(0, src.indexOf('let r = await postClaim();'));
|
|
286
|
+
assert.match(
|
|
287
|
+
beforeRequest, /process\.exit\s*\(/,
|
|
288
|
+
'the usage + worktree guards run before any network work; converting them buys nothing '
|
|
289
|
+
+ 'and a cliExit there would let execution continue past a refusal'
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('land-watch.js routes every exit through cliExit, not process.exit', () => {
|
|
294
|
+
const src = fs.readFileSync(LAND_WATCH, 'utf8');
|
|
295
|
+
const stripped = src.replace(/\/\/[^\n]*/g, '');
|
|
296
|
+
assert.ok(
|
|
297
|
+
!/process\.exit\s*\(/.test(stripped),
|
|
298
|
+
'no process.exit() may remain in land-watch.js — it is HOOK-INVOKED and exits 0 on every path '
|
|
299
|
+
+ 'on purpose, so the abort made the one script written never to fail always report 127'
|
|
300
|
+
);
|
|
301
|
+
assert.match(src, /cliExit\(/, 'the exit paths must set the exit code via cliExit');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('land-watch.js exits 0 and TERMINATES with no pending lands (fast path, no network)', () => {
|
|
305
|
+
const r = spawnSync(process.execPath, [LAND_WATCH], {
|
|
306
|
+
cwd: ROOT,
|
|
307
|
+
encoding: 'utf8',
|
|
308
|
+
timeout: 60_000, // dropping a hard exit trades a crash for a HANG if a handle leaks
|
|
309
|
+
env: { ...process.env, ...atBase(DEAD_BASE), ...isolatedHome(makeHome()) },
|
|
310
|
+
});
|
|
311
|
+
assert.notEqual(r.signal, 'SIGTERM', 'timed out — draining left a handle armed');
|
|
312
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `native abort on the land-watch path:\n${r.stderr}`);
|
|
313
|
+
assert.equal(r.status, 0, `land-watch must exit 0 on every path, got ${r.status} (127 = the abort)`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// THE PATH THAT ACTUALLY ABORTED. The fast path above returns before any network
|
|
317
|
+
// work, so on its own it proves nothing about this bug: the measured 3/3 aborts
|
|
318
|
+
// were on the NETWORK path (breadcrumb present → watch() → fetchTask() → a real
|
|
319
|
+
// round trip → exit). Plant a breadcrumb in an isolated HOME and serve a real 200
|
|
320
|
+
// so the full teardown runs, exactly as api.js's stub-server test does.
|
|
321
|
+
test('land-watch.js exits 0 after a REAL request — the teardown that aborted 3/3', async () => {
|
|
322
|
+
const { proc, port } = await startStub();
|
|
323
|
+
try {
|
|
324
|
+
for (let i = 0; i < 3; i++) {
|
|
325
|
+
// classifyLand drops a non-shipped breadcrumb after one pass, so re-plant it.
|
|
326
|
+
const home = makeHome({ apiBase: `http://127.0.0.1:${port}`, breadcrumbTaskId: '4242' });
|
|
327
|
+
const r = spawnSync(process.execPath, [LAND_WATCH], {
|
|
328
|
+
cwd: ROOT,
|
|
329
|
+
encoding: 'utf8',
|
|
330
|
+
timeout: 60_000,
|
|
331
|
+
env: { ...process.env, ...atBase(`http://127.0.0.1:${port}`), ...isolatedHome(home) },
|
|
332
|
+
});
|
|
333
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — draining left a handle armed`);
|
|
334
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively on the network path:\n${r.stderr}`);
|
|
335
|
+
assert.equal(r.status, 0, `run ${i + 1} exited ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
336
|
+
// Prove the run really took the network path rather than the idle fast-path,
|
|
337
|
+
// or this test would silently degrade into the one above.
|
|
338
|
+
assert.doesNotMatch(r.stderr || '', /no pending lands/,
|
|
339
|
+
`run ${i + 1} took the idle fast path — the breadcrumb was not seen, so the network teardown was never exercised`);
|
|
340
|
+
}
|
|
341
|
+
} finally {
|
|
342
|
+
proc.kill();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// claim.js. Its exits are post-request and it was measured aborting 3/3 on the
|
|
347
|
+
// refusal path. Both paths below run against the stub in an isolated HOME with a
|
|
348
|
+
// throwaway session, so no real API is touched and the developer's own config is
|
|
349
|
+
// never read or written. `--here` because CI may check out the MAIN checkout,
|
|
350
|
+
// where the worktree guard would refuse at exit 2 before any request is made.
|
|
351
|
+
test('claim.js exits 1 without aborting on a REFUSED claim — measured 3/3 aborting before', async () => {
|
|
352
|
+
const { proc, port } = await startStub({ claimStatus: 409 });
|
|
353
|
+
try {
|
|
354
|
+
const home = makeHome({ apiBase: `http://127.0.0.1:${port}` });
|
|
355
|
+
for (let i = 0; i < 3; i++) {
|
|
356
|
+
const r = spawnSync(process.execPath, [CLAIM, '4242', '--here'], {
|
|
357
|
+
cwd: ROOT,
|
|
358
|
+
encoding: 'utf8',
|
|
359
|
+
timeout: 60_000,
|
|
360
|
+
env: { ...process.env, ...atBase(`http://127.0.0.1:${port}`), ...isolatedHome(home) },
|
|
361
|
+
});
|
|
362
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — draining left a handle armed`);
|
|
363
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively:\n${r.stderr}`);
|
|
364
|
+
assert.equal(r.status, 1, `a refused claim must exit 1, got ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
365
|
+
// End-to-end proof that the new REBASE_REQUIRED rendering reaches a terminal,
|
|
366
|
+
// not just the unit test: the stub refuses with that exact envelope.
|
|
367
|
+
//
|
|
368
|
+
// Assert on wording only the BRANCH produces. Matching the hint text alone
|
|
369
|
+
// would pass with the branch disabled, because the generic fallback also
|
|
370
|
+
// prints d.hint now — measured, not assumed: a probe that disabled the branch
|
|
371
|
+
// kept this test green until these two assertions replaced it.
|
|
372
|
+
assert.match(r.stderr, /queue is gated/i, 'the REBASE_REQUIRED branch must render, not the bare fallback');
|
|
373
|
+
assert.match(r.stderr, /flagged: strand:unresolvable_conflict/, 'the branch must surface why the task was flagged');
|
|
374
|
+
assert.match(r.stderr, /strand-fix/, 'the server hint must reach the builder, not be dropped on the floor');
|
|
375
|
+
assert.match(r.stderr, /task 1003575/, 'the owed task must be named');
|
|
376
|
+
}
|
|
377
|
+
} finally {
|
|
378
|
+
proc.kill();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// THE OPEN QUESTION THE TASK FLAGGED, now answered rather than assumed: a
|
|
383
|
+
// SUCCESSFUL claim leaves main() by `return` with no explicit exit, so it was never
|
|
384
|
+
// covered by the refusal measurement above. It is also the heavier teardown — the
|
|
385
|
+
// success path makes several further best-effort calls after the claim — which is
|
|
386
|
+
// exactly the shape that aborted for api.js. Measured here: clean, exit 0.
|
|
387
|
+
test('claim.js exits 0 without aborting on a SUCCESSFUL claim — the heavier teardown', async () => {
|
|
388
|
+
const { proc, port } = await startStub({ claimStatus: 201 });
|
|
389
|
+
try {
|
|
390
|
+
const home = makeHome({ apiBase: `http://127.0.0.1:${port}` });
|
|
391
|
+
for (let i = 0; i < 3; i++) {
|
|
392
|
+
const r = spawnSync(process.execPath, [CLAIM, '4242', '--here'], {
|
|
393
|
+
cwd: ROOT,
|
|
394
|
+
encoding: 'utf8',
|
|
395
|
+
timeout: 60_000,
|
|
396
|
+
env: { ...process.env, ...atBase(`http://127.0.0.1:${port}`), ...isolatedHome(home) },
|
|
397
|
+
});
|
|
398
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — draining left a handle armed`);
|
|
399
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively on the success path:\n${r.stderr}`);
|
|
400
|
+
assert.equal(r.status, 0, `a successful claim must exit 0, got ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
401
|
+
assert.match(r.stdout, /Claimed task/, 'the success output must still be produced');
|
|
402
|
+
}
|
|
403
|
+
} finally {
|
|
404
|
+
proc.kill();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
155
408
|
// ---- main-audit.js — the deploy gate (task 1003772) ------------------------
|
|
156
409
|
// Ranges are always HEAD..HEAD: it resolves on every checkout including a shallow
|
|
157
410
|
// CI clone, audits zero commits, and still runs the WHOLE pipeline — fetchRankMap
|
package/tests/currency_label.mjs
CHANGED
|
@@ -51,11 +51,14 @@ test('clientBranding exposes project policy — the settings read-back (task 100
|
|
|
51
51
|
// `visibility` — the join door — joined the block in task 1002324 (ADR 0182 D1);
|
|
52
52
|
// `joinGrant` — what a PUBLIC join grants — in task 1002331 (ADR 0247), defaulting
|
|
53
53
|
// to `full`, the value that narrows nothing, so an un-pushed instance is unchanged
|
|
54
|
+
// `artistGate` joined in task 1003575 (ADR 0241) and is the ONE key here whose
|
|
55
|
+
// fallback is not today's behaviour: 'strict' is the owner's ruling, safe as a
|
|
56
|
+
// default because it can only ever delay a pin move, never a builder's ship.
|
|
54
57
|
assert.deepEqual(clientBranding({}).project,
|
|
55
|
-
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full' });
|
|
58
|
+
{ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict' });
|
|
56
59
|
assert.deepEqual(
|
|
57
|
-
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view' } }).project,
|
|
58
|
-
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view' },
|
|
60
|
+
clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' } }).project,
|
|
61
|
+
{ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' },
|
|
59
62
|
'what the instance actually resolved (env override included) is what it publishes');
|
|
60
63
|
});
|
|
61
64
|
|
|
@@ -74,7 +77,11 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
|
|
|
74
77
|
// `project` joined the allow-list in task 1003140: the per-project policy is public
|
|
75
78
|
// by nature and is the settings read-back a manage page needs (see clientBranding).
|
|
76
79
|
assert.deepEqual(Object.keys(projected).sort(), ['copy', 'currency', 'identity', 'project', 'rankLabels', 'reward', 'theme']);
|
|
77
|
-
|
|
80
|
+
// `artistGate` joined in task 1003575 (ADR 0241) — the project's artist strictness,
|
|
81
|
+
// public for the same read-back reason as its neighbours. Its companion stamp
|
|
82
|
+
// (artistGateSince) deliberately does NOT: that is platform bookkeeping the server
|
|
83
|
+
// reads, and no browser surface has a question it answers.
|
|
84
|
+
assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'visibility']);
|
|
78
85
|
for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
|
|
79
86
|
assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
|
|
80
87
|
}
|
|
@@ -170,5 +170,50 @@ await t('a DB error returns {error}, never throws', async () => {
|
|
|
170
170
|
assert.equal(r.outcome, 'error');
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
// ---- the outbound half must not outlive the inbound half (task 1003400) ----
|
|
174
|
+
//
|
|
175
|
+
// The 👍 this line invites is applied ONLY by discord-approvals.js, which lives in the
|
|
176
|
+
// `discord` module. onboarding (which posts) is default-ON; discord is default-OFF; and
|
|
177
|
+
// nothing gated one on the other. A default instance therefore posted a working-looking
|
|
178
|
+
// approve/decline control into a channel where no handler was listening — an Archon
|
|
179
|
+
// reacts, sees it land, and believes they admitted a builder who is still locked out.
|
|
180
|
+
|
|
181
|
+
await t('discord module OFF → the control is NOT posted', async () => {
|
|
182
|
+
let posted = false;
|
|
183
|
+
const r = await bcast.postMemberApprovalRequest(
|
|
184
|
+
{ id: 7, github_login: 'carol' },
|
|
185
|
+
{ postWebhook: async () => { posted = true; }, log: () => {}, isModuleEnabled: () => false },
|
|
186
|
+
);
|
|
187
|
+
assert.equal(posted, false, 'a 👍 nobody applies must never be invited');
|
|
188
|
+
assert.equal(r.skipped, 'discord_module_off');
|
|
189
|
+
assert.equal(r.posted, false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await t('discord module ON → the control posts exactly as before', async () => {
|
|
193
|
+
let line = null;
|
|
194
|
+
const r = await bcast.postMemberApprovalRequest(
|
|
195
|
+
{ id: 7, github_login: 'carol' },
|
|
196
|
+
{ postWebhook: async (_ch, l) => { line = l; }, log: () => {}, isModuleEnabled: () => true },
|
|
197
|
+
);
|
|
198
|
+
assert.equal(r.posted, true, 'the working path is untouched');
|
|
199
|
+
assert.deepEqual(ap.parseApprovalMarker(line), { kind: 'member', id: 7 }, 'and still round-trips');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await t('enablement unresolvable → fails CLOSED (no post)', async () => {
|
|
203
|
+
let posted = false;
|
|
204
|
+
const r = await bcast.postMemberApprovalRequest(
|
|
205
|
+
{ id: 7, github_login: 'carol' },
|
|
206
|
+
{ postWebhook: async () => { posted = true; }, log: () => {}, isModuleEnabled: () => { throw new Error('config unreadable'); } },
|
|
207
|
+
);
|
|
208
|
+
assert.equal(posted, false, 'a state we cannot vouch for is not a state we post into');
|
|
209
|
+
assert.equal(r.skipped, 'discord_module_off');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await t('the gate is reachable through the published doorway, not a direct core require', async () => {
|
|
213
|
+
const doorway = require('../src/module-api.js');
|
|
214
|
+
assert.equal(typeof doorway.isModuleEnabled, 'function', 'ADR 0083: modules read enablement here');
|
|
215
|
+
assert.throws(() => doorway.isModuleEnabled('no-such-module'), /unknown module/, 'a typo must be a bug, not a silent false');
|
|
216
|
+
});
|
|
217
|
+
|
|
173
218
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
174
219
|
process.exit(failed ? 1 : 0);
|