@bongos/core 1.17.0
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 +5727 -0
- package/.claude/skills/blocker-review/SKILL.md +91 -0
- package/.claude/skills/blocker-solve/SKILL.md +96 -0
- package/.claude/skills/builder-backup/SKILL.md +54 -0
- package/.claude/skills/builder-box/SKILL.md +45 -0
- package/.claude/skills/builder-claim/SKILL.md +76 -0
- package/.claude/skills/builder-connect/SKILL.md +59 -0
- package/.claude/skills/builder-cost/SKILL.md +47 -0
- package/.claude/skills/builder-end/SKILL.md +81 -0
- package/.claude/skills/builder-exit/SKILL.md +88 -0
- package/.claude/skills/builder-key/SKILL.md +40 -0
- package/.claude/skills/builder-reauth/SKILL.md +50 -0
- package/.claude/skills/builder-redteam/SKILL.md +109 -0
- package/.claude/skills/builder-release/SKILL.md +41 -0
- package/.claude/skills/builder-setup/SKILL.md +62 -0
- package/.claude/skills/builder-ship/SKILL.md +79 -0
- package/.claude/skills/builder-stage/SKILL.md +51 -0
- package/.claude/skills/builder-start/SKILL.md +46 -0
- package/.claude/skills/close-box/SKILL.md +43 -0
- package/.claude/skills/design-sync/SKILL.md +69 -0
- package/.claude/skills/dev/SKILL.md +49 -0
- package/.claude/skills/feedback/SKILL.md +32 -0
- package/.claude/skills/figma-design-sync/SKILL.md +74 -0
- package/.claude/skills/goal-review/SKILL.md +89 -0
- package/.claude/skills/idea-triage/SKILL.md +144 -0
- package/.claude/skills/ideate/SKILL.md +82 -0
- package/.claude/skills/merge-mode/SKILL.md +169 -0
- package/.claude/skills/new-project/SKILL.md +108 -0
- package/.claude/skills/otb-character-review/SKILL.md +71 -0
- package/.claude/skills/otb-design-review/SKILL.md +47 -0
- package/.claude/skills/otb-feedback-capture/SKILL.md +45 -0
- package/.claude/skills/otb-figma-sync/SKILL.md +54 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +45 -0
- package/.claude/skills/paint/SKILL.md +70 -0
- package/.claude/skills/planning-session/SKILL.md +299 -0
- package/.claude/skills/priority-session/SKILL.md +134 -0
- package/.claude/skills/read-session-export/SKILL.md +58 -0
- package/.claude/skills/recall/SKILL.md +52 -0
- package/.claude/skills/session-handoff/SKILL.md +46 -0
- package/.claude/skills/status/SKILL.md +33 -0
- package/.devcontainer/Dockerfile +69 -0
- package/.devcontainer/README.md +73 -0
- package/.devcontainer/devcontainer-lock.json +19 -0
- package/.devcontainer/devcontainer.json +93 -0
- package/.devcontainer/healthcheck.sh +84 -0
- package/.devcontainer/init-firewall.sh +198 -0
- package/.devcontainer/post-create.sh +153 -0
- package/.dockerignore +36 -0
- package/.env.local.example +32 -0
- package/.gitattributes +83 -0
- package/.gitignore +91 -0
- package/CLAUDE.md +242 -0
- package/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +51 -0
- package/Dockerfile +46 -0
- package/GOVERNANCE.md +44 -0
- package/LICENSE +661 -0
- package/README.md +88 -0
- package/bin/bongos.js +222 -0
- package/clients/bongos-client/README.md +48 -0
- package/clients/bongos-client/bongos-client.global.js +617 -0
- package/clients/bongos-client/examples/hello-world.mjs +62 -0
- package/clients/bongos-client/index.cjs +619 -0
- package/clients/bongos-client/index.d.ts +793 -0
- package/clients/bongos-client/index.mjs +614 -0
- package/clients/bongos-client/package.json +32 -0
- package/config/branding.neutral.json +60 -0
- package/config/design-tokens.neutral.json +51 -0
- package/config/design-tokens.schema.json +96 -0
- package/config/modules.neutral.json +9 -0
- package/docker-compose.yml +52 -0
- package/docker-entrypoint.sh +30 -0
- package/docs/adr/0001-stack-choice-single-vps.md +45 -0
- package/docs/adr/0002-digitalocean-over-hetzner.md +27 -0
- package/docs/adr/0003-cloudflare-origin-cert-over-letsencrypt.md +35 -0
- package/docs/adr/0004-amazonprimea-name-and-trademark-acceptance.md +45 -0
- package/docs/adr/0005-invisible-other-players-in-v1.md +37 -0
- package/docs/adr/0006-singleton-worldroom-plus-queueroom.md +30 -0
- package/docs/adr/0007-persistent-identity-via-localstorage-uuid.md +31 -0
- package/docs/adr/0008-family-based-tile-generation.md +86 -0
- package/docs/adr/0008-google-chat-oauth-user-auth.md +37 -0
- package/docs/adr/0009-autotile-architecture.md +181 -0
- package/docs/adr/0010-pms-architecture.md +70 -0
- package/docs/adr/0011-terrain-id-rendering-model.md +174 -0
- package/docs/adr/0012-ground-object-cell-split.md +208 -0
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +158 -0
- package/docs/adr/0014-families-canonical-over-vocabulary.md +103 -0
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +125 -0
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +132 -0
- package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +74 -0
- package/docs/adr/0018-three-rank-model-goes-live.md +97 -0
- package/docs/adr/0019-red-team-bounty-payout-table.md +52 -0
- package/docs/adr/0020-security-model-public-stub.md +70 -0
- package/docs/adr/0021-per-builder-skill-model-preferences.md +99 -0
- package/docs/adr/0022-secrets-policy.md +124 -0
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +118 -0
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +277 -0
- package/docs/adr/0024-multi-agent-system-architecture.md +125 -0
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +59 -0
- package/docs/adr/0025-structured-criterion-task-link.md +44 -0
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +117 -0
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +170 -0
- package/docs/adr/0028-honest-llm-spend-accounting.md +109 -0
- package/docs/adr/0029-status-page-outage-resilience.md +59 -0
- package/docs/adr/0030-builders-hall-live-updates.md +59 -0
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +222 -0
- package/docs/adr/0032-discord-mirror-of-gds.md +50 -0
- package/docs/adr/0033-discord-bot-service-principal.md +82 -0
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +53 -0
- package/docs/adr/0035-builder-onboarding-three-paths.md +128 -0
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +40 -0
- package/docs/adr/0037-discord-channel-management.md +51 -0
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +111 -0
- package/docs/adr/0039-setup-first-onboarding-ux.md +49 -0
- package/docs/adr/0040-remote-control-default-browser-onramp.md +47 -0
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +40 -0
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +82 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +74 -0
- package/docs/adr/0044-mediterranean-palette-replacement.md +73 -0
- package/docs/adr/0044-per-box-live-game-preview.md +57 -0
- package/docs/adr/0045-devbox-desktop-app.md +133 -0
- package/docs/adr/0046-sandbox-first-review-gate.md +72 -0
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +36 -0
- package/docs/adr/0048-works-in-progress-ia-split.md +32 -0
- package/docs/adr/0049-split-parallel-safety-contract.md +46 -0
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +43 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +57 -0
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +70 -0
- package/docs/adr/0053-scoped-dev-box-session.md +55 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +63 -0
- package/docs/adr/0055-server-mediated-branch-publish.md +47 -0
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +62 -0
- package/docs/adr/0057-container-cost-ledger.md +28 -0
- package/docs/adr/0057-discord-archon-approval-channels.md +64 -0
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +64 -0
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +37 -0
- package/docs/adr/0060-gds-retrieval-layer.md +97 -0
- package/docs/adr/0061-context-layer-decomposition.md +61 -0
- package/docs/adr/0062-medusa-instance-model-decoupling.md +107 -0
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +44 -0
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +49 -0
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +69 -0
- package/docs/adr/0066-generated-file-map-sections.md +51 -0
- package/docs/adr/0067-session-start-fast-forward-pull.md +43 -0
- package/docs/adr/0068-shared-art-key-for-newcomers.md +38 -0
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +35 -0
- package/docs/adr/0070-deterministic-session-card-delivery.md +44 -0
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +59 -0
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +35 -0
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +39 -0
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +64 -0
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +37 -0
- package/docs/adr/0074-memory-map-stays-light.md +32 -0
- package/docs/adr/0075-per-system-cost-attribution.md +37 -0
- package/docs/adr/0076-in-session-deterministic-compaction.md +58 -0
- package/docs/adr/0077-content-addressed-llm-cache.md +32 -0
- package/docs/adr/0078-deterministic-first-cron.md +48 -0
- package/docs/adr/0079-value-gated-autonomous-engagement.md +39 -0
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +46 -0
- package/docs/adr/0081-tool-agnostic-design-layer.md +62 -0
- package/docs/adr/0082-productized-key-storage-provisioning.md +32 -0
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +55 -0
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +143 -0
- package/docs/adr/0084-auto-derived-per-task-rank-floor.md +86 -0
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +39 -0
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +123 -0
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +49 -0
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +65 -0
- package/docs/adr/0088-consistent-character-generative-animation.md +169 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +179 -0
- package/docs/adr/0090-metic-task-authoring.md +73 -0
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +106 -0
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +117 -0
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +72 -0
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +49 -0
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +65 -0
- package/docs/adr/0095-cross-agent-context-management.md +58 -0
- package/docs/adr/0096-require-reward-before-workable.md +209 -0
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +46 -0
- package/docs/adr/0097-retroactive-reward-backfill.md +124 -0
- package/docs/adr/0098-public-mirror-publish-manifest.md +92 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +55 -0
- package/docs/adr/0100-two-repo-instance-model.md +60 -0
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +90 -0
- package/docs/adr/0102-finish-bounding-the-kernel.md +36 -0
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +70 -0
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +28 -0
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +42 -0
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +47 -0
- package/docs/adr/0106-goal-owner-manager-authority.md +84 -0
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +83 -0
- package/docs/adr/0108-instance-composition-configurable-root.md +87 -0
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +44 -0
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +49 -0
- package/docs/adr/0111-instance-hosting-provisioning-module.md +135 -0
- package/docs/adr/0112-private-goals-invitations-join-requests.md +72 -0
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +41 -0
- package/docs/adr/0114-instance-docs-discovery-contract.md +32 -0
- package/docs/adr/0115-scheduled-routines-portability.md +31 -0
- package/docs/adr/0116-api-error-envelope-and-code-taxonomy.md +62 -0
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +50 -0
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +47 -0
- package/docs/adr/0119-api-pagination-contract.md +34 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +108 -0
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +97 -0
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +43 -0
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +36 -0
- package/docs/adr/0124-typed-api-response-bodies.md +37 -0
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +36 -0
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +30 -0
- package/docs/adr/0127-cross-box-fleet-observability.md +64 -0
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +46 -0
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +34 -0
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +61 -0
- package/docs/adr/0131-rank-scoped-skill-visibility.md +47 -0
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +54 -0
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +42 -0
- package/docs/adr/0134-private-first-npm-distribution.md +38 -0
- package/docs/adr/README.md +208 -0
- package/docs/api/openapi.json +13720 -0
- package/docs/api-reference.md +437 -0
- package/docs/architecture.md +244 -0
- package/docs/branding-contract.md +66 -0
- package/docs/branding-fill-prompt.md +25 -0
- package/docs/canonical-permissions.md +241 -0
- package/docs/design/container-exterior-tiles.md +49 -0
- package/docs/design/container-exterior.md +81 -0
- package/docs/design/container-interior.md +77 -0
- package/docs/design/modular-architecture/00-research-report.md +107 -0
- package/docs/design/modular-architecture/01-architecture.md +125 -0
- package/docs/design/modular-architecture/02-module-map.md +116 -0
- package/docs/design/modular-architecture/03-builder-flows.md +104 -0
- package/docs/design/modular-architecture/04-module-lifecycle.md +62 -0
- package/docs/design/modular-architecture/README.md +27 -0
- package/docs/design/vanilla-hall-ui-redesign-scope.md +284 -0
- package/docs/design/world-map-scroll.md +41 -0
- package/docs/design-contract.md +320 -0
- package/docs/file-map.md +418 -0
- package/docs/handoff-template.md +92 -0
- package/docs/modules-contract.md +293 -0
- package/docs/onboarding/browser-terminal-guide.md +73 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +28 -0
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +54 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +37 -0
- package/docs/onboarding/diagrams/04-architecture.mmd +51 -0
- package/docs/onboarding/diagrams/05-versioning.mmd +29 -0
- package/docs/onboarding/diagrams/README.md +94 -0
- package/docs/onboarding/diagrams/assertions.json +170 -0
- package/docs/onboarding/diagrams/puppeteer-config.json +1 -0
- package/docs/onboarding/diagrams/render-config.json +9 -0
- package/docs/onboarding/drachmae-and-karma.md +178 -0
- package/docs/onboarding/primer.md +202 -0
- package/docs/project-context.template.md +67 -0
- package/docs/recipes/README.md +35 -0
- package/docs/recipes/audit-replay.md +100 -0
- package/docs/recipes/bongos-cli-release.md +66 -0
- package/docs/recipes/builder-box-lifecycle.md +152 -0
- package/docs/recipes/claude-design-loop.md +143 -0
- package/docs/recipes/claude-gemini-bongos-doors.md +155 -0
- package/docs/recipes/cloudbongos-staging-gate.md +139 -0
- package/docs/recipes/cloudbongos-standup.md +111 -0
- package/docs/recipes/devbox-app-release.md +93 -0
- package/docs/recipes/devbox-app-windows-test.md +128 -0
- package/docs/recipes/failed-ship-recovery-proof.md +102 -0
- package/docs/recipes/figma-design-loop.md +129 -0
- package/docs/recipes/gds-db-backup.md +255 -0
- package/docs/recipes/instance-deploy.md +37 -0
- package/docs/recipes/managed-settings-remote-control.md +416 -0
- package/docs/recipes/memory-backup.md +71 -0
- package/docs/recipes/memory-sync-conflicts.md +98 -0
- package/docs/recipes/multi-builder-merge.md +78 -0
- package/docs/recipes/ops-gotchas.md +212 -0
- package/docs/recipes/overnight-builder.md +53 -0
- package/docs/recipes/packaging-the-core.md +49 -0
- package/docs/recipes/parallel-load-results-2026-05-30.md +63 -0
- package/docs/recipes/parallel-load-test.md +32 -0
- package/docs/recipes/pixel-art-pipeline.md +411 -0
- package/docs/recipes/private-npm-distribution.md +79 -0
- package/docs/recipes/search-index-ingestion.md +67 -0
- package/docs/recipes/self-host.md +60 -0
- package/docs/recipes/ship-and-regrade.md +72 -0
- package/docs/recipes/staging-data.md +50 -0
- package/docs/recipes/staging-environment.md +215 -0
- package/docs/recipes/upgrading-the-core.md +58 -0
- package/docs/research/cursor-competitive-analysis.md +255 -0
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +227 -0
- package/migrations/003_pms.sql +284 -0
- package/migrations/004_pms_cost_idempotency.sql +13 -0
- package/migrations/005_pms_smoke_filter.sql +44 -0
- package/migrations/006_pms_v2.sql +378 -0
- package/migrations/007_pms_v2_hierarchy_trigger.sql +59 -0
- package/migrations/008_pms_v2_migration_allocator.sql +27 -0
- package/migrations/009_pms_v2_blockers_table.sql +68 -0
- package/migrations/010_uptime.sql +29 -0
- package/migrations/011_backfill_v2_v3_done_when.sql +20 -0
- package/migrations/012_pms_v2_task_lifecycle_states.sql +82 -0
- package/migrations/013_claim_locking.sql +22 -0
- package/migrations/014_pms_task_discipline.sql +86 -0
- package/migrations/015_pms_estimation_calibration.sql +140 -0
- package/migrations/016_pms_task_grades.sql +60 -0
- package/migrations/017_version_progress_exclude_abandoned.sql +49 -0
- package/migrations/019_rename_pms_v3_to_gds_v3.sql +65 -0
- package/migrations/020_task_dependencies.sql +99 -0
- package/migrations/021_v3_renumber_and_deps.sql +224 -0
- package/migrations/022_v3_reconcile_dep_gated_status.sql +41 -0
- package/migrations/023_pms_rank.sql +30 -0
- package/migrations/025_builder_exit_status.sql +15 -0
- package/migrations/027_builders_preferred_disciplines.sql +46 -0
- package/migrations/028_tasks_parallel_safe.sql +71 -0
- package/migrations/029_rank_three_live.sql +32 -0
- package/migrations/030_vulnerability_reports.sql +61 -0
- package/migrations/031_reward_simplification.sql +57 -0
- package/migrations/032_repo_health_snapshots.sql +56 -0
- package/migrations/033_audit_log.sql +64 -0
- package/migrations/034_lifecycle_breakdown_leaf_only.sql +62 -0
- package/migrations/035_auth_token_hygiene.sql +66 -0
- package/migrations/037_builder_skill_model_prefs.sql +44 -0
- package/migrations/038_task_override_requests.sql +51 -0
- package/migrations/039_newcomer_friendly_column.sql +124 -0
- package/migrations/040_builder_karma.sql +58 -0
- package/migrations/041_kind_multipliers_and_idea_bonus.sql +42 -0
- package/migrations/042_criterion_proposal_workflow.sql +78 -0
- package/migrations/043_builder_onboarding_observability.sql +66 -0
- package/migrations/044_tasks_requires_rank.sql +97 -0
- package/migrations/045_builder_onboarding_state.sql +147 -0
- package/migrations/046_cost_log_builder_attribution.sql +42 -0
- package/migrations/054_builder_memory.sql +108 -0
- package/migrations/055_task_criteria.sql +90 -0
- package/migrations/058_peer_votes.sql +41 -0
- package/migrations/059_onboarding_pick_disciplines.sql +59 -0
- package/migrations/060_access_requests.sql +46 -0
- package/migrations/061_backfill_override_ship_attribution.sql +29 -0
- package/migrations/062_session_records.sql +34 -0
- package/migrations/063_session_records_tool_errors_idx.sql +11 -0
- package/migrations/064_grade_attempts.sql +18 -0
- package/migrations/065_bfg_principal.sql +58 -0
- package/migrations/066_live_notify_triggers.sql +69 -0
- package/migrations/067_builder_boxes.sql +117 -0
- package/migrations/067_discord_bot_foundation.sql +69 -0
- package/migrations/068_thetes_requires_rank.sql +27 -0
- package/migrations/069_builder_ssh_keys.sql +66 -0
- package/migrations/070_box_intents.sql +101 -0
- package/migrations/071_box_first_provision_approval.sql +46 -0
- package/migrations/072_onboarding_ship_three_remove_primer.sql +80 -0
- package/migrations/073_box_terminal_access.sql +46 -0
- package/migrations/075_builder_sound_prefs.sql +28 -0
- package/migrations/078_box_claude_active.sql +22 -0
- package/migrations/079_box_first_connected_at.sql +20 -0
- package/migrations/081_tasks_needs_migration.sql +34 -0
- package/migrations/084_tasks_security_sensitive.sql +24 -0
- package/migrations/085_security_engagement_docs.sql +34 -0
- package/migrations/086_rename_disciplines.sql +125 -0
- package/migrations/088_builders_anthropic_link.sql +28 -0
- package/migrations/089_builders_monthly_budget.sql +19 -0
- package/migrations/090_box_scoped_sessions.sql +38 -0
- package/migrations/091_session_token_reward.sql +44 -0
- package/migrations/093_tasks_sandbox_review.sql +22 -0
- package/migrations/102_remove_box_first_provision_approval.sql +58 -0
- package/migrations/103_search_chunks.sql +90 -0
- package/migrations/104_builder_box_blocked.sql +34 -0
- package/migrations/105_box_host_keys.sql +35 -0
- package/migrations/111_medusa_customer0_framing.sql +35 -0
- package/migrations/112_drop_track_check.sql +25 -0
- package/migrations/113_merge_lease.sql +16 -0
- package/migrations/114_cloudbongos_rename_live_strings.sql +32 -0
- package/migrations/118_currency_label_decoupling.sql +23 -0
- package/migrations/119_builder_behavior_prefs.sql +31 -0
- package/migrations/121_box_code_version.sql +37 -0
- package/migrations/122_builder_gemini_key.sql +30 -0
- package/migrations/123_cost_log_tiers.sql +33 -0
- package/migrations/124_cost_baseline.sql +45 -0
- package/migrations/125_llm_cache.sql +54 -0
- package/migrations/126_autonomy_runs.sql +33 -0
- package/migrations/143_cost_calibration.sql +63 -0
- package/migrations/145_backfill_task_requires_rank.sql +84 -0
- package/migrations/160_goal_tier.sql +88 -0
- package/migrations/163_polymorphic_dependencies.sql +187 -0
- package/migrations/164_event_sound_prefs.sql +20 -0
- package/migrations/165_task_module_key.sql +15 -0
- package/migrations/171_claims_published_head_sha.sql +24 -0
- package/migrations/174_close_orphaned_claims_on_terminal_tasks.sql +30 -0
- package/migrations/175_task_grades_diff_hash.sql +9 -0
- package/migrations/176_promotion_respects_open_blockers.sql +58 -0
- package/migrations/176_ui_discipline.sql +40 -0
- package/migrations/177_require_reward_before_workable.sql +76 -0
- package/migrations/178_reward_gate_auto_assign_revert_autopromote.sql +61 -0
- package/migrations/179_search_chunk_usefulness.sql +33 -0
- package/migrations/180_one_active_claim_per_session.sql +54 -0
- package/migrations/181_cost_log_skill_name.sql +33 -0
- package/migrations/184_claim_release_requested.sql +25 -0
- package/migrations/185_module_upstreaming_goal_wiring.sql +59 -0
- package/migrations/187_goal_sort_order.sql +30 -0
- package/migrations/187_tasks_kind_check_add_verify.sql +21 -0
- package/migrations/README.md +88 -0
- package/migrations/core_188_goal_visibility_and_membership_requests.sql +71 -0
- package/migrations/core_189_core_upgrades_ledger.sql +37 -0
- package/migrations/core_189_credit_log_task_id_index.sql +19 -0
- package/migrations/core_190_rebuild_claimable_task_views.sql +71 -0
- package/migrations/instance/018_pms_v3_planning_seed.sql +131 -0
- package/migrations/instance/080_gds_v4_planning_seed.sql +75 -0
- package/migrations/instance/110_medusa_planning_seed.sql +70 -0
- package/migrations/instance/120_bongos_v1_planning_seed.sql +69 -0
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +26 -0
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +29 -0
- package/migrations/instance/172_v2_world_planning_seed.sql +163 -0
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +33 -0
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +132 -0
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +116 -0
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +69 -0
- package/modules/autonomy/CLAUDE.md +49 -0
- package/modules/autonomy/db.js +50 -0
- package/modules/autonomy/module.json +13 -0
- package/modules/autonomy/pollers/routine-timers.js +146 -0
- package/modules/autonomy/routes/autonomy.js +37 -0
- package/modules/builder-settings/CLAUDE.md +58 -0
- package/modules/builder-settings/builder-needs.js +115 -0
- package/modules/builder-settings/db.js +168 -0
- package/modules/builder-settings/display-name.js +84 -0
- package/modules/builder-settings/event-sound-prefs.js +169 -0
- package/modules/builder-settings/interaction-prefs.js +201 -0
- package/modules/builder-settings/model-alloc-prefs.js +147 -0
- package/modules/builder-settings/module.json +10 -0
- package/modules/builder-settings/render-prefs.js +103 -0
- package/modules/builder-settings/routes/settings.js +417 -0
- package/modules/builder-settings/settings.js +52 -0
- package/modules/builder-settings/skill-prefs.js +196 -0
- package/modules/builder-settings/sound-prefs.js +178 -0
- package/modules/builder-settings/wandering-prefs.js +282 -0
- package/modules/dev-box/CLAUDE.md +38 -0
- package/modules/dev-box/box-access.js +275 -0
- package/modules/dev-box/box-credential.js +94 -0
- package/modules/dev-box/box-onboard.js +486 -0
- package/modules/dev-box/boxes.js +842 -0
- package/modules/dev-box/db.js +39 -0
- package/modules/dev-box/module.json +14 -0
- package/modules/dev-box/routes/box.js +977 -0
- package/modules/discord/CLAUDE.md +50 -0
- package/modules/discord/auth.js +54 -0
- package/modules/discord/bug-attachments.js +210 -0
- package/modules/discord/db.js +96 -0
- package/modules/discord/discord-approvals.js +145 -0
- package/modules/discord/discord-audit.js +29 -0
- package/modules/discord/discord-bot.js +545 -0
- package/modules/discord/discord-bugs.js +257 -0
- package/modules/discord/discord-channels.js +412 -0
- package/modules/discord/discord-inbound.js +147 -0
- package/modules/discord/discord-message-buffer.js +54 -0
- package/modules/discord/discord-reaction.js +186 -0
- package/modules/discord/discord-role-sync.js +318 -0
- package/modules/discord/module.json +14 -0
- package/modules/discord/pollers/discord-bot.js +19 -0
- package/modules/discord/routes/bug-attachments.js +44 -0
- package/modules/discord/routes/discord.js +102 -0
- package/modules/discord/ship-broadcast.js +78 -0
- package/modules/discord/webhook.js +74 -0
- package/modules/economy/CLAUDE.md +48 -0
- package/modules/economy/achievements.js +594 -0
- package/modules/economy/cost-classify.js +331 -0
- package/modules/economy/cost-summary-shape.js +90 -0
- package/modules/economy/cost.js +438 -0
- package/modules/economy/credits.js +430 -0
- package/modules/economy/migrations/economy_001_fix_gds_shipper_description.sql +91 -0
- package/modules/economy/module.json +10 -0
- package/modules/economy/reward.js +61 -0
- package/modules/economy/routes/cost.js +97 -0
- package/modules/economy/routes/leaderboard.js +26 -0
- package/modules/economy/routes/reward.js +42 -0
- package/modules/economy/streak.js +285 -0
- package/modules/grading/CLAUDE.md +42 -0
- package/modules/grading/db.js +39 -0
- package/modules/grading/grader-rubric.json +151 -0
- package/modules/grading/grader-workers/efficiency.js +213 -0
- package/modules/grading/grader-workers/extract-json.js +80 -0
- package/modules/grading/grader-workers/fence.js +100 -0
- package/modules/grading/grader-workers/hacker.js +239 -0
- package/modules/grading/grader-workers/index.js +183 -0
- package/modules/grading/grader-workers/narc.js +207 -0
- package/modules/grading/grader-workers/quality.js +198 -0
- package/modules/grading/grader-workers/rubric.js +20 -0
- package/modules/grading/grader-workers/worker-prompt.js +89 -0
- package/modules/grading/grader-workers/worker-verdict.js +50 -0
- package/modules/grading/grader.js +1790 -0
- package/modules/grading/module.json +12 -0
- package/modules/grading/routes/grade.js +51 -0
- package/modules/hall-ui/CLAUDE.md +61 -0
- package/modules/hall-ui/module.json +14 -0
- package/modules/hall-ui/public/atlas.html +180 -0
- package/modules/hall-ui/public/atlas.js +336 -0
- package/modules/hall-ui/public/box-panel.js +423 -0
- package/modules/hall-ui/public/brand-apply.js +117 -0
- package/modules/hall-ui/public/builders.js +3028 -0
- package/modules/hall-ui/public/chime.wav +0 -0
- package/modules/hall-ui/public/copy-on-click.js +174 -0
- package/modules/hall-ui/public/cursors/arrow.png +0 -0
- package/modules/hall-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier-out.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier.png +0 -0
- package/modules/hall-ui/public/diagram-viewer.css +125 -0
- package/modules/hall-ui/public/diagram-viewer.js +330 -0
- package/modules/hall-ui/public/diagrams.html +113 -0
- package/modules/hall-ui/public/dom-utils.js +212 -0
- package/modules/hall-ui/public/drachmae.html +68 -0
- package/modules/hall-ui/public/drachmae.js +136 -0
- package/modules/hall-ui/public/gate.html +143 -0
- package/modules/hall-ui/public/gate.js +228 -0
- package/modules/hall-ui/public/goals-page.js +1297 -0
- package/modules/hall-ui/public/goals.html +376 -0
- package/modules/hall-ui/public/goals.js +119 -0
- package/modules/hall-ui/public/hall-widgets.js +189 -0
- package/modules/hall-ui/public/harbor.html +167 -0
- package/modules/hall-ui/public/harbor.js +335 -0
- package/modules/hall-ui/public/index.html +116 -0
- package/modules/hall-ui/public/instances-panel.js +172 -0
- package/modules/hall-ui/public/md-reader.js +234 -0
- package/modules/hall-ui/public/modules.html +119 -0
- package/modules/hall-ui/public/modules.js +154 -0
- package/modules/hall-ui/public/new-project-wizard.js +186 -0
- package/modules/hall-ui/public/not-ready.html +48 -0
- package/modules/hall-ui/public/pair.html +119 -0
- package/modules/hall-ui/public/pair.js +132 -0
- package/modules/hall-ui/public/primer.css +404 -0
- package/modules/hall-ui/public/primer.html +85 -0
- package/modules/hall-ui/public/primer.js +641 -0
- package/modules/hall-ui/public/profile.html +85 -0
- package/modules/hall-ui/public/profile.js +236 -0
- package/modules/hall-ui/public/ranks.html +69 -0
- package/modules/hall-ui/public/ranks.js +206 -0
- package/modules/hall-ui/public/roadmap.html +209 -0
- package/modules/hall-ui/public/roadmap.js +375 -0
- package/modules/hall-ui/public/sessions.html +92 -0
- package/modules/hall-ui/public/sessions.js +222 -0
- package/modules/hall-ui/public/settings.html +640 -0
- package/modules/hall-ui/public/settings.js +1219 -0
- package/modules/hall-ui/public/shell.js +495 -0
- package/modules/hall-ui/public/style.css +1748 -0
- package/modules/hall-ui/public/task-detail.js +253 -0
- package/modules/hall-ui/public/task.html +83 -0
- package/modules/hall-ui/public/task.js +326 -0
- package/modules/hall-ui/public/watch.html +333 -0
- package/modules/hall-ui/public/watch.js +859 -0
- package/modules/hall-ui/public/work.html +469 -0
- package/modules/hall-ui/public/work.js +1292 -0
- package/modules/ideas/CLAUDE.md +37 -0
- package/modules/ideas/blockers.js +100 -0
- package/modules/ideas/inbox.js +459 -0
- package/modules/ideas/module.json +10 -0
- package/modules/ideas/routes/blockers.js +120 -0
- package/modules/ideas/routes/inbox.js +230 -0
- package/modules/lifecycle/CLAUDE.md +63 -0
- package/modules/lifecycle/conflict-resolve.js +436 -0
- package/modules/lifecycle/db.js +4970 -0
- package/modules/lifecycle/done-when.js +355 -0
- package/modules/lifecycle/github-push.js +969 -0
- package/modules/lifecycle/goal-conflicts.js +133 -0
- package/modules/lifecycle/hierarchy.js +104 -0
- package/modules/lifecycle/lifecycle.js +58 -0
- package/modules/lifecycle/merge-lock.js +261 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +28 -0
- package/modules/lifecycle/migrations/lifecycle_002_backfill_orphan_task_goals.sql +36 -0
- package/modules/lifecycle/module.json +13 -0
- package/modules/lifecycle/pollers/publish-reconciler.js +20 -0
- package/modules/lifecycle/publish-reconciler.js +418 -0
- package/modules/lifecycle/routes/analytics.js +63 -0
- package/modules/lifecycle/routes/claims.js +506 -0
- package/modules/lifecycle/routes/dependencies.js +82 -0
- package/modules/lifecycle/routes/done-when.js +136 -0
- package/modules/lifecycle/routes/gate-approvals.js +197 -0
- package/modules/lifecycle/routes/goals.js +1419 -0
- package/modules/lifecycle/routes/lifecycle.js +43 -0
- package/modules/lifecycle/routes/tasks.js +1703 -0
- package/modules/lifecycle/routes/versions.js +128 -0
- package/modules/lifecycle/ship-card.js +505 -0
- package/modules/lifecycle/ship-preflight.js +225 -0
- package/modules/lifecycle/task-classifier.js +512 -0
- package/modules/lifecycle/version-close.js +106 -0
- package/modules/memory/CLAUDE.md +40 -0
- package/modules/memory/learnings.js +275 -0
- package/modules/memory/memory.js +572 -0
- package/modules/memory/module.json +12 -0
- package/modules/memory/routes/learnings.js +90 -0
- package/modules/memory/routes/memory.js +916 -0
- package/modules/memory/routes/search.js +188 -0
- package/modules/memory/search-chunker.js +252 -0
- package/modules/memory/search.js +320 -0
- package/modules/onboarding/CLAUDE.md +55 -0
- package/modules/onboarding/approval-broadcast.js +59 -0
- package/modules/onboarding/db.js +54 -0
- package/modules/onboarding/module.json +10 -0
- package/modules/onboarding/newcomer-restock.js +212 -0
- package/modules/onboarding/onboarding-state.js +532 -0
- package/modules/onboarding/onboarding.js +508 -0
- package/modules/onboarding/port.js +63 -0
- package/modules/onboarding/routes/access-requests.js +208 -0
- package/modules/onboarding/routes/onboarding.js +54 -0
- package/modules/security/CLAUDE.md +44 -0
- package/modules/security/module.json +10 -0
- package/modules/security/routes/reports.js +251 -0
- package/modules/security/secret-scrub.js +201 -0
- package/modules/security/security.js +294 -0
- package/modules/sessions/CLAUDE.md +39 -0
- package/modules/sessions/bfg.js +357 -0
- package/modules/sessions/db.js +326 -0
- package/modules/sessions/module.json +10 -0
- package/modules/sessions/pulse.js +149 -0
- package/modules/sessions/routes/sessions.js +331 -0
- package/modules/status-ui/CLAUDE.md +47 -0
- package/modules/status-ui/module.json +14 -0
- package/modules/status-ui/public/README.md +29 -0
- package/modules/status-ui/public/cursors/arrow.png +0 -0
- package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/status-ui/public/index.html +363 -0
- package/modules/status-ui/public/snapshots/.gitkeep +0 -0
- package/modules/status-ui/public/status.js +1375 -0
- package/modules/status-ui/public/style.css +1342 -0
- package/package-lock.json +975 -0
- package/package.json +28 -0
- package/public-docs/index.html +35 -0
- package/public-docs/redoc.standalone.js +1838 -0
- package/public-docs/redoc.standalone.js.LICENSE.txt +102 -0
- package/public-landing/buddha.png +0 -0
- package/public-landing/create.html +868 -0
- package/public-landing/index.html +168 -0
- package/scripts/bongos-mcp.js +28 -0
- package/scripts/cron/style_guide_audit.sh +134 -0
- package/scripts/discord/README.md +52 -0
- package/scripts/discord/lib.js +80 -0
- package/scripts/discord/post.js +51 -0
- package/scripts/gds/CLAUDE.md +144 -0
- package/scripts/gds/abandon.js +58 -0
- package/scripts/gds/adopt-detect.js +123 -0
- package/scripts/gds/adopt-import.js +111 -0
- package/scripts/gds/adopt-preflight.js +121 -0
- package/scripts/gds/api.js +78 -0
- package/scripts/gds/architect-audit.js +288 -0
- package/scripts/gds/art-stage.js +152 -0
- package/scripts/gds/audit-authorship.sh +243 -0
- package/scripts/gds/audit-deps.js +121 -0
- package/scripts/gds/audit-exports.js +128 -0
- package/scripts/gds/audit-replay.sh +161 -0
- package/scripts/gds/audit-ships.js +251 -0
- package/scripts/gds/autonomy-gate.js +135 -0
- package/scripts/gds/backfill-cost.js +165 -0
- package/scripts/gds/backfill-sessions.js +351 -0
- package/scripts/gds/backfill-task-rewards.js +408 -0
- package/scripts/gds/backup.js +94 -0
- package/scripts/gds/bfg-principal-token.js +65 -0
- package/scripts/gds/box-auth-check.js +160 -0
- package/scripts/gds/box-connect-lib.js +453 -0
- package/scripts/gds/box.js +1258 -0
- package/scripts/gds/build-bongos.js +94 -0
- package/scripts/gds/capture.js +123 -0
- package/scripts/gds/cf-tunnel.js +419 -0
- package/scripts/gds/ci-grade.js +82 -0
- package/scripts/gds/claim-recall.js +112 -0
- package/scripts/gds/claim.js +333 -0
- package/scripts/gds/classify-discipline.js +153 -0
- package/scripts/gds/claude-materialize.js +176 -0
- package/scripts/gds/cleanup-orphans.js +74 -0
- package/scripts/gds/cli-lib.js +518 -0
- package/scripts/gds/code.js +101 -0
- package/scripts/gds/codemod-rename-src-gds.js +145 -0
- package/scripts/gds/conductor-dispatch.js +426 -0
- package/scripts/gds/context-pack.js +228 -0
- package/scripts/gds/coreB-carve-proof.sh +716 -0
- package/scripts/gds/cost.js +47 -0
- package/scripts/gds/db-backup-nightly.sh +160 -0
- package/scripts/gds/db-backup-offsite.sh +152 -0
- package/scripts/gds/demote.js +40 -0
- package/scripts/gds/design-sync.js +62 -0
- package/scripts/gds/diagnose-achievements.js +118 -0
- package/scripts/gds/diagram-facts.js +319 -0
- package/scripts/gds/discipline-modes.json +25 -0
- package/scripts/gds/discord-channels.js +66 -0
- package/scripts/gds/do-api.js +176 -0
- package/scripts/gds/docs-entropy.js +403 -0
- package/scripts/gds/doctor.js +389 -0
- package/scripts/gds/feedback-latest.js +94 -0
- package/scripts/gds/fetch-art-key.js +106 -0
- package/scripts/gds/fetch-sound-prefs.js +88 -0
- package/scripts/gds/figma-design-sync.js +61 -0
- package/scripts/gds/fitness.js +1446 -0
- package/scripts/gds/gate-review.js +322 -0
- package/scripts/gds/gating.js +325 -0
- package/scripts/gds/gds-db-backup.service +24 -0
- package/scripts/gds/gds-db-backup.timer +20 -0
- package/scripts/gds/gds-db-offsite.service +29 -0
- package/scripts/gds/gds-db-offsite.timer +18 -0
- package/scripts/gds/gen-api-client.js +508 -0
- package/scripts/gds/gen-api-docs.js +1037 -0
- package/scripts/gds/gen-atlas.js +366 -0
- package/scripts/gds/gen-diagrams.js +608 -0
- package/scripts/gds/gen-file-map.js +196 -0
- package/scripts/gds/gen-repo-map.js +519 -0
- package/scripts/gds/gen-session-index.js +210 -0
- package/scripts/gds/git-merge-regen.js +147 -0
- package/scripts/gds/grade-smoke.js +205 -0
- package/scripts/gds/hooks/pre-push.js +169 -0
- package/scripts/gds/http-api-client.js +88 -0
- package/scripts/gds/init.js +903 -0
- package/scripts/gds/install-git-hooks.js +114 -0
- package/scripts/gds/key-card.js +213 -0
- package/scripts/gds/key-status.js +126 -0
- package/scripts/gds/land-watch.js +168 -0
- package/scripts/gds/learning-capture.js +145 -0
- package/scripts/gds/lib.sh +44 -0
- package/scripts/gds/linkify-refs.js +473 -0
- package/scripts/gds/local-preview-lib.js +159 -0
- package/scripts/gds/local-preview.js +95 -0
- package/scripts/gds/login.js +225 -0
- package/scripts/gds/main-audit.js +279 -0
- package/scripts/gds/me.js +88 -0
- package/scripts/gds/memory-map.js +174 -0
- package/scripts/gds/merge-lock.js +71 -0
- package/scripts/gds/migration-namespace.js +200 -0
- package/scripts/gds/mirror-lineage-check.js +319 -0
- package/scripts/gds/mirror-redact.js +305 -0
- package/scripts/gds/module.js +482 -0
- package/scripts/gds/newcomer-floor.config.json +13 -0
- package/scripts/gds/newcomer-floor.js +415 -0
- package/scripts/gds/newcomer-floor.templates.json +86 -0
- package/scripts/gds/newcomer-floor.test.js +235 -0
- package/scripts/gds/next-number.js +163 -0
- package/scripts/gds/oauth-secret.js +197 -0
- package/scripts/gds/onboard.js +217 -0
- package/scripts/gds/onboarding-analyze.js +226 -0
- package/scripts/gds/onboarding-config.js +178 -0
- package/scripts/gds/onboarding-ship.js +92 -0
- package/scripts/gds/package-core.js +684 -0
- package/scripts/gds/paste-token.js +131 -0
- package/scripts/gds/portability-smoke-art.sh +85 -0
- package/scripts/gds/portability-smoke-memory.sh +147 -0
- package/scripts/gds/provision.js +1324 -0
- package/scripts/gds/publish-manifest.js +307 -0
- package/scripts/gds/pulse.js +309 -0
- package/scripts/gds/push-main.js +45 -0
- package/scripts/gds/read-session-export.js +359 -0
- package/scripts/gds/reauth.js +149 -0
- package/scripts/gds/recall.js +135 -0
- package/scripts/gds/release.js +71 -0
- package/scripts/gds/render-ideas.js +173 -0
- package/scripts/gds/repo-metrics.js +363 -0
- package/scripts/gds/resolve-skill-model.js +57 -0
- package/scripts/gds/run-routine.js +197 -0
- package/scripts/gds/run-unit-tests.js +178 -0
- package/scripts/gds/sandbox-stage.js +653 -0
- package/scripts/gds/search-ingest-db.js +209 -0
- package/scripts/gds/search-ingest-md.js +225 -0
- package/scripts/gds/security-baseline-check.sh +180 -0
- package/scripts/gds/seed-adopt-tasks.js +75 -0
- package/scripts/gds/seed-api-contract-goal-tasks.js +141 -0
- package/scripts/gds/seed-bongos-app-tasks.js +294 -0
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +81 -0
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +100 -0
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +307 -0
- package/scripts/gds/seed-bongos-module-system-tasks.js +346 -0
- package/scripts/gds/seed-bongos-tasks.js +214 -0
- package/scripts/gds/seed-gds-v4-tasks.js +624 -0
- package/scripts/gds/seed-governance-tasks.js +340 -0
- package/scripts/gds/seed-medusa-tasks.js +440 -0
- package/scripts/gds/seed-module-upstreaming.js +219 -0
- package/scripts/gds/seed-productization-tasks.js +235 -0
- package/scripts/gds/seed-provisioning-tasks.js +83 -0
- package/scripts/gds/seed-staging.sh +104 -0
- package/scripts/gds/seed-v2-world-tasks.js +283 -0
- package/scripts/gds/seed-v3-tasks.js +1981 -0
- package/scripts/gds/session-cost.js +393 -0
- package/scripts/gds/session-digest.js +179 -0
- package/scripts/gds/session-eval.js +576 -0
- package/scripts/gds/session.js +404 -0
- package/scripts/gds/setup.js +504 -0
- package/scripts/gds/shell.js +100 -0
- package/scripts/gds/ship.js +3618 -0
- package/scripts/gds/smoke-batch-claim.sh +213 -0
- package/scripts/gds/smoke-batch-release.sh +296 -0
- package/scripts/gds/smoke-box.sh +150 -0
- package/scripts/gds/smoke-claim-consistency.sh +212 -0
- package/scripts/gds/smoke-dependencies.sh +84 -0
- package/scripts/gds/smoke-gds.js +125 -0
- package/scripts/gds/smoke-write.sh +168 -0
- package/scripts/gds/start.js +782 -0
- package/scripts/gds/status.js +365 -0
- package/scripts/gds/triage.js +366 -0
- package/scripts/gds/upgrade.js +421 -0
- package/scripts/gds/validate-design.js +254 -0
- package/scripts/gds/version-close.js +549 -0
- package/scripts/migrate.sh +183 -0
- package/scripts/migrate_cost_ledger.sh +178 -0
- package/scripts/public-mirror-export.js +291 -0
- package/scripts/render-diagrams.sh +88 -0
- package/scripts/security/dep-audit.js +276 -0
- package/scripts/setup-builder.js +274 -0
- package/scripts/setup-builder.sh +28 -0
- package/scripts/snapshots/backfill-history.mjs +292 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +66 -0
- package/scripts/snapshots/render-pre-art-eras.mjs +329 -0
- package/scripts/snapshots/render-world.mjs +322 -0
- package/scripts/status-mirror-sync.js +192 -0
- package/scripts/status-probe.mjs +185 -0
- package/server.js +212 -0
- package/src/asset-version.js +71 -0
- package/src/bongos/CLAUDE.md +64 -0
- package/src/bongos/api-errors.js +281 -0
- package/src/bongos/api-path-404.js +67 -0
- package/src/bongos/api-prefix.js +38 -0
- package/src/bongos/app-pair.js +172 -0
- package/src/bongos/auth.js +979 -0
- package/src/bongos/cascade-dispatch.js +123 -0
- package/src/bongos/db-kernel.js +252 -0
- package/src/bongos/db.js +911 -0
- package/src/bongos/goal-scope-check.js +120 -0
- package/src/bongos/hierarchy-config.js +189 -0
- package/src/bongos/live-channel.js +145 -0
- package/src/bongos/llm-cache.js +260 -0
- package/src/bongos/llm-pricing.js +220 -0
- package/src/bongos/metrics.js +286 -0
- package/src/bongos/middleware/audit.js +274 -0
- package/src/bongos/middleware/error-envelope.js +93 -0
- package/src/bongos/middleware/rate-limit.js +195 -0
- package/src/bongos/module-scope-map.js +271 -0
- package/src/bongos/optimizer.js +166 -0
- package/src/bongos/path-match.js +47 -0
- package/src/bongos/permission-path-check.js +248 -0
- package/src/bongos/pool.js +55 -0
- package/src/bongos/repo-info.js +91 -0
- package/src/bongos/route-rank-check.js +420 -0
- package/src/bongos/routes/CLAUDE.md +43 -0
- package/src/bongos/routes/_helpers.js +369 -0
- package/src/bongos/routes/audit-log.js +59 -0
- package/src/bongos/routes/auth.js +755 -0
- package/src/bongos/routes/backup.js +104 -0
- package/src/bongos/routes/builders.js +526 -0
- package/src/bongos/routes/healthz.js +16 -0
- package/src/bongos/routes/instance.js +129 -0
- package/src/bongos/routes/live.js +125 -0
- package/src/bongos/routes/llm-cache.js +119 -0
- package/src/bongos/routes/me.js +380 -0
- package/src/bongos/routes/modules.js +131 -0
- package/src/bongos/routes/optimizer.js +54 -0
- package/src/bongos/routes/public.js +476 -0
- package/src/bongos/routes/security.js +207 -0
- package/src/bongos/routes.js +166 -0
- package/src/bongos/secret-box.js +177 -0
- package/src/bongos/sensitive-surfaces.js +30 -0
- package/src/bongos/serve-internal.js +889 -0
- package/src/bongos/uptime-poller.js +90 -0
- package/src/bongos-downloads.js +70 -0
- package/src/branding.js +182 -0
- package/src/build-info.js +77 -0
- package/src/devbox-downloads.js +67 -0
- package/src/instance-config.js +200 -0
- package/src/module-api.js +366 -0
- package/src/module-loader/loader.js +268 -0
- package/src/module-loader/manifest-schema.js +140 -0
- package/src/module-loader/provenance.js +28 -0
- package/src/module-loader/semver.js +100 -0
- package/src/module-seams.js +291 -0
- package/src/modules.js +254 -0
- package/src/platform-server.js +135 -0
- package/src/ui/adapters/claude-design/index.js +333 -0
- package/src/ui/adapters/figma/index.js +328 -0
- package/tests/abandon_task.mjs +138 -0
- package/tests/achievements_reconcile.mjs +167 -0
- package/tests/adopt_detect.mjs +141 -0
- package/tests/adopt_import.mjs +132 -0
- package/tests/adopt_preflight.mjs +105 -0
- package/tests/anti_grid.mjs +89 -0
- package/tests/api_client.mjs +231 -0
- package/tests/api_docs.mjs +283 -0
- package/tests/api_error_envelope_buildcheck.mjs +56 -0
- package/tests/api_error_envelope_middleware.mjs +99 -0
- package/tests/api_error_registry.mjs +108 -0
- package/tests/api_pagination_contract.mjs +51 -0
- package/tests/api_path_404.mjs +96 -0
- package/tests/app_pair.mjs +168 -0
- package/tests/architect.mjs +140 -0
- package/tests/art_key.mjs +121 -0
- package/tests/art_stage.mjs +121 -0
- package/tests/asset_version.mjs +94 -0
- package/tests/atlas.mjs +69 -0
- package/tests/audit_deps.mjs +74 -0
- package/tests/audit_middleware.mjs +385 -0
- package/tests/auth_admission_gate.mjs +164 -0
- package/tests/auth_anthropic_identity.mjs +95 -0
- package/tests/auth_duplicate_cookie.mjs +56 -0
- package/tests/auth_error_brand.mjs +77 -0
- package/tests/auth_flow_slice1.mjs +223 -0
- package/tests/auth_inactive_builder.mjs +94 -0
- package/tests/auth_page_gate.mjs +92 -0
- package/tests/auth_session_ttl.mjs +155 -0
- package/tests/autonomous_fit_protected_paths.mjs +115 -0
- package/tests/autonomy_gate.mjs +85 -0
- package/tests/autotile_variants.mjs +256 -0
- package/tests/autotile_xverify.mjs +133 -0
- package/tests/backfill_sessions.mjs +101 -0
- package/tests/backfill_task_rewards.mjs +152 -0
- package/tests/backup_cli.mjs +104 -0
- package/tests/bfg.mjs +327 -0
- package/tests/bfg_write.mjs +423 -0
- package/tests/bongos_box_connect.mjs +89 -0
- package/tests/bongos_dispatch.mjs +74 -0
- package/tests/bongos_downloads.mjs +118 -0
- package/tests/bongos_login.mjs +34 -0
- package/tests/box_access.mjs +186 -0
- package/tests/box_auth_check.mjs +114 -0
- package/tests/box_code_staleness.mjs +116 -0
- package/tests/box_connect_e2e.mjs +196 -0
- package/tests/box_cost.mjs +70 -0
- package/tests/box_dns_repoint.mjs +151 -0
- package/tests/box_host_keys.mjs +102 -0
- package/tests/box_onboard.mjs +317 -0
- package/tests/box_scope_session.mjs +115 -0
- package/tests/box_ship_permissions.mjs +71 -0
- package/tests/box_terminal_ssrf.mjs +90 -0
- package/tests/boxes.mjs +1255 -0
- package/tests/branding.mjs +88 -0
- package/tests/bug_attachments.mjs +189 -0
- package/tests/build_info.mjs +88 -0
- package/tests/builder_needs.mjs +99 -0
- package/tests/cap_newcomer_tasks.mjs +114 -0
- package/tests/cascade-dispatch.mjs +95 -0
- package/tests/cf_tunnel.mjs +459 -0
- package/tests/claim_recall.mjs +150 -0
- package/tests/claims_bind_session.mjs +47 -0
- package/tests/claude_materialize.mjs +148 -0
- package/tests/cli_box_scope.mjs +52 -0
- package/tests/cli_lib_formatters.mjs +87 -0
- package/tests/cli_token_reissue.mjs +147 -0
- package/tests/client_bundle_route.mjs +47 -0
- package/tests/cloud_bongos_pack.mjs +85 -0
- package/tests/conductor.mjs +496 -0
- package/tests/confirm_attribution.mjs +156 -0
- package/tests/conflict_resolve.mjs +462 -0
- package/tests/context_pack.mjs +240 -0
- package/tests/cost.mjs +94 -0
- package/tests/cost_audit.mjs +122 -0
- package/tests/cost_budget_status.mjs +78 -0
- package/tests/cost_reconcile.mjs +102 -0
- package/tests/cost_summary_shape.mjs +94 -0
- package/tests/credit_grant.mjs +376 -0
- package/tests/criterion_progress.mjs +220 -0
- package/tests/criterion_proposal.mjs +109 -0
- package/tests/criterion_ref_parse.mjs +53 -0
- package/tests/currency_label.mjs +119 -0
- package/tests/day_cycle.mjs +197 -0
- package/tests/dependencies_migration.mjs +77 -0
- package/tests/dependencies_polymorphic.mjs +212 -0
- package/tests/deploy_instance.mjs +78 -0
- package/tests/devbox_downloads.mjs +122 -0
- package/tests/diff_derived_signals.mjs +115 -0
- package/tests/discord_approvals.mjs +174 -0
- package/tests/discord_autojoin.mjs +158 -0
- package/tests/discord_bot.mjs +94 -0
- package/tests/discord_bugs.mjs +297 -0
- package/tests/discord_channels.mjs +337 -0
- package/tests/discord_foundation.mjs +76 -0
- package/tests/discord_inbound.mjs +190 -0
- package/tests/discord_message_buffer.mjs +87 -0
- package/tests/discord_oauth.mjs +55 -0
- package/tests/discord_reaction.mjs +170 -0
- package/tests/discord_role_sync.mjs +151 -0
- package/tests/display_name.mjs +123 -0
- package/tests/dns_preflight.mjs +74 -0
- package/tests/event_sound_prefs.mjs +194 -0
- package/tests/eviction_policy.mjs +279 -0
- package/tests/figma_adapter.mjs +199 -0
- package/tests/file_map.mjs +91 -0
- package/tests/first_admin_bootstrap.mjs +40 -0
- package/tests/fitness.mjs +435 -0
- package/tests/fitness_two_root.mjs +104 -0
- package/tests/fixtures/session-506-digest.json +34 -0
- package/tests/gate_approvals.mjs +451 -0
- package/tests/gate_review.mjs +152 -0
- package/tests/git_merge_regen.mjs +151 -0
- package/tests/github_push_ancestry.mjs +119 -0
- package/tests/github_push_land_proof.mjs +127 -0
- package/tests/github_push_merge_driver.mjs +175 -0
- package/tests/goal_abuse_matrix.mjs +187 -0
- package/tests/goal_achievement.mjs +146 -0
- package/tests/goal_conflicts.mjs +305 -0
- package/tests/goal_dependencies_read.mjs +73 -0
- package/tests/goal_member_roles.mjs +420 -0
- package/tests/goal_membership_requests.mjs +332 -0
- package/tests/goal_routes.mjs +203 -0
- package/tests/goal_scope_check.mjs +167 -0
- package/tests/goal_tier.mjs +312 -0
- package/tests/goal_wall_proof.mjs +112 -0
- package/tests/grade_attempts.mjs +73 -0
- package/tests/grade_attribution.mjs +117 -0
- package/tests/grade_diff_hash.mjs +191 -0
- package/tests/grade_server_authoritative.mjs +305 -0
- package/tests/grade_unavailable_display.mjs +66 -0
- package/tests/grader.mjs +1960 -0
- package/tests/grader_bypass.mjs +57 -0
- package/tests/grader_panel.mjs +252 -0
- package/tests/grader_prompt_fencing.mjs +192 -0
- package/tests/helpers.mjs +52 -0
- package/tests/hierarchy-config.mjs +81 -0
- package/tests/init.mjs +463 -0
- package/tests/instance_config.mjs +76 -0
- package/tests/instance_manifest.mjs +110 -0
- package/tests/interaction_prefs.mjs +72 -0
- package/tests/key-card.mjs +110 -0
- package/tests/key-status.mjs +45 -0
- package/tests/land_watch.mjs +115 -0
- package/tests/lifecycle-card.mjs +145 -0
- package/tests/lifecycle_breakdown_view.mjs +236 -0
- package/tests/linkify_refs.mjs +264 -0
- package/tests/live_channel.mjs +128 -0
- package/tests/live_sse.mjs +122 -0
- package/tests/llm-cache-route.mjs +110 -0
- package/tests/llm-cache.mjs +140 -0
- package/tests/llm-pricing.mjs +152 -0
- package/tests/local_preview.mjs +136 -0
- package/tests/main_audit.mjs +137 -0
- package/tests/memory_isolation.mjs +431 -0
- package/tests/memory_map.mjs +107 -0
- package/tests/memory_pull.mjs +429 -0
- package/tests/merge_lease.mjs +100 -0
- package/tests/merge_lock.mjs +312 -0
- package/tests/metrics.mjs +147 -0
- package/tests/migration_allocation.mjs +146 -0
- package/tests/migration_namespace.mjs +185 -0
- package/tests/migrations_fresh_replay_guard.mjs +38 -0
- package/tests/mirror_lineage_fulltree.mjs +45 -0
- package/tests/mirror_lineage_proof.mjs +72 -0
- package/tests/mirror_redact.mjs +193 -0
- package/tests/model_alloc_prefs.mjs +63 -0
- package/tests/module-scope-map.mjs +88 -0
- package/tests/module_api.mjs +110 -0
- package/tests/module_cli.mjs +292 -0
- package/tests/module_contributions.mjs +148 -0
- package/tests/module_loader.mjs +286 -0
- package/tests/module_manifest.mjs +194 -0
- package/tests/module_owned_tests.mjs +105 -0
- package/tests/module_provenance.mjs +34 -0
- package/tests/module_route_rank.mjs +147 -0
- package/tests/module_seams.mjs +68 -0
- package/tests/modules.mjs +156 -0
- package/tests/modules_route.mjs +38 -0
- package/tests/newcomer_floor.mjs +17 -0
- package/tests/newcomer_restock.mjs +80 -0
- package/tests/next_number.mjs +154 -0
- package/tests/oauth_secret.mjs +60 -0
- package/tests/onboard.mjs +70 -0
- package/tests/onboarding_gate.mjs +267 -0
- package/tests/onboarding_observability.mjs +344 -0
- package/tests/onboarding_route_signals.mjs +175 -0
- package/tests/onboarding_state.mjs +346 -0
- package/tests/one_active_claim_per_session.mjs +111 -0
- package/tests/orphaned_claim_ship.mjs +135 -0
- package/tests/override_attribution.mjs +142 -0
- package/tests/override_request_ownership.mjs +235 -0
- package/tests/package_core.mjs +435 -0
- package/tests/parallel-load.mjs +402 -0
- package/tests/path_match.mjs +62 -0
- package/tests/permission_path.mjs +151 -0
- package/tests/platform_boot.mjs +167 -0
- package/tests/pre_compact.mjs +60 -0
- package/tests/preview_server.mjs +127 -0
- package/tests/profile_route.mjs +197 -0
- package/tests/provision.mjs +805 -0
- package/tests/provisioning_github_app_routes.mjs +98 -0
- package/tests/public_mirror_export.mjs +124 -0
- package/tests/public_task_resolver.mjs +112 -0
- package/tests/publish_branch_route.mjs +422 -0
- package/tests/publish_manifest.mjs +184 -0
- package/tests/publish_reconciler.mjs +728 -0
- package/tests/rank_gate.mjs +463 -0
- package/tests/rate_limit_public.mjs +217 -0
- package/tests/read_session_export.mjs +89 -0
- package/tests/recall_cli.mjs +82 -0
- package/tests/recall_hints.mjs +119 -0
- package/tests/recent_shipped_window.mjs +80 -0
- package/tests/regrade_eligibility.mjs +202 -0
- package/tests/render_prefs.mjs +122 -0
- package/tests/repo_map.mjs +92 -0
- package/tests/reward_gate.mjs +284 -0
- package/tests/route_rank_check.mjs +243 -0
- package/tests/route_validation.mjs +490 -0
- package/tests/routine_timers.mjs +72 -0
- package/tests/rubric_fixtures.mjs +140 -0
- package/tests/run_routine.mjs +69 -0
- package/tests/sandbox_stage.mjs +582 -0
- package/tests/scrubber_corpus.mjs +257 -0
- package/tests/search_chunker.mjs +209 -0
- package/tests/search_ingest.mjs +96 -0
- package/tests/search_ingest_db.mjs +88 -0
- package/tests/search_isolation.mjs +204 -0
- package/tests/search_route.mjs +95 -0
- package/tests/secret_box.mjs +189 -0
- package/tests/security_docs.mjs +186 -0
- package/tests/security_route.mjs +108 -0
- package/tests/served_paths_shipped.mjs +118 -0
- package/tests/session-cards.mjs +142 -0
- package/tests/session_506_regression.mjs +69 -0
- package/tests/session_caps.mjs +238 -0
- package/tests/session_cost.mjs +133 -0
- package/tests/session_digest.mjs +73 -0
- package/tests/session_eval.mjs +386 -0
- package/tests/session_index.mjs +105 -0
- package/tests/session_records.mjs +369 -0
- package/tests/session_search.mjs +78 -0
- package/tests/session_start_freshness.mjs +123 -0
- package/tests/setup_smoke.mjs +154 -0
- package/tests/ship_autolinkify.mjs +60 -0
- package/tests/ship_automerge_injection.mjs +174 -0
- package/tests/ship_broadcast_routing.mjs +125 -0
- package/tests/ship_broadcast_server.mjs +175 -0
- package/tests/ship_card.mjs +280 -0
- package/tests/ship_ci_deploy.mjs +374 -0
- package/tests/ship_preflight.mjs +368 -0
- package/tests/ship_premerge.mjs +75 -0
- package/tests/ship_resume.mjs +129 -0
- package/tests/ship_upload_skip.mjs +89 -0
- package/tests/ship_zero_commit.mjs +60 -0
- package/tests/single_host_hall.mjs +98 -0
- package/tests/skill_prefs.mjs +150 -0
- package/tests/smoke_preflight.mjs +66 -0
- package/tests/snapshot_picker.mjs +233 -0
- package/tests/sound_prefs.mjs +198 -0
- package/tests/sprite_facing.mjs +35 -0
- package/tests/status-probe.mjs +89 -0
- package/tests/status_backfill.mjs +94 -0
- package/tests/stop_refcheck.mjs +127 -0
- package/tests/streak.mjs +175 -0
- package/tests/subdomain_auth.mjs +58 -0
- package/tests/task_classifier_vocab.mjs +41 -0
- package/tests/task_create_idempotency.mjs +96 -0
- package/tests/task_goal_link.mjs +93 -0
- package/tests/task_ownership_gate.mjs +125 -0
- package/tests/terrain_picker.mjs +298 -0
- package/tests/touches_overlap.mjs +75 -0
- package/tests/triage_development.mjs +140 -0
- package/tests/turn_in_place.mjs +280 -0
- package/tests/upgrade.mjs +357 -0
- package/tests/verify_task_guard.mjs +64 -0
- package/tests/version_close.mjs +76 -0
- package/tests/versions_route.mjs +93 -0
- package/tests/walkability.mjs +188 -0
- package/tests/wandering_prefs.mjs +176 -0
- package/tests/worktree_disambiguation.mjs +105 -0
- package/tests/worktree_guard.mjs +90 -0
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
// src/bongos/routes/goals.js — the Goal tier's HTTP surface (ADR 0086 §3, BV1.R57).
|
|
2
|
+
//
|
|
3
|
+
// A goal is a shared, module-scoped workspace (the `goals` + `goal_members`
|
|
4
|
+
// tables from R56). This router lets a Metic+ create one, lets builders view and
|
|
5
|
+
// join/leave them, and adds a `?goal_id=` filter to GET /tasks (in tasks.js).
|
|
6
|
+
//
|
|
7
|
+
// SCOPE WALL (ADR 0086 §3/§4). `scope_modules` is the wall, set at create time by
|
|
8
|
+
// a trusted rank and never self-granted:
|
|
9
|
+
// - POST /goals is metic+archon, but ARCHON-ONLY when scope_modules names a
|
|
10
|
+
// `protected` module (module-scope-map.js, R54) — the protected core.
|
|
11
|
+
// - Joining a protected-scope goal is Archon-mediated: a non-Archon cannot
|
|
12
|
+
// self-join; an Archon admits them via POST /goals/:id/members, and only a
|
|
13
|
+
// Metic+ may be admitted. Per ADR 0086's Deferred section, a sub-Metic builder
|
|
14
|
+
// working INSIDE a protected-scope goal is held for the Phase-5 security-
|
|
15
|
+
// reviewed ADR — until then protected-scope goals require a Metic+ holder, and
|
|
16
|
+
// the self-service "request → approve" queue is not built here (it co-evolves
|
|
17
|
+
// with that ADR). Normal (non-protected) goals are open-join.
|
|
18
|
+
//
|
|
19
|
+
// The bounded task-authoring gate (POST /goals/:id/tasks) is R60, not here; this
|
|
20
|
+
// router references no task-authoring grant. The grader/route-rank fitness check
|
|
21
|
+
// pins these routes' ranks in route-rank-check.EXPECTED_RANKS.
|
|
22
|
+
|
|
23
|
+
const express = require('express');
|
|
24
|
+
// Intra-module + the published doorway — a module never requires a core internal.
|
|
25
|
+
const api = require('../../../src/module-api');
|
|
26
|
+
const auth = api;
|
|
27
|
+
const db = require('../db');
|
|
28
|
+
const hierarchy = require('../hierarchy');
|
|
29
|
+
const { classifyTaskKind, VALID_KINDS, VALID_DISCIPLINES, TASK_FIELD_BOUNDS } = require('../task-classifier');
|
|
30
|
+
const { validateOrRespond, parseId, LIMITS } = api;
|
|
31
|
+
const { isModuleKey, scopeIncludesProtected, moduleKeys } = api.moduleScopeMap;
|
|
32
|
+
// goalScopeCheck — the PURE scope wall (BV1.R59 / ADR 0086 §3), reached through
|
|
33
|
+
// the published doorway (a module never requires the core file directly — the
|
|
34
|
+
// module-boundary fitness check would red the build). The create gate below calls
|
|
35
|
+
// its check() VERBATIM; the two walls (scope-subset + protected-path) live there,
|
|
36
|
+
// never re-implemented here.
|
|
37
|
+
const goalScopeCheck = api.goalScopeCheck;
|
|
38
|
+
// The pure conflict detector (task 1736) — a module sibling, like ../db.
|
|
39
|
+
const goalConflicts = require('../goal-conflicts');
|
|
40
|
+
|
|
41
|
+
const VALID_GOAL_STATUSES = ['open', 'achieved', 'archived'];
|
|
42
|
+
const VALID_MEMBER_ROLES = ['lead', 'member'];
|
|
43
|
+
const METIC_PLUS = ['metic', 'archon'];
|
|
44
|
+
const RESOLVE_ACTIONS = ['sequence', 'flag_release', 'rescope'];
|
|
45
|
+
// Private goals (task 1952 / ADR 0112): the visibility values, and the resolve
|
|
46
|
+
// actions for each side of the membership-request queue.
|
|
47
|
+
const VALID_VISIBILITIES = ['public', 'private'];
|
|
48
|
+
const INVITE_RESPONSE_ACTIONS = ['accept', 'decline']; // the invitee resolves their own invite
|
|
49
|
+
const JOIN_REQUEST_RESPONSE_ACTIONS = ['approve', 'deny']; // an owner/manager/Archon resolves a request
|
|
50
|
+
|
|
51
|
+
// Shared gate for the goal lifecycle + scope mutations (BV1.R64): the caller must
|
|
52
|
+
// be a member of the goal, or an Archon (who may act on any goal). Returns true to
|
|
53
|
+
// proceed; a false result means "not a member" → the route should 403. This is the
|
|
54
|
+
// membership half of the wall; the protected-module rank check is enforced per-route
|
|
55
|
+
// (scope is never self-granted).
|
|
56
|
+
async function isMemberOrArchon(builder, goalId) {
|
|
57
|
+
if (builder.rank === 'archon') return true;
|
|
58
|
+
return db.isGoalMember({ goalId, builderId: builder.id });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// authorizeGoalTaskCreate — the ORDERED, FAIL-CLOSED authorization decision for
|
|
62
|
+
// POST /goals/:id/tasks (BV1.R60 / ADR 0086 §3): the single point that lets a
|
|
63
|
+
// lower-rank builder author their OWN work safely. PURE + exported (the
|
|
64
|
+
// shouldRefuseSessionClaim precedent in claims.js) so every rejection is pinned
|
|
65
|
+
// by a DB-free unit test AND reused by the BV1.R61 abuse matrix — no live route
|
|
66
|
+
// or DB needed.
|
|
67
|
+
//
|
|
68
|
+
// The route fetches goal / version / membership, then calls this; it rejects on
|
|
69
|
+
// the FIRST failure (fail-closed — a missing/ambiguous input denies):
|
|
70
|
+
// goal_not_found → goal_not_open → not_a_member → version_not_found →
|
|
71
|
+
// version_closed → (scope-subset + protected-path, via goalScopeCheck.check).
|
|
72
|
+
// The scope+protected wall is goal-scope-check.check() VERBATIM (R59) — itself
|
|
73
|
+
// ordered + fail-closed (empty-scope → empty-touches → unknown-scope →
|
|
74
|
+
// scope-subset → protected-path) — never re-implemented here. `isMember` is the
|
|
75
|
+
// resolved "member OR Archon" boolean the route computes (isMemberOrArchon).
|
|
76
|
+
//
|
|
77
|
+
// This is the CREATE-time filter: it sees only DECLARED touches[] (advisory — git
|
|
78
|
+
// merge is the real collision detector, ADR 0049), so it is deliberately the FIRST
|
|
79
|
+
// filter, NOT the last. The ship-time gates stay authoritative on the REAL diff
|
|
80
|
+
// (grader permission/route-rank pre-passes + serverSideGuards, gate-review
|
|
81
|
+
// ESCALATE floor, post-push main-audit — ADR 0086 §7). Returns { ok:true } or
|
|
82
|
+
// { ok:false, status, body } (the exact HTTP status + JSON the route sends).
|
|
83
|
+
function authorizeGoalTaskCreate({ goal, version, isMember, builderRank, touches }) {
|
|
84
|
+
if (!goal) {
|
|
85
|
+
return { ok: false, status: 404, body: { error: 'goal_not_found' } };
|
|
86
|
+
}
|
|
87
|
+
if (goal.status !== 'open') {
|
|
88
|
+
return { ok: false, status: 409, body: {
|
|
89
|
+
error: 'goal_not_open', status: goal.status,
|
|
90
|
+
message: 'Tasks can only be authored under an open goal.',
|
|
91
|
+
} };
|
|
92
|
+
}
|
|
93
|
+
if (!isMember) {
|
|
94
|
+
return { ok: false, status: 403, body: {
|
|
95
|
+
error: 'not_a_member',
|
|
96
|
+
message: 'Join the goal (or be an Archon) to author tasks within it.',
|
|
97
|
+
} };
|
|
98
|
+
}
|
|
99
|
+
if (!version) {
|
|
100
|
+
return { ok: false, status: 404, body: {
|
|
101
|
+
error: 'version_not_found',
|
|
102
|
+
message: `The goal's version '${goal.version_id}' no longer exists.`,
|
|
103
|
+
} };
|
|
104
|
+
}
|
|
105
|
+
if (version.status === 'shipped') {
|
|
106
|
+
return { ok: false, status: 409, body: {
|
|
107
|
+
error: 'version_closed',
|
|
108
|
+
message: `Version '${goal.version_id}' has shipped — it is closed history and cannot take new tasks.`,
|
|
109
|
+
} };
|
|
110
|
+
}
|
|
111
|
+
// The wall — scope-subset + protected-path in one pure call (R59). It bounds
|
|
112
|
+
// EVERYONE by the goal's modules (scope-subset), and additionally blocks a
|
|
113
|
+
// sub-Metic member from authoring into the protected core (protected-path),
|
|
114
|
+
// reusing the SINGLE protected roster (permission-path-check) — no drift.
|
|
115
|
+
const gate = goalScopeCheck.check(goal.scope_modules, touches, builderRank);
|
|
116
|
+
if (!gate.ok) {
|
|
117
|
+
return { ok: false, status: 403, body: {
|
|
118
|
+
error: gate.code, reason: gate.reason, details: gate.details,
|
|
119
|
+
} };
|
|
120
|
+
}
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The goal AUTHORITY MODEL (task 1735). Two layers, deliberately different kinds:
|
|
125
|
+
// - OWNER — goals.created_by, the fixed authority. Not a role row: it is set at
|
|
126
|
+
// create time (the creator) and moves only via the explicit transfer route.
|
|
127
|
+
// The owner holds every goal power.
|
|
128
|
+
// - MANAGER — the goal_members.role 'lead' value (the hall renders it
|
|
129
|
+
// "manager"). Owner-equivalent EXCEPT: a manager cannot DEMOTE a fellow
|
|
130
|
+
// manager (owner-only), and cannot transfer ownership.
|
|
131
|
+
// A goal with created_by NULL (the migration-160 system defaults) has no owner;
|
|
132
|
+
// Archons act as one — the archon arm of both gates below.
|
|
133
|
+
function isGoalOwner(goal, builderId) {
|
|
134
|
+
return goal.created_by != null && String(goal.created_by) === String(builderId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// authorizeMemberRoleChange — the ORDERED, FAIL-CLOSED authorization decision for
|
|
138
|
+
// PATCH /goals/:id/members/:builderId (task 1735). Today the only role-assignment
|
|
139
|
+
// path is the Archon admission route; this gate lets the goal's own authority
|
|
140
|
+
// manage roles without an Archon in the loop:
|
|
141
|
+
// - the owner, a manager, or an Archon may promote member→lead(manager);
|
|
142
|
+
// - ONLY the owner (or an Archon) may demote lead(manager)→member;
|
|
143
|
+
// - the OWNER's row can never be demoted — owner authority is created_by, not
|
|
144
|
+
// the role value, so a demoted owner row would just lie to the member list.
|
|
145
|
+
// PURE + exported (the authorizeGoalTaskCreate precedent above) so every
|
|
146
|
+
// rejection is pinned by a DB-free unit test. The route fetches goal /
|
|
147
|
+
// memberships, then calls this; it rejects on the FIRST failure (fail-closed —
|
|
148
|
+
// a missing/ambiguous input denies):
|
|
149
|
+
// goal_not_found → goal_not_open → not_owner_or_manager → not_a_member
|
|
150
|
+
// (target) → no-op same-role → owner_only_demotion → owner_role_fixed.
|
|
151
|
+
// Authority (not_owner_or_manager) is checked BEFORE the target lookup so an
|
|
152
|
+
// unauthorized actor never learns who is or isn't a member (the O1 no-leak
|
|
153
|
+
// ordering precedent in the abuse matrix). The DB helper (setGoalMemberRole)
|
|
154
|
+
// re-asserts the owner-demotion guard atomically in the UPDATE itself — this
|
|
155
|
+
// gate is the testable contract, the SQL guard is the backstop.
|
|
156
|
+
// Returns { ok:true }, { ok:true, noop:true } (role already as requested), or
|
|
157
|
+
// { ok:false, status, body } (the exact HTTP status + JSON the route sends).
|
|
158
|
+
function authorizeMemberRoleChange({ goal, actorId, actorRank, actorMembership, targetMembership, targetId, role }) {
|
|
159
|
+
if (!goal) {
|
|
160
|
+
return { ok: false, status: 404, body: { error: 'goal_not_found' } };
|
|
161
|
+
}
|
|
162
|
+
if (goal.status !== 'open') {
|
|
163
|
+
return { ok: false, status: 409, body: {
|
|
164
|
+
error: 'goal_not_open', status: goal.status,
|
|
165
|
+
message: 'Member roles can only be changed on an open goal.',
|
|
166
|
+
} };
|
|
167
|
+
}
|
|
168
|
+
const actorIsOwner = isGoalOwner(goal, actorId);
|
|
169
|
+
const actorIsManager = !!actorMembership && actorMembership.role === 'lead';
|
|
170
|
+
if (actorRank !== 'archon' && !actorIsOwner && !actorIsManager) {
|
|
171
|
+
return { ok: false, status: 403, body: {
|
|
172
|
+
error: 'not_owner_or_manager',
|
|
173
|
+
message: 'Only this goal\'s owner or a manager (or an Archon) may change member roles.',
|
|
174
|
+
} };
|
|
175
|
+
}
|
|
176
|
+
if (!targetMembership) {
|
|
177
|
+
return { ok: false, status: 404, body: {
|
|
178
|
+
error: 'not_a_member',
|
|
179
|
+
message: 'That builder is not a member of this goal — admit them first.',
|
|
180
|
+
} };
|
|
181
|
+
}
|
|
182
|
+
if (targetMembership.role === role) {
|
|
183
|
+
return { ok: true, noop: true };
|
|
184
|
+
}
|
|
185
|
+
if (role === 'member') {
|
|
186
|
+
// A demotion. Managers promote; only the owner (or an Archon) demotes.
|
|
187
|
+
if (actorRank !== 'archon' && !actorIsOwner) {
|
|
188
|
+
return { ok: false, status: 403, body: {
|
|
189
|
+
error: 'owner_only_demotion',
|
|
190
|
+
message: 'Only the goal owner (or an Archon) may demote a manager.',
|
|
191
|
+
} };
|
|
192
|
+
}
|
|
193
|
+
if (isGoalOwner(goal, targetId)) {
|
|
194
|
+
return { ok: false, status: 409, body: {
|
|
195
|
+
error: 'owner_role_fixed',
|
|
196
|
+
message: 'The goal owner cannot be demoted — transfer ownership first.',
|
|
197
|
+
} };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { ok: true };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// authorizeGoalManagement — the shared ORDERED, FAIL-CLOSED prefix for actions
|
|
204
|
+
// a goal's own authority performs (task 1736: conflict resolution; the same
|
|
205
|
+
// three steps open authorizeMemberRoleChange above): goal exists → goal open →
|
|
206
|
+
// actor is owner|manager|Archon. Returns { ok:true } or { ok:false, status,
|
|
207
|
+
// body }. PURE + exported, same precedent as its siblings.
|
|
208
|
+
function authorizeGoalManagement({ goal, actorId, actorRank, actorMembership }) {
|
|
209
|
+
if (!goal) {
|
|
210
|
+
return { ok: false, status: 404, body: { error: 'goal_not_found' } };
|
|
211
|
+
}
|
|
212
|
+
if (goal.status !== 'open') {
|
|
213
|
+
return { ok: false, status: 409, body: {
|
|
214
|
+
error: 'goal_not_open', status: goal.status,
|
|
215
|
+
message: 'Only an open goal can be managed.',
|
|
216
|
+
} };
|
|
217
|
+
}
|
|
218
|
+
const actorIsOwner = isGoalOwner(goal, actorId);
|
|
219
|
+
const actorIsManager = !!actorMembership && actorMembership.role === 'lead';
|
|
220
|
+
if (actorRank !== 'archon' && !actorIsOwner && !actorIsManager) {
|
|
221
|
+
return { ok: false, status: 403, body: {
|
|
222
|
+
error: 'not_owner_or_manager',
|
|
223
|
+
message: 'Only this goal\'s owner or a manager (or an Archon) may do this.',
|
|
224
|
+
} };
|
|
225
|
+
}
|
|
226
|
+
return { ok: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// authorizeOwnershipTransfer — the ORDERED, FAIL-CLOSED decision for
|
|
230
|
+
// POST /goals/:id/transfer (task 1735): the owner hands the goal to another
|
|
231
|
+
// builder (the old owner keeps their manager row). Ownership is creator-
|
|
232
|
+
// equivalent authority and goal CREATION is Metic+ — so the transfer target
|
|
233
|
+
// must be Metic+ too (fail-closed; this also keeps the protected-scope
|
|
234
|
+
// membership wall intact, since a protected-scope goal requires Metic+ members).
|
|
235
|
+
// Ordering: goal_not_found → goal_not_open → not_owner → builder_not_found →
|
|
236
|
+
// metic_required_for_ownership → no-op self-transfer.
|
|
237
|
+
function authorizeOwnershipTransfer({ goal, actorId, actorRank, targetBuilder }) {
|
|
238
|
+
if (!goal) {
|
|
239
|
+
return { ok: false, status: 404, body: { error: 'goal_not_found' } };
|
|
240
|
+
}
|
|
241
|
+
if (goal.status !== 'open') {
|
|
242
|
+
return { ok: false, status: 409, body: {
|
|
243
|
+
error: 'goal_not_open', status: goal.status,
|
|
244
|
+
message: 'Ownership can only be transferred on an open goal.',
|
|
245
|
+
} };
|
|
246
|
+
}
|
|
247
|
+
if (actorRank !== 'archon' && !isGoalOwner(goal, actorId)) {
|
|
248
|
+
return { ok: false, status: 403, body: {
|
|
249
|
+
error: 'not_owner',
|
|
250
|
+
message: 'Only the goal owner (or an Archon) may transfer ownership.',
|
|
251
|
+
} };
|
|
252
|
+
}
|
|
253
|
+
if (!targetBuilder) {
|
|
254
|
+
return { ok: false, status: 404, body: { error: 'builder_not_found' } };
|
|
255
|
+
}
|
|
256
|
+
if (!METIC_PLUS.includes(targetBuilder.rank)) {
|
|
257
|
+
return { ok: false, status: 403, body: {
|
|
258
|
+
error: 'metic_required_for_ownership',
|
|
259
|
+
message: 'Goal ownership is creator-equivalent authority — the new owner must be Metic+.',
|
|
260
|
+
} };
|
|
261
|
+
}
|
|
262
|
+
if (isGoalOwner(goal, targetBuilder.id)) {
|
|
263
|
+
return { ok: true, noop: true };
|
|
264
|
+
}
|
|
265
|
+
return { ok: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// -------------------------------------------------------------------------
|
|
269
|
+
// Private goals: the invite / join-request queue (task 1952 / ADR 0112). A
|
|
270
|
+
// private goal is join-GATED (not hidden): a non-member can't self-join — they
|
|
271
|
+
// are INVITED (a consented, pending row they accept) or REQUEST to join (an
|
|
272
|
+
// owner/manager/Archon approves). Both directions live in goal_membership_requests
|
|
273
|
+
// (kind='invite'|'request'); the resolve is atomic (db.resolveMembershipRequest
|
|
274
|
+
// moves only from status='pending', so a re-submit can't double-add a member).
|
|
275
|
+
// -------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
// protectedScopeAdmissionBlock — the SINGLE protected-scope admission rule reused
|
|
278
|
+
// by BOTH new admission paths (invite-create and join-request-approve) AND the
|
|
279
|
+
// existing POST /goals/:id/members (whose inline checks this generalizes). A
|
|
280
|
+
// protected-scope goal admits members only via an Archon, and only Metic+ builders
|
|
281
|
+
// — sub-Metic membership inside protected scope is deferred to ADR 0086's Phase-5
|
|
282
|
+
// ADR. Centralized so the protected wall can never drift between paths (the SEC
|
|
283
|
+
// #863 single-roster lesson). Returns { status, body } to send, or null to proceed.
|
|
284
|
+
function protectedScopeAdmissionBlock({ scopeProtected, actorRank, targetRank }) {
|
|
285
|
+
if (!scopeProtected) return null;
|
|
286
|
+
if (actorRank !== 'archon') {
|
|
287
|
+
return { status: 403, body: {
|
|
288
|
+
error: 'archon_required_for_protected_scope',
|
|
289
|
+
message: 'This goal is scoped to a protected module; only an Archon may admit members.',
|
|
290
|
+
} };
|
|
291
|
+
}
|
|
292
|
+
if (!METIC_PLUS.includes(targetRank)) {
|
|
293
|
+
return { status: 403, body: {
|
|
294
|
+
error: 'metic_required_for_protected_scope',
|
|
295
|
+
message: 'A protected-scope goal requires a Metic+ member; sub-Metic membership is deferred to the Phase-5 ADR.',
|
|
296
|
+
} };
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// authorizeCreateJoinRequest — the ORDERED, FAIL-CLOSED gate for
|
|
302
|
+
// POST /goals/:id/requests (task 1952): a builder asks to join. A request only
|
|
303
|
+
// makes sense where self-join is gated — a PUBLIC, NON-protected goal is open-join,
|
|
304
|
+
// so a request there is refused with a "just join" hint. Ordering:
|
|
305
|
+
// goal_not_found → goal_not_open → already_a_member → goal_is_open (public+unprotected).
|
|
306
|
+
// isMember is the caller's own membership; scopeProtected is scopeIncludesProtected(goal).
|
|
307
|
+
// PURE + exported (the authorizeGoalTaskCreate precedent).
|
|
308
|
+
function authorizeCreateJoinRequest({ goal, isMember, scopeProtected }) {
|
|
309
|
+
if (!goal) return { ok: false, status: 404, body: { error: 'goal_not_found' } };
|
|
310
|
+
if (goal.status !== 'open') {
|
|
311
|
+
return { ok: false, status: 409, body: {
|
|
312
|
+
error: 'goal_not_open', status: goal.status,
|
|
313
|
+
message: 'You can only request to join an open goal.',
|
|
314
|
+
} };
|
|
315
|
+
}
|
|
316
|
+
if (isMember) {
|
|
317
|
+
return { ok: false, status: 409, body: {
|
|
318
|
+
error: 'already_a_member', message: 'You are already a member of this goal.',
|
|
319
|
+
} };
|
|
320
|
+
}
|
|
321
|
+
if (goal.visibility !== 'private' && !scopeProtected) {
|
|
322
|
+
return { ok: false, status: 409, body: {
|
|
323
|
+
error: 'goal_is_open',
|
|
324
|
+
message: 'This goal is open — just join it (POST /goals/:id/join); no request needed.',
|
|
325
|
+
} };
|
|
326
|
+
}
|
|
327
|
+
return { ok: true };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// authorizeInvitationResponse — the ORDERED, FAIL-CLOSED gate for
|
|
331
|
+
// POST /goals/:id/invitations/:reqId/respond (task 1952): ONLY the invited builder
|
|
332
|
+
// may accept/decline their OWN pending invite. Containment first — the row must
|
|
333
|
+
// belong to THIS goal and be an 'invite' — so the invitation route can never touch
|
|
334
|
+
// a join-request row. Ordering: request_not_found → wrong_goal → not_an_invitation
|
|
335
|
+
// → not_pending → not_your_invitation. PURE + exported.
|
|
336
|
+
function authorizeInvitationResponse({ request, goalId, actorId }) {
|
|
337
|
+
if (!request) return { ok: false, status: 404, body: { error: 'request_not_found' } };
|
|
338
|
+
if (String(request.goal_id) !== String(goalId)) {
|
|
339
|
+
return { ok: false, status: 404, body: { error: 'request_not_found', message: 'No such invitation on this goal.' } };
|
|
340
|
+
}
|
|
341
|
+
if (request.kind !== 'invite') {
|
|
342
|
+
return { ok: false, status: 400, body: { error: 'not_an_invitation', message: 'That row is a join-request, not an invitation.' } };
|
|
343
|
+
}
|
|
344
|
+
if (request.status !== 'pending') {
|
|
345
|
+
return { ok: false, status: 409, body: { error: 'not_pending', status: request.status } };
|
|
346
|
+
}
|
|
347
|
+
if (String(request.builder_id) !== String(actorId)) {
|
|
348
|
+
return { ok: false, status: 403, body: {
|
|
349
|
+
error: 'not_your_invitation',
|
|
350
|
+
message: 'Only the invited builder may accept or decline this invitation.',
|
|
351
|
+
} };
|
|
352
|
+
}
|
|
353
|
+
return { ok: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// authorizeJoinRequestResponse — the gate for POST /goals/:id/requests/:reqId/respond
|
|
357
|
+
// (task 1952): an owner|manager|Archon approves/denies a pending join-request. The
|
|
358
|
+
// authority prefix is authorizeGoalManagement VERBATIM (goal exists/open + owner|
|
|
359
|
+
// manager|Archon — checked BEFORE the request lookup so an unauthorized actor never
|
|
360
|
+
// learns whether a request exists, the O1 no-leak ordering). Then request
|
|
361
|
+
// containment + state: request_not_found → wrong_goal → not_a_join_request →
|
|
362
|
+
// not_pending. PURE + exported.
|
|
363
|
+
function authorizeJoinRequestResponse({ goal, actorId, actorRank, actorMembership, request, goalId }) {
|
|
364
|
+
const mgmt = authorizeGoalManagement({ goal, actorId, actorRank, actorMembership });
|
|
365
|
+
if (!mgmt.ok) return mgmt;
|
|
366
|
+
if (!request) return { ok: false, status: 404, body: { error: 'request_not_found' } };
|
|
367
|
+
if (String(request.goal_id) !== String(goalId)) {
|
|
368
|
+
return { ok: false, status: 404, body: { error: 'request_not_found', message: 'No such join-request on this goal.' } };
|
|
369
|
+
}
|
|
370
|
+
if (request.kind !== 'request') {
|
|
371
|
+
return { ok: false, status: 400, body: { error: 'not_a_join_request', message: 'That row is an invitation, not a join-request.' } };
|
|
372
|
+
}
|
|
373
|
+
if (request.status !== 'pending') {
|
|
374
|
+
return { ok: false, status: 409, body: { error: 'not_pending', status: request.status } };
|
|
375
|
+
}
|
|
376
|
+
return { ok: true };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = function buildGoalsRouter() {
|
|
380
|
+
const router = express.Router();
|
|
381
|
+
|
|
382
|
+
// POST /goals — create a goal.
|
|
383
|
+
// rank: metic+archon — a Metic or Archon may create a goal; creation of a goal
|
|
384
|
+
// whose scope_modules includes a PROTECTED module is Archon-only (in-handler).
|
|
385
|
+
router.post('/goals', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
|
|
386
|
+
if (validateOrRespond(req, res, {
|
|
387
|
+
version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
|
|
388
|
+
title: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
|
|
389
|
+
description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
|
|
390
|
+
scope_modules: { type: 'array', maxItems: 64, itemsType: 'string' },
|
|
391
|
+
succeeds_goal_id: { type: 'integer', min: 1 },
|
|
392
|
+
})) return;
|
|
393
|
+
|
|
394
|
+
const scopeModules = req.body.scope_modules || [];
|
|
395
|
+
// Fail closed on an unknown module key — the scope wall is meaningless if it
|
|
396
|
+
// can name a module that doesn't exist.
|
|
397
|
+
const unknown = scopeModules.filter((k) => !isModuleKey(k));
|
|
398
|
+
if (unknown.length) {
|
|
399
|
+
return res.fail('unknown_module', 400, { unknown, valid: moduleKeys() });
|
|
400
|
+
}
|
|
401
|
+
// The scope is set by a trusted rank at planning time; a protected-module
|
|
402
|
+
// scope can only be set by an Archon (ADR 0086 §3/§4).
|
|
403
|
+
if (scopeIncludesProtected(scopeModules) && req.builder.rank !== 'archon') {
|
|
404
|
+
return res.fail('archon_required_for_protected_scope', { status: 403, message: 'A goal scoped to a protected module can only be created by an Archon.' });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const version = await db.getVersion(req.body.version_id);
|
|
409
|
+
if (!version) {
|
|
410
|
+
return res.fail('version_not_found', { status: 404, message: 'No such version.' });
|
|
411
|
+
}
|
|
412
|
+
// Rescope carry-forward (ADR 0086 §6 / BV1.R64): a new-version goal may declare
|
|
413
|
+
// the (achieved) goal it continues. Validate the predecessor exists BEFORE
|
|
414
|
+
// creating the successor, so a bad ref can't leave an orphan goal behind.
|
|
415
|
+
const succeedsId = req.body.succeeds_goal_id ?? null;
|
|
416
|
+
if (succeedsId != null) {
|
|
417
|
+
const predecessor = await db.getGoal(succeedsId);
|
|
418
|
+
if (!predecessor) {
|
|
419
|
+
return res.fail('predecessor_not_found', { status: 404, message: 'No such goal to carry forward.' });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const goal = await db.createGoal({
|
|
423
|
+
versionId: req.body.version_id,
|
|
424
|
+
title: req.body.title,
|
|
425
|
+
description: (req.body.description || '').trim() || null,
|
|
426
|
+
scopeModules,
|
|
427
|
+
createdBy: req.builder.id,
|
|
428
|
+
});
|
|
429
|
+
// Stamp the predecessor's succeeded_by pointer at the new goal (the lineage).
|
|
430
|
+
if (succeedsId != null) {
|
|
431
|
+
await db.setGoalSuccessor({ goalId: succeedsId, successorId: goal.id });
|
|
432
|
+
}
|
|
433
|
+
return res.status(201).json({ ok: true, goal, succeeds_goal_id: succeedsId });
|
|
434
|
+
} catch (err) {
|
|
435
|
+
console.error('[gds] POST /goals', err);
|
|
436
|
+
return res.fail('goal_create_failed', { status: 500, message: 'internal error' });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// GET /goals?version=&status= — list goals (read).
|
|
441
|
+
// rank: any-builder — viewing the goal board.
|
|
442
|
+
router.get('/goals', auth.requireBuilder, async (req, res) => {
|
|
443
|
+
const { version, status } = req.query;
|
|
444
|
+
if (status && !VALID_GOAL_STATUSES.includes(status)) {
|
|
445
|
+
return res.fail('bad_status', 400, { valid: VALID_GOAL_STATUSES });
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const goals = await db.listGoals({ versionId: version || null, status: status || null });
|
|
449
|
+
return res.json({ goals });
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.error('[gds] GET /goals', err);
|
|
452
|
+
return res.fail('list_failed', { status: 500, message: 'internal error' });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// POST /goals/reorder — Archon drag-to-reorder: set the priority order of
|
|
457
|
+
// every goal WITHIN one version (task 1760). ordered_ids must be exactly
|
|
458
|
+
// that version's goal ids, each once (db.reorderGoals fails closed
|
|
459
|
+
// otherwise) — a partial list can't silently orphan a goal's priority.
|
|
460
|
+
// Mounted BEFORE GET /goals/:id so 'reorder' is never captured as an id.
|
|
461
|
+
// rank: archon — priority is an owner decision, never self-granted by a
|
|
462
|
+
// goal's own member/manager (unlike the goal-authority routes above).
|
|
463
|
+
router.post('/goals/reorder', auth.requireBuilder, auth.requireRank('archon'), async (req, res) => {
|
|
464
|
+
if (validateOrRespond(req, res, {
|
|
465
|
+
version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
|
|
466
|
+
ordered_ids: { required: true, type: 'array', maxItems: 500, itemsType: 'integer' },
|
|
467
|
+
})) return;
|
|
468
|
+
const versionId = req.body.version_id;
|
|
469
|
+
const orderedIds = req.body.ordered_ids;
|
|
470
|
+
if (orderedIds.length === 0 || orderedIds.some((id) => !Number.isInteger(id) || id <= 0)) {
|
|
471
|
+
return res.fail('bad_ordered_ids', { status: 400, message: 'ordered_ids must be a non-empty list of positive integers.' });
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const version = await db.getVersion(versionId);
|
|
475
|
+
if (!version) return res.fail('version_not_found', 404);
|
|
476
|
+
const goals = await db.reorderGoals({ versionId, orderedIds });
|
|
477
|
+
return res.json({ ok: true, goals });
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (err && err.code === 'SET_MISMATCH') {
|
|
480
|
+
return res.fail('set_mismatch', { status: 400, message: err.message });
|
|
481
|
+
}
|
|
482
|
+
console.error('[gds] POST /goals/reorder', err);
|
|
483
|
+
return res.fail('reorder_failed', { status: 500, message: 'internal error' });
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// GET /goals/inbox — the caller's private-goals inbox (task 1952 / ADR 0112):
|
|
488
|
+
// { invitations: invites addressed to me (accept/decline); joinRequests: pending
|
|
489
|
+
// join-requests on goals I own or manage (approve/deny); myPendingRequests: my own
|
|
490
|
+
// outstanding requests }. Powers the home-page inbox widget (task 1954) + the
|
|
491
|
+
// goals page. Mounted BEFORE GET /goals/:id so the literal 'inbox' path is matched
|
|
492
|
+
// as its own route, not captured as an id (the /goals/reorder precedent).
|
|
493
|
+
// rank: any-builder — every builder reads their OWN inbox (db.listInboxFor scopes
|
|
494
|
+
// by req.builder.id; a builder can't read another's).
|
|
495
|
+
router.get('/goals/inbox', auth.requireBuilder, async (req, res) => {
|
|
496
|
+
try {
|
|
497
|
+
const inbox = await db.listInboxFor(req.builder.id);
|
|
498
|
+
return res.json(inbox);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.error('[gds] GET /goals/inbox', err);
|
|
501
|
+
return res.fail('inbox_failed', { status: 500, message: 'internal error' });
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// GET /goals/:id — one goal with its members + linked criteria (read).
|
|
506
|
+
// rank: any-builder.
|
|
507
|
+
router.get('/goals/:id', auth.requireBuilder, async (req, res) => {
|
|
508
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
509
|
+
if (id === null) return;
|
|
510
|
+
try {
|
|
511
|
+
const goal = await db.getGoal(id);
|
|
512
|
+
if (!goal) return res.fail('goal_not_found', 404);
|
|
513
|
+
const [members, criteria, dependencies] = await Promise.all([
|
|
514
|
+
db.listGoalMembers(id),
|
|
515
|
+
db.listCriteriaForGoal(id),
|
|
516
|
+
db.getGoalDependencies(id), // task 1761 — prerequisite goals + satisfied state
|
|
517
|
+
]);
|
|
518
|
+
return res.json({ goal, members, criteria, dependencies });
|
|
519
|
+
} catch (err) {
|
|
520
|
+
console.error('[gds] GET /goals/:id', err);
|
|
521
|
+
return res.fail('fetch_failed', { status: 500, message: 'internal error' });
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// POST /goals/:id/join — join an open goal.
|
|
526
|
+
// rank: any-builder — open join for a NORMAL goal. A PROTECTED-scope goal is
|
|
527
|
+
// Archon-mediated: a non-Archon cannot self-join (an Archon admits them via
|
|
528
|
+
// POST /goals/:id/members). Sub-Metic membership inside protected scope is
|
|
529
|
+
// deferred to the Phase-5 ADR (ADR 0086 Deferred).
|
|
530
|
+
router.post('/goals/:id/join', auth.requireBuilder, async (req, res) => {
|
|
531
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
532
|
+
if (id === null) return;
|
|
533
|
+
try {
|
|
534
|
+
const goal = await db.getGoal(id);
|
|
535
|
+
if (!goal) return res.fail('goal_not_found', 404);
|
|
536
|
+
if (goal.status !== 'open') {
|
|
537
|
+
return res.fail('goal_not_open', 409, { status: goal.status });
|
|
538
|
+
}
|
|
539
|
+
if (scopeIncludesProtected(goal.scope_modules) && req.builder.rank !== 'archon') {
|
|
540
|
+
return res.fail('archon_approval_required', { status: 403, message: 'This goal is scoped to a protected module; an Archon must add you (POST /goals/:id/members).' });
|
|
541
|
+
}
|
|
542
|
+
// Private goals are join-GATED (task 1952 / ADR 0112): a non-member can't
|
|
543
|
+
// self-join — they request to join or wait for an invite. An existing member
|
|
544
|
+
// re-joining is a harmless upsert, and an Archon may still self-join anything.
|
|
545
|
+
if (goal.visibility === 'private' && req.builder.rank !== 'archon') {
|
|
546
|
+
const alreadyMember = await db.isGoalMember({ goalId: id, builderId: req.builder.id });
|
|
547
|
+
if (!alreadyMember) {
|
|
548
|
+
return res.fail('goal_private', { status: 409, message: 'This goal is private — request to join (POST /goals/:id/requests) or wait for an invitation.' });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const member = await db.addGoalMember({ goalId: id, builderId: req.builder.id, role: 'member' });
|
|
552
|
+
return res.status(201).json({ ok: true, member });
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.error('[gds] POST /goals/:id/join', err);
|
|
555
|
+
return res.fail('join_failed', { status: 500, message: 'internal error' });
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// POST /goals/:id/leave — leave a goal you're in.
|
|
560
|
+
// rank: any-builder.
|
|
561
|
+
router.post('/goals/:id/leave', auth.requireBuilder, async (req, res) => {
|
|
562
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
563
|
+
if (id === null) return;
|
|
564
|
+
try {
|
|
565
|
+
const { removed } = await db.removeGoalMember({ goalId: id, builderId: req.builder.id });
|
|
566
|
+
if (!removed) return res.fail('not_a_member', 409);
|
|
567
|
+
return res.json({ ok: true });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
console.error('[gds] POST /goals/:id/leave', err);
|
|
570
|
+
return res.fail('leave_failed', { status: 500, message: 'internal error' });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// PATCH /goals/:id/visibility — make a goal public|private (task 1952 / ADR 0112).
|
|
575
|
+
// The owner, a manager, or an Archon (authorizeGoalManagement) flips it; a private
|
|
576
|
+
// goal is join-gated (see POST /goals/:id/join). 'public' is the default.
|
|
577
|
+
// rank: any-builder (requireBuilder) + the in-handler owner|manager|Archon gate.
|
|
578
|
+
// Pinned in route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
|
|
579
|
+
router.patch('/goals/:id/visibility', auth.requireBuilder, async (req, res) => {
|
|
580
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
581
|
+
if (id === null) return;
|
|
582
|
+
if (validateOrRespond(req, res, { visibility: { required: true, type: 'string' } })) return;
|
|
583
|
+
const visibility = req.body.visibility;
|
|
584
|
+
if (!VALID_VISIBILITIES.includes(visibility)) {
|
|
585
|
+
return res.fail('bad_visibility', 400, { valid: VALID_VISIBILITIES });
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const goal = await db.getGoal(id);
|
|
589
|
+
const actorMembership = goal
|
|
590
|
+
? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
|
|
591
|
+
: null;
|
|
592
|
+
const decision = authorizeGoalManagement({
|
|
593
|
+
goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
|
|
594
|
+
});
|
|
595
|
+
if (!decision.ok) return res.status(decision.status).json(decision.body);
|
|
596
|
+
const updated = await db.setGoalVisibility({ goalId: id, visibility });
|
|
597
|
+
return res.json({ ok: true, goal: updated });
|
|
598
|
+
} catch (err) {
|
|
599
|
+
console.error('[gds] PATCH /goals/:id/visibility', err);
|
|
600
|
+
return res.fail('visibility_update_failed', { status: 500, message: 'internal error' });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// POST /goals/:id/requests — a builder asks to join a (private or protected)
|
|
605
|
+
// goal (task 1952 / ADR 0112). Refused on a public, non-protected goal (that's
|
|
606
|
+
// open-join — just POST /goals/:id/join). Creates a pending kind='request' the
|
|
607
|
+
// goal's owner/manager/Archon approves from their inbox. The pending-unique index
|
|
608
|
+
// (one open ask per goal+builder) surfaces as 409 request_pending.
|
|
609
|
+
// rank: any-builder (requireBuilder) + the in-handler authorizeCreateJoinRequest.
|
|
610
|
+
router.post('/goals/:id/requests', auth.requireBuilder, async (req, res) => {
|
|
611
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
612
|
+
if (id === null) return;
|
|
613
|
+
if (validateOrRespond(req, res, { note: { type: 'string', maxLength: LIMITS.NOTES } })) return;
|
|
614
|
+
try {
|
|
615
|
+
const goal = await db.getGoal(id);
|
|
616
|
+
const isMember = goal ? await db.isGoalMember({ goalId: id, builderId: req.builder.id }) : false;
|
|
617
|
+
const scopeProtected = goal ? scopeIncludesProtected(goal.scope_modules) : false;
|
|
618
|
+
const decision = authorizeCreateJoinRequest({ goal, isMember, scopeProtected });
|
|
619
|
+
if (!decision.ok) return res.status(decision.status).json(decision.body);
|
|
620
|
+
const request = await db.createMembershipRequest({
|
|
621
|
+
goalId: id, builderId: req.builder.id, kind: 'request',
|
|
622
|
+
createdBy: req.builder.id, note: (req.body.note || '').trim() || null,
|
|
623
|
+
});
|
|
624
|
+
return res.status(201).json({ ok: true, request });
|
|
625
|
+
} catch (err) {
|
|
626
|
+
if (err && err.code === 'PENDING_EXISTS') {
|
|
627
|
+
return res.fail('request_pending', { status: 409, message: 'You already have a pending request or invitation for this goal.' });
|
|
628
|
+
}
|
|
629
|
+
console.error('[gds] POST /goals/:id/requests', err);
|
|
630
|
+
return res.fail('request_failed', { status: 500, message: 'internal error' });
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// POST /goals/:id/requests/:reqId/respond { action: approve|deny } — an owner|
|
|
635
|
+
// manager|Archon resolves a pending join-request (task 1952). approve → the
|
|
636
|
+
// requester is added as a member; deny → the row is rejected (kept as the audit
|
|
637
|
+
// trail). The membership add is atomic with the resolve (resolveMembershipRequest
|
|
638
|
+
// moves only from status='pending'). A protected-scope goal still admits only
|
|
639
|
+
// Metic+ via an Archon — the same wall as invite + POST /members.
|
|
640
|
+
// rank: any-builder (requireBuilder) + the in-handler authorizeJoinRequestResponse.
|
|
641
|
+
router.post('/goals/:id/requests/:reqId/respond', auth.requireBuilder, async (req, res) => {
|
|
642
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
643
|
+
if (id === null) return;
|
|
644
|
+
const reqId = parseId(req, res, { param: 'reqId', code: 'bad_request_id', positiveInt: true });
|
|
645
|
+
if (reqId === null) return;
|
|
646
|
+
if (validateOrRespond(req, res, { action: { required: true, type: 'string' } })) return;
|
|
647
|
+
const action = req.body.action;
|
|
648
|
+
if (!JOIN_REQUEST_RESPONSE_ACTIONS.includes(action)) {
|
|
649
|
+
return res.fail('bad_action', 400, { valid: JOIN_REQUEST_RESPONSE_ACTIONS });
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const goal = await db.getGoal(id);
|
|
653
|
+
const [actorMembership, request] = goal
|
|
654
|
+
? await Promise.all([
|
|
655
|
+
db.getGoalMember({ goalId: id, builderId: req.builder.id }),
|
|
656
|
+
db.getMembershipRequest(reqId),
|
|
657
|
+
])
|
|
658
|
+
: [null, null];
|
|
659
|
+
const decision = authorizeJoinRequestResponse({
|
|
660
|
+
goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership, request, goalId: id,
|
|
661
|
+
});
|
|
662
|
+
if (!decision.ok) return res.status(decision.status).json(decision.body);
|
|
663
|
+
|
|
664
|
+
if (action === 'deny') {
|
|
665
|
+
const resolved = await db.resolveMembershipRequest({ id: reqId, status: 'rejected', resolvedBy: req.builder.id });
|
|
666
|
+
if (!resolved) return res.fail('not_pending', { status: 409, message: 'The request changed underneath this response — re-read and retry.' });
|
|
667
|
+
return res.json({ ok: true, action, request: resolved });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// approve — the protected-scope admission wall, read against the goal's
|
|
671
|
+
// CURRENT scope (re-fetched here, not the top-of-handler read, so an Archon
|
|
672
|
+
// re-scope since then is seen — the invitation-accept path's pattern). The
|
|
673
|
+
// residual micro-race with a concurrent re-scope is defense-in-depth: ADR
|
|
674
|
+
// 0086 §7 makes the SHIP-TIME gates authoritative on the real diff, and this
|
|
675
|
+
// mirrors the POST /goals/:id/members admission path — a stale membership row
|
|
676
|
+
// never grants actual protected-code access.
|
|
677
|
+
const freshGoal = (await db.getGoal(id)) || goal;
|
|
678
|
+
if (scopeIncludesProtected(freshGoal.scope_modules)) {
|
|
679
|
+
const target = await api.getBuilderById(request.builder_id);
|
|
680
|
+
const block = protectedScopeAdmissionBlock({
|
|
681
|
+
scopeProtected: true, actorRank: req.builder.rank, targetRank: target && target.rank,
|
|
682
|
+
});
|
|
683
|
+
if (block) return res.status(block.status).json(block.body);
|
|
684
|
+
}
|
|
685
|
+
// Resolve + add atomically (one statement); the requester (request.builder_id)
|
|
686
|
+
// becomes the member. null = the request was resolved out from under us → 409.
|
|
687
|
+
const outcome = await db.acceptMembershipRequestAndAddMember({ id: reqId, resolvedBy: req.builder.id });
|
|
688
|
+
if (!outcome) return res.fail('not_pending', { status: 409, message: 'The request changed underneath this response — re-read and retry.' });
|
|
689
|
+
return res.json({ ok: true, action, request: outcome.request, member: outcome.member });
|
|
690
|
+
} catch (err) {
|
|
691
|
+
console.error('[gds] POST /goals/:id/requests/:reqId/respond', err);
|
|
692
|
+
return res.fail('request_respond_failed', { status: 500, message: 'internal error' });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// POST /goals/:id/invitations { builder_id, note? } — an owner|manager|Archon
|
|
697
|
+
// invites a builder (task 1952). Creates a PENDING kind='invite' the invitee
|
|
698
|
+
// accepts from their inbox (a consented add — distinct from POST /goals/:id/members,
|
|
699
|
+
// the immediate force-add, kept as-is). Protected-scope invites keep the Archon-
|
|
700
|
+
// actor + Metic+-target wall (protectedScopeAdmissionBlock). 409 if the target is
|
|
701
|
+
// already a member or already has a pending row.
|
|
702
|
+
// rank: any-builder (requireBuilder) + the in-handler owner|manager|Archon gate.
|
|
703
|
+
router.post('/goals/:id/invitations', auth.requireBuilder, async (req, res) => {
|
|
704
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
705
|
+
if (id === null) return;
|
|
706
|
+
if (validateOrRespond(req, res, {
|
|
707
|
+
note: { type: 'string', maxLength: LIMITS.NOTES },
|
|
708
|
+
builder_id: {}, // number or numeric string (coerced below); untyped for strict-mode
|
|
709
|
+
})) return;
|
|
710
|
+
// builder_id accepts a JSON number OR a numeric string (the /members precedent —
|
|
711
|
+
// pg serializes bigint ids as strings, so a client echoing an id would fail an
|
|
712
|
+
// integer-typed validator).
|
|
713
|
+
const builderId = Number(req.body.builder_id);
|
|
714
|
+
if (!Number.isInteger(builderId) || builderId <= 0) {
|
|
715
|
+
return res.fail('bad_builder_id', { status: 400, message: 'builder_id must be a positive integer.' });
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const goal = await db.getGoal(id);
|
|
719
|
+
const actorMembership = goal
|
|
720
|
+
? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
|
|
721
|
+
: null;
|
|
722
|
+
const decision = authorizeGoalManagement({
|
|
723
|
+
goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
|
|
724
|
+
});
|
|
725
|
+
if (!decision.ok) return res.status(decision.status).json(decision.body);
|
|
726
|
+
const target = await api.getBuilderById(builderId);
|
|
727
|
+
if (!target) return res.fail('builder_not_found', 404);
|
|
728
|
+
// Protected-scope wall against the goal's CURRENT scope (re-fetched, not the
|
|
729
|
+
// top-of-handler read — matches the respond paths). Residual re-scope race is
|
|
730
|
+
// defense-in-depth (ADR 0086 §7 — ship-time gates authoritative; mirrors
|
|
731
|
+
// POST /goals/:id/members).
|
|
732
|
+
const freshGoal = (await db.getGoal(id)) || goal;
|
|
733
|
+
const block = protectedScopeAdmissionBlock({
|
|
734
|
+
scopeProtected: scopeIncludesProtected(freshGoal.scope_modules),
|
|
735
|
+
actorRank: req.builder.rank, targetRank: target.rank,
|
|
736
|
+
});
|
|
737
|
+
if (block) return res.status(block.status).json(block.body);
|
|
738
|
+
if (await db.isGoalMember({ goalId: id, builderId })) {
|
|
739
|
+
return res.fail('already_a_member', { status: 409, message: 'That builder is already a member of this goal.' });
|
|
740
|
+
}
|
|
741
|
+
const request = await db.createMembershipRequest({
|
|
742
|
+
goalId: id, builderId, kind: 'invite',
|
|
743
|
+
createdBy: req.builder.id, note: (req.body.note || '').trim() || null,
|
|
744
|
+
});
|
|
745
|
+
return res.status(201).json({ ok: true, request });
|
|
746
|
+
} catch (err) {
|
|
747
|
+
if (err && err.code === 'PENDING_EXISTS') {
|
|
748
|
+
return res.fail('invite_pending', { status: 409, message: 'That builder already has a pending invitation or request for this goal.' });
|
|
749
|
+
}
|
|
750
|
+
console.error('[gds] POST /goals/:id/invitations', err);
|
|
751
|
+
return res.fail('invite_failed', { status: 500, message: 'internal error' });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// POST /goals/:id/invitations/:reqId/respond { action: accept|decline } — ONLY the
|
|
756
|
+
// invited builder resolves their OWN pending invite (task 1952). accept → they're
|
|
757
|
+
// added as a member; decline → the row is rejected. A protected-scope goal still
|
|
758
|
+
// requires a Metic+ member, re-checked against the accepting builder's current rank
|
|
759
|
+
// (the goal could have been re-scoped after the invite).
|
|
760
|
+
// rank: any-builder (requireBuilder) + the in-handler authorizeInvitationResponse.
|
|
761
|
+
router.post('/goals/:id/invitations/:reqId/respond', auth.requireBuilder, async (req, res) => {
|
|
762
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
763
|
+
if (id === null) return;
|
|
764
|
+
const reqId = parseId(req, res, { param: 'reqId', code: 'bad_request_id', positiveInt: true });
|
|
765
|
+
if (reqId === null) return;
|
|
766
|
+
if (validateOrRespond(req, res, { action: { required: true, type: 'string' } })) return;
|
|
767
|
+
const action = req.body.action;
|
|
768
|
+
if (!INVITE_RESPONSE_ACTIONS.includes(action)) {
|
|
769
|
+
return res.fail('bad_action', 400, { valid: INVITE_RESPONSE_ACTIONS });
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
const request = await db.getMembershipRequest(reqId);
|
|
773
|
+
const decision = authorizeInvitationResponse({ request, goalId: id, actorId: req.builder.id });
|
|
774
|
+
if (!decision.ok) return res.status(decision.status).json(decision.body);
|
|
775
|
+
|
|
776
|
+
if (action === 'decline') {
|
|
777
|
+
const resolved = await db.resolveMembershipRequest({ id: reqId, status: 'rejected', resolvedBy: req.builder.id });
|
|
778
|
+
if (!resolved) return res.fail('not_pending', { status: 409, message: 'The invitation changed underneath this response — re-read and retry.' });
|
|
779
|
+
return res.json({ ok: true, action, request: resolved });
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// accept — re-assert the protected-scope Metic+ membership invariant against
|
|
783
|
+
// the accepting builder's CURRENT rank (the goal may have been re-scoped since
|
|
784
|
+
// the invite). The invitee is consenting, so the actor-is-Archon half of the
|
|
785
|
+
// admission rule does NOT apply here — only the Metic+ membership floor does.
|
|
786
|
+
const goal = await db.getGoal(id);
|
|
787
|
+
if (goal && scopeIncludesProtected(goal.scope_modules) && !METIC_PLUS.includes(req.builder.rank)) {
|
|
788
|
+
return res.fail('metic_required_for_protected_scope', { status: 403, message: 'This goal is scoped to a protected module and requires a Metic+ member.' });
|
|
789
|
+
}
|
|
790
|
+
// Resolve + add atomically (one statement) — never an 'accepted' invite
|
|
791
|
+
// with no membership (the task-1952 review fix).
|
|
792
|
+
const outcome = await db.acceptMembershipRequestAndAddMember({ id: reqId, resolvedBy: req.builder.id });
|
|
793
|
+
if (!outcome) return res.fail('not_pending', { status: 409, message: 'The invitation changed underneath this response — re-read and retry.' });
|
|
794
|
+
return res.json({ ok: true, action, request: outcome.request, member: outcome.member });
|
|
795
|
+
} catch (err) {
|
|
796
|
+
console.error('[gds] POST /goals/:id/invitations/:reqId/respond', err);
|
|
797
|
+
return res.fail('invitation_respond_failed', { status: 500, message: 'internal error' });
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// POST /goals/:id/members — the goal's own authority (owner | manager | Archon)
|
|
802
|
+
// invites a builder to the goal (task 1762: widened from Archon-only so a
|
|
803
|
+
// manager doesn't need an Archon in the loop for a normal goal). This stays the
|
|
804
|
+
// approval grant for a protected-scope goal, which a non-Archon cannot self-
|
|
805
|
+
// join AND a non-Archon cannot invite into (archon_required_for_protected_scope
|
|
806
|
+
// below) — protected-scope admission is deliberately Archon-mediated on BOTH
|
|
807
|
+
// sides of the grant. For a protected-scope goal the target must additionally
|
|
808
|
+
// be Metic+ (ADR 0086 Deferred: protected-scope goals require a Metic+ holder
|
|
809
|
+
// until the Phase-5 ADR).
|
|
810
|
+
// rank: any-builder (requireBuilder) + the in-handler (owner | manager |
|
|
811
|
+
// Archon) gate (authorizeGoalManagement) — like the sibling owner/manager
|
|
812
|
+
// routes, the wall is in-handler, NOT a rank middleware. Pinned in
|
|
813
|
+
// route-rank-check.EXPECTED_RANKS.
|
|
814
|
+
router.post('/goals/:id/members', auth.requireBuilder, async (req, res) => {
|
|
815
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
816
|
+
if (id === null) return;
|
|
817
|
+
if (validateOrRespond(req, res, {
|
|
818
|
+
role: { type: 'string' },
|
|
819
|
+
// builder_id accepts a JSON number OR a numeric string (coerced below);
|
|
820
|
+
// declared untyped so strict-mode accepts either form.
|
|
821
|
+
builder_id: {},
|
|
822
|
+
})) return;
|
|
823
|
+
// builder_id accepts a JSON number OR a numeric string: ids round-trip
|
|
824
|
+
// through JSON as strings (pg serializes bigint as a string), so a client
|
|
825
|
+
// echoing an id from a goal-members response would otherwise be rejected.
|
|
826
|
+
const builderId = Number(req.body.builder_id);
|
|
827
|
+
if (!Number.isInteger(builderId) || builderId <= 0) {
|
|
828
|
+
return res.fail('bad_builder_id', { status: 400, message: 'builder_id must be a positive integer.' });
|
|
829
|
+
}
|
|
830
|
+
const role = req.body.role || 'member';
|
|
831
|
+
if (!VALID_MEMBER_ROLES.includes(role)) {
|
|
832
|
+
return res.fail('bad_role', 400, { valid: VALID_MEMBER_ROLES });
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
const goal = await db.getGoal(id);
|
|
836
|
+
const actorMembership = goal
|
|
837
|
+
? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
|
|
838
|
+
: null;
|
|
839
|
+
const decision = authorizeGoalManagement({
|
|
840
|
+
goal,
|
|
841
|
+
actorId: req.builder.id,
|
|
842
|
+
actorRank: req.builder.rank,
|
|
843
|
+
actorMembership,
|
|
844
|
+
});
|
|
845
|
+
if (!decision.ok) {
|
|
846
|
+
return res.status(decision.status).json(decision.body);
|
|
847
|
+
}
|
|
848
|
+
// Protected-scope admission is Archon-mediated on the ACTOR side too — a
|
|
849
|
+
// goal's own owner/manager cannot use this widened grant to route around
|
|
850
|
+
// the protected-module wall (ADR 0086 §3/§4; mirrors the join-route check).
|
|
851
|
+
if (scopeIncludesProtected(goal.scope_modules) && req.builder.rank !== 'archon') {
|
|
852
|
+
return res.fail('archon_required_for_protected_scope', { status: 403, message: 'This goal is scoped to a protected module; only an Archon may add members.' });
|
|
853
|
+
}
|
|
854
|
+
const target = await api.getBuilderById(builderId);
|
|
855
|
+
if (!target) return res.fail('builder_not_found', 404);
|
|
856
|
+
if (scopeIncludesProtected(goal.scope_modules) && !METIC_PLUS.includes(target.rank)) {
|
|
857
|
+
return res.fail('metic_required_for_protected_scope', { status: 403, message: 'A protected-scope goal requires a Metic+ member; sub-Metic membership is deferred to the Phase-5 ADR.' });
|
|
858
|
+
}
|
|
859
|
+
const member = await db.addGoalMember({ goalId: id, builderId, role });
|
|
860
|
+
return res.status(201).json({ ok: true, member });
|
|
861
|
+
} catch (err) {
|
|
862
|
+
console.error('[gds] POST /goals/:id/members', err);
|
|
863
|
+
return res.fail('add_member_failed', { status: 500, message: 'internal error' });
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// PATCH /goals/:id/members/:builderId — role management by the goal's own
|
|
868
|
+
// authority (task 1735): the owner, a manager, or an Archon promotes
|
|
869
|
+
// member→lead(manager); ONLY the owner (or an Archon) demotes; the owner's
|
|
870
|
+
// row is never demotable. The decision is the pure ordered gate above
|
|
871
|
+
// (authorizeMemberRoleChange); the DB write re-asserts the owner-demotion
|
|
872
|
+
// guard atomically (setGoalMemberRole), so a race cannot demote an owner.
|
|
873
|
+
// rank: any-builder (requireBuilder) + the in-handler (owner | manager |
|
|
874
|
+
// Archon) gate — like the authoring keystone, the wall is in-handler, NOT a
|
|
875
|
+
// rank middleware. Pinned in route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
|
|
876
|
+
router.patch('/goals/:id/members/:builderId', auth.requireBuilder, async (req, res) => {
|
|
877
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
878
|
+
if (id === null) return;
|
|
879
|
+
const builderId = parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
|
|
880
|
+
if (builderId === null) return;
|
|
881
|
+
if (validateOrRespond(req, res, {
|
|
882
|
+
role: { required: true, type: 'string' },
|
|
883
|
+
})) return;
|
|
884
|
+
const role = req.body.role;
|
|
885
|
+
if (!VALID_MEMBER_ROLES.includes(role)) {
|
|
886
|
+
return res.fail('bad_role', 400, { valid: VALID_MEMBER_ROLES });
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const goal = await db.getGoal(id);
|
|
890
|
+
// The gate's reads — only taken when the goal exists (a 404 needs none).
|
|
891
|
+
const [actorMembership, targetMembership] = goal
|
|
892
|
+
? await Promise.all([
|
|
893
|
+
db.getGoalMember({ goalId: id, builderId: req.builder.id }),
|
|
894
|
+
db.getGoalMember({ goalId: id, builderId }),
|
|
895
|
+
])
|
|
896
|
+
: [null, null];
|
|
897
|
+
const decision = authorizeMemberRoleChange({
|
|
898
|
+
goal,
|
|
899
|
+
actorId: req.builder.id,
|
|
900
|
+
actorRank: req.builder.rank,
|
|
901
|
+
actorMembership,
|
|
902
|
+
targetMembership,
|
|
903
|
+
targetId: builderId,
|
|
904
|
+
role,
|
|
905
|
+
});
|
|
906
|
+
if (!decision.ok) {
|
|
907
|
+
return res.status(decision.status).json(decision.body);
|
|
908
|
+
}
|
|
909
|
+
if (decision.noop) {
|
|
910
|
+
return res.json({ ok: true, member: targetMembership, changed: false });
|
|
911
|
+
}
|
|
912
|
+
const member = await db.setGoalMemberRole({ goalId: id, builderId, role });
|
|
913
|
+
if (!member) {
|
|
914
|
+
// The atomic backstop fired: the membership vanished, or ownership
|
|
915
|
+
// moved onto the target between the gate's read and this write.
|
|
916
|
+
// Honest conflict, not silent success.
|
|
917
|
+
return res.fail('role_change_conflict', { status: 409, message: 'The goal membership changed underneath this request — re-read the goal and retry.' });
|
|
918
|
+
}
|
|
919
|
+
return res.json({ ok: true, member, changed: true });
|
|
920
|
+
} catch (err) {
|
|
921
|
+
console.error('[gds] PATCH /goals/:id/members/:builderId', err);
|
|
922
|
+
return res.fail('role_change_failed', { status: 500, message: 'internal error' });
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// POST /goals/:id/transfer — hand the goal to a new owner (task 1735). Owner
|
|
927
|
+
// (or Archon) only; the target must be Metic+ (ownership is creator-equivalent
|
|
928
|
+
// authority — see authorizeOwnershipTransfer). The DB move is atomic: created_by
|
|
929
|
+
// reassigns and the new owner's membership upserts to 'lead' in one statement;
|
|
930
|
+
// the OLD owner keeps their lead row — they drop to manager, not out.
|
|
931
|
+
// rank: any-builder (requireBuilder) + the in-handler (owner | Archon) gate.
|
|
932
|
+
// Pinned in route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
|
|
933
|
+
router.post('/goals/:id/transfer', auth.requireBuilder, async (req, res) => {
|
|
934
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
935
|
+
if (id === null) return;
|
|
936
|
+
if (validateOrRespond(req, res, {
|
|
937
|
+
builder_id: {}, // new owner id — number or numeric string (coerced below)
|
|
938
|
+
})) return;
|
|
939
|
+
// builder_id accepts a JSON number OR a numeric string (the /members
|
|
940
|
+
// precedent — pg serializes bigint ids as strings).
|
|
941
|
+
const newOwnerId = Number(req.body && req.body.builder_id);
|
|
942
|
+
if (!Number.isInteger(newOwnerId) || newOwnerId <= 0) {
|
|
943
|
+
return res.fail('bad_builder_id', { status: 400, message: 'builder_id must be a positive integer.' });
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
const goal = await db.getGoal(id);
|
|
947
|
+
const targetBuilder = goal ? await api.getBuilderById(newOwnerId) : null;
|
|
948
|
+
const decision = authorizeOwnershipTransfer({
|
|
949
|
+
goal,
|
|
950
|
+
actorId: req.builder.id,
|
|
951
|
+
actorRank: req.builder.rank,
|
|
952
|
+
targetBuilder,
|
|
953
|
+
});
|
|
954
|
+
if (!decision.ok) {
|
|
955
|
+
return res.status(decision.status).json(decision.body);
|
|
956
|
+
}
|
|
957
|
+
if (decision.noop) {
|
|
958
|
+
return res.json({ ok: true, goal, changed: false });
|
|
959
|
+
}
|
|
960
|
+
const updated = await db.transferGoalOwnership({ goalId: id, newOwnerId });
|
|
961
|
+
if (!updated) {
|
|
962
|
+
return res.fail('transfer_conflict', { status: 409, message: 'The goal changed underneath this request — re-read it and retry.' });
|
|
963
|
+
}
|
|
964
|
+
return res.json({ ok: true, goal: updated, changed: true });
|
|
965
|
+
} catch (err) {
|
|
966
|
+
console.error('[gds] POST /goals/:id/transfer', err);
|
|
967
|
+
return res.fail('transfer_failed', { status: 500, message: 'internal error' });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// GET /goals/:id/conflicts — the server-side task-conflict view (task 1736).
|
|
972
|
+
// Replaces the hall Goals page's client-side exact-string approximation (the
|
|
973
|
+
// page feature-detects on 404): glob-aware touches[] intersection via the
|
|
974
|
+
// canonical path-match machinery, claim-aware severity, and cross-goal
|
|
975
|
+
// overlaps against other OPEN goals sharing scope_modules. Detection only —
|
|
976
|
+
// resolution is the POST below.
|
|
977
|
+
// rank: any-builder — a read; the same audience as GET /goals/:id.
|
|
978
|
+
router.get('/goals/:id/conflicts', auth.requireBuilder, async (req, res) => {
|
|
979
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
980
|
+
if (id === null) return;
|
|
981
|
+
try {
|
|
982
|
+
const goal = await db.getGoal(id);
|
|
983
|
+
if (!goal) return res.fail('goal_not_found', 404);
|
|
984
|
+
const goalTasks = await db.listTasks({ goalId: id });
|
|
985
|
+
// Sibling scope: other open goals sharing at least one scope module. A
|
|
986
|
+
// scopeless goal (the migration-160 defaults) shares territory with
|
|
987
|
+
// nobody by declaration — only its own tasks are compared.
|
|
988
|
+
const scope = goal.scope_modules || [];
|
|
989
|
+
let crossGoal = [];
|
|
990
|
+
if (scope.length) {
|
|
991
|
+
const openGoals = await db.listGoals({ status: 'open' });
|
|
992
|
+
const siblings = openGoals.filter((g) =>
|
|
993
|
+
String(g.id) !== String(goal.id)
|
|
994
|
+
&& (g.scope_modules || []).some((m) => scope.includes(m)));
|
|
995
|
+
crossGoal = await Promise.all(siblings.map(async (g) => ({
|
|
996
|
+
goal: g,
|
|
997
|
+
tasks: await db.listTasks({ goalId: g.id }),
|
|
998
|
+
})));
|
|
999
|
+
}
|
|
1000
|
+
const allIds = [...goalTasks, ...crossGoal.flatMap((c) => c.tasks)].map((t) => t.id);
|
|
1001
|
+
const activeClaims = await db.listActiveClaimsForTasks(allIds);
|
|
1002
|
+
const activeClaimTaskIds = new Set(activeClaims.map((c) => String(c.task_id)));
|
|
1003
|
+
const conflicts = goalConflicts.computeGoalConflicts({ goalTasks, activeClaimTaskIds, crossGoal });
|
|
1004
|
+
return res.json({
|
|
1005
|
+
ok: true,
|
|
1006
|
+
goal_id: Number(goal.id),
|
|
1007
|
+
conflicts,
|
|
1008
|
+
cross_goals_considered: crossGoal.map((c) => ({ id: Number(c.goal.id), title: c.goal.title })),
|
|
1009
|
+
});
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
console.error('[gds] GET /goals/:id/conflicts', err);
|
|
1012
|
+
return res.fail('conflicts_failed', { status: 500, message: 'internal error' });
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// POST /goals/:id/conflicts/resolve — the owner/manager resolution actions
|
|
1017
|
+
// for a detected conflict (task 1736; authority model ADR 0106):
|
|
1018
|
+
// sequence {first_task_id, then_task_id} — order the work: `then`
|
|
1019
|
+
// gains a dependency on `first` via the ADR 0015 edge CRUD
|
|
1020
|
+
// (cycle-checked), so the claim gate serializes them.
|
|
1021
|
+
// flag_release {task_id, note?} — soft-nudge the active claim holder to
|
|
1022
|
+
// wrap up/release; surfaces on their own /me claim reads.
|
|
1023
|
+
// rescope {task_id, touches[]} — rewrite the DECLARED blast radius at
|
|
1024
|
+
// planning time (ADR 0049 advisory semantics). Refused while
|
|
1025
|
+
// the task holds an active claim — mid-claim touches rewriting
|
|
1026
|
+
// is the drift-expand task 879 removed; flag_release instead.
|
|
1027
|
+
// All targets must belong to THIS goal (containment — the parent-attach
|
|
1028
|
+
// precedent: a manager can't reshape another goal's tasks through this route).
|
|
1029
|
+
// rank: any-builder (requireBuilder) + the in-handler (owner | manager |
|
|
1030
|
+
// Archon) gate (authorizeGoalManagement). Pinned in
|
|
1031
|
+
// route-rank-check.MODULE_EXPECTED_RANKS['lifecycle'].
|
|
1032
|
+
router.post('/goals/:id/conflicts/resolve', auth.requireBuilder, async (req, res) => {
|
|
1033
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1034
|
+
if (id === null) return;
|
|
1035
|
+
if (validateOrRespond(req, res, {
|
|
1036
|
+
action: { required: true, type: 'string' },
|
|
1037
|
+
note: { type: 'string', maxLength: LIMITS.NOTES },
|
|
1038
|
+
touches: { type: 'array', maxItems: 500, itemsType: 'string' },
|
|
1039
|
+
// Task-id operands for the sequence/dedup actions (number or numeric
|
|
1040
|
+
// string, resolved below) — declared so strict-mode accepts them.
|
|
1041
|
+
first_task_id: {},
|
|
1042
|
+
then_task_id: {},
|
|
1043
|
+
task_id: {},
|
|
1044
|
+
})) return;
|
|
1045
|
+
const action = req.body.action;
|
|
1046
|
+
if (!RESOLVE_ACTIONS.includes(action)) {
|
|
1047
|
+
return res.fail('bad_action', 400, { valid: RESOLVE_ACTIONS });
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const goal = await db.getGoal(id);
|
|
1051
|
+
const actorMembership = goal
|
|
1052
|
+
? await db.getGoalMember({ goalId: id, builderId: req.builder.id })
|
|
1053
|
+
: null;
|
|
1054
|
+
const decision = authorizeGoalManagement({
|
|
1055
|
+
goal, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
|
|
1056
|
+
});
|
|
1057
|
+
if (!decision.ok) {
|
|
1058
|
+
return res.status(decision.status).json(decision.body);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Containment: resolve a task id from the body and require it to live in
|
|
1062
|
+
// THIS goal. Accepts a number or a numeric string (ids round-trip through
|
|
1063
|
+
// JSON as strings — the /members precedent).
|
|
1064
|
+
const goalTask = async (raw, field) => {
|
|
1065
|
+
const tid = Number(raw);
|
|
1066
|
+
if (!Number.isInteger(tid) || tid <= 0) {
|
|
1067
|
+
return { err: { status: 400, body: { error: 'bad_task_id', field } } };
|
|
1068
|
+
}
|
|
1069
|
+
const task = await db.getTask(tid);
|
|
1070
|
+
if (!task) {
|
|
1071
|
+
return { err: { status: 404, body: { error: 'task_not_found', task_id: tid } } };
|
|
1072
|
+
}
|
|
1073
|
+
if (String(task.goal_id ?? '') !== String(goal.id)) {
|
|
1074
|
+
return { err: { status: 400, body: {
|
|
1075
|
+
error: 'task_outside_goal', task_id: tid,
|
|
1076
|
+
message: 'Resolution actions apply only to this goal\'s own tasks.',
|
|
1077
|
+
} } };
|
|
1078
|
+
}
|
|
1079
|
+
return { task };
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
if (action === 'sequence') {
|
|
1083
|
+
const first = await goalTask(req.body.first_task_id, 'first_task_id');
|
|
1084
|
+
if (first.err) return res.status(first.err.status).json(first.err.body);
|
|
1085
|
+
const then = await goalTask(req.body.then_task_id, 'then_task_id');
|
|
1086
|
+
if (then.err) return res.status(then.err.status).json(then.err.body);
|
|
1087
|
+
try {
|
|
1088
|
+
await db.addTaskDependency(then.task.id, first.task.id);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
if (err && err.code === 'SELF_DEP') return res.fail('self_dep', 400);
|
|
1091
|
+
if (err && err.code === 'CYCLE') return res.fail('cycle', 409, { path: err.path });
|
|
1092
|
+
if (err && err.code === 'TASK_NOT_FOUND') return res.fail('task_not_found', 404);
|
|
1093
|
+
throw err;
|
|
1094
|
+
}
|
|
1095
|
+
const dependencies = await db.getTaskDependencies(then.task.id);
|
|
1096
|
+
return res.json({
|
|
1097
|
+
ok: true, action,
|
|
1098
|
+
first_task_id: Number(first.task.id), then_task_id: Number(then.task.id),
|
|
1099
|
+
dependencies,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (action === 'flag_release') {
|
|
1104
|
+
const t = await goalTask(req.body.task_id, 'task_id');
|
|
1105
|
+
if (t.err) return res.status(t.err.status).json(t.err.body);
|
|
1106
|
+
const flagged = await db.requestClaimRelease({
|
|
1107
|
+
taskId: t.task.id,
|
|
1108
|
+
requestedBy: req.builder.id,
|
|
1109
|
+
note: (req.body.note || '').trim() || null,
|
|
1110
|
+
});
|
|
1111
|
+
if (!flagged) {
|
|
1112
|
+
return res.fail('no_active_claim', { status: 409, message: 'That task holds no active claim to flag.' });
|
|
1113
|
+
}
|
|
1114
|
+
return res.json({ ok: true, action, claim: flagged });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// action === 'rescope'
|
|
1118
|
+
const t = await goalTask(req.body.task_id, 'task_id');
|
|
1119
|
+
if (t.err) return res.status(t.err.status).json(t.err.body);
|
|
1120
|
+
const touches = req.body.touches;
|
|
1121
|
+
if (!Array.isArray(touches) || touches.length === 0) {
|
|
1122
|
+
return res.fail('touches_required', { status: 400, message: 'rescope needs a non-empty touches[] — the declared blast radius.' });
|
|
1123
|
+
}
|
|
1124
|
+
// The SAME scope wall the R60 create-time gate runs (goal-scope-check
|
|
1125
|
+
// VERBATIM): rescope is a second authoring surface for the touches field,
|
|
1126
|
+
// so without this a member could author an in-scope task and then rewrite
|
|
1127
|
+
// it out of the goal's territory (or at protected paths) past the wall.
|
|
1128
|
+
// Bounds everyone (scope-subset), floors sub-Metic off protected paths.
|
|
1129
|
+
const wall = goalScopeCheck.check(goal.scope_modules, touches, req.builder.rank);
|
|
1130
|
+
if (!wall.ok) {
|
|
1131
|
+
return res.fail(wall.code, 403, { reason: wall.reason, details: wall.details });
|
|
1132
|
+
}
|
|
1133
|
+
const active = await db.listActiveClaimsForTasks([t.task.id]);
|
|
1134
|
+
if (active.length) {
|
|
1135
|
+
return res.fail('task_claimed', { status: 409, message: 'That task has an active claim — ask the holder to wrap up (flag_release) instead of rescoping under them.' });
|
|
1136
|
+
}
|
|
1137
|
+
const updated = await db.rescopeTaskTouches({
|
|
1138
|
+
taskId: t.task.id,
|
|
1139
|
+
touches,
|
|
1140
|
+
securitySensitive: t.task.security_sensitive === true,
|
|
1141
|
+
currentRequiresRank: t.task.requires_rank ?? 'xenos',
|
|
1142
|
+
});
|
|
1143
|
+
if (!updated) {
|
|
1144
|
+
return res.fail('rescope_conflict', { status: 409, message: 'The task changed underneath this request — re-read and retry.' });
|
|
1145
|
+
}
|
|
1146
|
+
return res.json({
|
|
1147
|
+
ok: true, action,
|
|
1148
|
+
task_id: Number(updated.id),
|
|
1149
|
+
touches: updated.touches,
|
|
1150
|
+
requires_rank: updated.requires_rank,
|
|
1151
|
+
});
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
console.error('[gds] POST /goals/:id/conflicts/resolve', err);
|
|
1154
|
+
return res.fail('conflict_resolve_failed', { status: 500, message: 'internal error' });
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// POST /goals/:id/archive — retire a goal (open|achieved → archived). Archived
|
|
1159
|
+
// goals are re-openable (below). This is the lifecycle disposition the /goal-review
|
|
1160
|
+
// skill reaches for once a goal is achieved (ADR 0086 §6 / BV1.R64).
|
|
1161
|
+
// rank: metic+archon — a goal member (Metic+) or an Archon dispositions the goal.
|
|
1162
|
+
router.post('/goals/:id/archive', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
|
|
1163
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1164
|
+
if (id === null) return;
|
|
1165
|
+
try {
|
|
1166
|
+
const goal = await db.getGoal(id);
|
|
1167
|
+
if (!goal) return res.fail('goal_not_found', 404);
|
|
1168
|
+
if (!(await isMemberOrArchon(req.builder, id))) {
|
|
1169
|
+
return res.fail('not_a_member', { status: 403, message: 'Join the goal (or be an Archon) to disposition it.' });
|
|
1170
|
+
}
|
|
1171
|
+
if (goal.status === 'archived') {
|
|
1172
|
+
return res.fail('already_archived', 409);
|
|
1173
|
+
}
|
|
1174
|
+
const updated = await db.setGoalStatus({ goalId: id, status: 'archived' });
|
|
1175
|
+
return res.json({ ok: true, goal: updated });
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
console.error('[gds] POST /goals/:id/archive', err);
|
|
1178
|
+
return res.fail('archive_failed', { status: 500, message: 'internal error' });
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// POST /goals/:id/reopen — bring an archived goal back to open (ADR 0086 §6 /
|
|
1183
|
+
// BV1.R64). Only archived → open; an already open/achieved goal is a 409.
|
|
1184
|
+
// rank: metic+archon — same gate as archive.
|
|
1185
|
+
router.post('/goals/:id/reopen', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
|
|
1186
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1187
|
+
if (id === null) return;
|
|
1188
|
+
try {
|
|
1189
|
+
const goal = await db.getGoal(id);
|
|
1190
|
+
if (!goal) return res.fail('goal_not_found', 404);
|
|
1191
|
+
if (!(await isMemberOrArchon(req.builder, id))) {
|
|
1192
|
+
return res.fail('not_a_member', { status: 403, message: 'Join the goal (or be an Archon) to reopen it.' });
|
|
1193
|
+
}
|
|
1194
|
+
if (goal.status !== 'archived') {
|
|
1195
|
+
return res.fail('not_archived', 409, { status: goal.status });
|
|
1196
|
+
}
|
|
1197
|
+
const updated = await db.setGoalStatus({ goalId: id, status: 'open' });
|
|
1198
|
+
return res.json({ ok: true, goal: updated });
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
console.error('[gds] POST /goals/:id/reopen', err);
|
|
1201
|
+
return res.fail('reopen_failed', { status: 500, message: 'internal error' });
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// POST /goals/:id/scope — "ask to widen": add module(s) to a goal's scope_modules
|
|
1206
|
+
// (ADR 0086 §4 / BV1.R64). Scope is NEVER self-granted, so this is the gated
|
|
1207
|
+
// approve-and-apply: requireRank('metic','archon') + the caller must be a member
|
|
1208
|
+
// (or Archon), and a PROTECTED module can only be added by an Archon (mirrors the
|
|
1209
|
+
// POST /goals create gate). A sub-Metic member therefore cannot widen at all — they
|
|
1210
|
+
// ask a Metic+/Archon out-of-band, exactly as joining a protected goal works (R57).
|
|
1211
|
+
// The goal must be open. Body: { add: ["module-key", ...] }; idempotent (set-union).
|
|
1212
|
+
// rank: metic+archon — widens the module wall; pinned in route-rank-check.EXPECTED_RANKS.
|
|
1213
|
+
router.post('/goals/:id/scope', auth.requireBuilder, auth.requireRank('metic', 'archon'), async (req, res) => {
|
|
1214
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1215
|
+
if (id === null) return;
|
|
1216
|
+
if (validateOrRespond(req, res, {
|
|
1217
|
+
add: { required: true, type: 'array', maxItems: 64, itemsType: 'string' },
|
|
1218
|
+
})) return;
|
|
1219
|
+
const add = [...new Set(req.body.add)];
|
|
1220
|
+
if (add.length === 0) {
|
|
1221
|
+
return res.fail('empty_add', { status: 400, message: 'add must name at least one module.' });
|
|
1222
|
+
}
|
|
1223
|
+
const unknown = add.filter((k) => !isModuleKey(k));
|
|
1224
|
+
if (unknown.length) {
|
|
1225
|
+
return res.fail('unknown_module', 400, { unknown, valid: moduleKeys() });
|
|
1226
|
+
}
|
|
1227
|
+
// The wall: a protected module can only be added by an Archon — the scope is set
|
|
1228
|
+
// by a trusted rank, never self-granted (ADR 0086 §4).
|
|
1229
|
+
if (scopeIncludesProtected(add) && req.builder.rank !== 'archon') {
|
|
1230
|
+
return res.fail('archon_required_for_protected_scope', { status: 403, message: 'Adding a protected module to a goal scope can only be done by an Archon.' });
|
|
1231
|
+
}
|
|
1232
|
+
try {
|
|
1233
|
+
const goal = await db.getGoal(id);
|
|
1234
|
+
if (!goal) return res.fail('goal_not_found', 404);
|
|
1235
|
+
if (goal.status !== 'open') {
|
|
1236
|
+
return res.fail('goal_not_open', 409, { status: goal.status });
|
|
1237
|
+
}
|
|
1238
|
+
if (!(await isMemberOrArchon(req.builder, id))) {
|
|
1239
|
+
return res.fail('not_a_member', { status: 403, message: 'Join the goal (or be an Archon) to widen its scope.' });
|
|
1240
|
+
}
|
|
1241
|
+
const updated = await db.addGoalScopeModules({ goalId: id, modules: add });
|
|
1242
|
+
return res.json({ ok: true, goal: updated });
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
console.error('[gds] POST /goals/:id/scope', err);
|
|
1245
|
+
return res.fail('scope_widen_failed', { status: 500, message: 'internal error' });
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// POST /goals/:id/tasks — bounded task authoring within a goal's scope
|
|
1250
|
+
// (BV1.R60 / ADR 0086 §3). THE ENFORCEMENT KEYSTONE of the goal-scoped work
|
|
1251
|
+
// hierarchy: the one route that lets a member (even a sub-Metic one) author
|
|
1252
|
+
// their OWN work — bounded by the goal's module wall, and never able to mint
|
|
1253
|
+
// work they couldn't otherwise be trusted to claim.
|
|
1254
|
+
//
|
|
1255
|
+
// requireBuilder + the in-handler, ORDERED, FAIL-CLOSED gate
|
|
1256
|
+
// (authorizeGoalTaskCreate above). On pass, db.createTask stamps goal_id with
|
|
1257
|
+
// requires_rank AUTO-DERIVED (ADR 0084) — NEVER taken from client input: a
|
|
1258
|
+
// protected/sensitive task auto-floors to Metic, so a sub-Metic author simply
|
|
1259
|
+
// cannot claim it (there is no self-granted escape). The task's version is the
|
|
1260
|
+
// GOAL's version (never client-supplied) and it is born 'ready' so the author
|
|
1261
|
+
// can immediately claim + work it — the maneuverability grant (the scope wall +
|
|
1262
|
+
// auto-rank-floor already bound WHAT they may create). One-level sub-tasks
|
|
1263
|
+
// attach via hierarchy.attachParent (the depth-cap + version-match authority),
|
|
1264
|
+
// constrained to the SAME goal.
|
|
1265
|
+
//
|
|
1266
|
+
// rank: any-builder (requireBuilder) + goal membership + the scope wall. Pinned
|
|
1267
|
+
// in route-rank-check.EXPECTED_RANKS['lifecycle'] by BV1.R61.
|
|
1268
|
+
router.post('/goals/:id/tasks', auth.requireBuilder, async (req, res) => {
|
|
1269
|
+
const id = parseId(req, res, { code: 'bad_goal_id' });
|
|
1270
|
+
if (id === null) return;
|
|
1271
|
+
if (validateOrRespond(req, res, {
|
|
1272
|
+
title: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
|
|
1273
|
+
description: { type: 'string', maxLength: LIMITS.DESCRIPTION },
|
|
1274
|
+
// touches[] is REQUIRED — it is the blast radius the scope wall checks. A
|
|
1275
|
+
// task with no declared touches can't be bounded to the goal, so the pure
|
|
1276
|
+
// check() denies it too (empty_touches); requiring it gives a clean 400 up
|
|
1277
|
+
// front rather than a 403.
|
|
1278
|
+
touches: { required: true, type: 'array', maxItems: 500, itemsType: 'string' },
|
|
1279
|
+
est_minutes: { type: 'integer', min: 0, max: 100000 },
|
|
1280
|
+
priority: { type: 'integer', min: TASK_FIELD_BOUNDS.priority.min, max: TASK_FIELD_BOUNDS.priority.max },
|
|
1281
|
+
kind: { type: 'string', maxLength: 64 },
|
|
1282
|
+
discipline: { type: 'string', maxLength: 64 },
|
|
1283
|
+
value_summary: { type: 'string', maxLength: LIMITS.VALUE_SUMMARY },
|
|
1284
|
+
parent_task_id: { type: 'integer', min: 1 },
|
|
1285
|
+
// Deliberately NOT read from the client (validateOrRespond ignores unknown
|
|
1286
|
+
// fields, so any of these a client smuggles in are simply never used):
|
|
1287
|
+
// • version_id — derived from the goal (a member can't author cross-version);
|
|
1288
|
+
// • requires_rank — AUTO-derived (ADR 0084); this is the self-raised-rank
|
|
1289
|
+
// escape the whole model exists to prevent, so the route never forwards it;
|
|
1290
|
+
// • status — server-set to 'ready';
|
|
1291
|
+
// • security_sensitive / module_key — out of R60 scope.
|
|
1292
|
+
})) return;
|
|
1293
|
+
|
|
1294
|
+
const body = req.body || {};
|
|
1295
|
+
const touches = Array.isArray(body.touches) ? [...body.touches] : [];
|
|
1296
|
+
|
|
1297
|
+
try {
|
|
1298
|
+
// Fetch the inputs the ordered gate consults. version is the GOAL's version
|
|
1299
|
+
// (never client input). The couple of extra indexed reads on a reject path
|
|
1300
|
+
// are immaterial and keep the whole decision in ONE testable place.
|
|
1301
|
+
const goal = await db.getGoal(id);
|
|
1302
|
+
const isMember = goal ? await isMemberOrArchon(req.builder, id) : false;
|
|
1303
|
+
const version = goal ? await db.getVersion(goal.version_id) : null;
|
|
1304
|
+
|
|
1305
|
+
const decision = authorizeGoalTaskCreate({
|
|
1306
|
+
goal, version, isMember, builderRank: req.builder.rank, touches,
|
|
1307
|
+
});
|
|
1308
|
+
if (!decision.ok) {
|
|
1309
|
+
return res.status(decision.status).json(decision.body);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// One-level sub-task (optional). Validate the parent BEFORE creating
|
|
1313
|
+
// anything, so a bad parent is a clean 4xx with no orphan task: it must
|
|
1314
|
+
// exist, live in THIS goal (containment — a member can't graft work onto
|
|
1315
|
+
// another goal's tree), and not itself be a sub-task (the depth cap; a fresh
|
|
1316
|
+
// task can have no children, so that half is trivially met).
|
|
1317
|
+
const parentTaskId = body.parent_task_id ?? null;
|
|
1318
|
+
if (parentTaskId !== null) {
|
|
1319
|
+
const parent = await db.getTask(parentTaskId);
|
|
1320
|
+
if (!parent) {
|
|
1321
|
+
return res.fail('parent_not_found', { status: 404, message: `No task #${parentTaskId} to attach under.` });
|
|
1322
|
+
}
|
|
1323
|
+
if (String(parent.goal_id ?? '') !== String(goal.id)) {
|
|
1324
|
+
return res.fail('parent_outside_goal', { status: 400, message: `Parent task #${parentTaskId} is not part of this goal — a sub-task attaches to a parent within the same goal.` });
|
|
1325
|
+
}
|
|
1326
|
+
if (parent.parent_task_id !== null) {
|
|
1327
|
+
return res.fail('parent_has_parent', { status: 409, message: `Task #${parentTaskId} is already a sub-task — the hierarchy is one level deep (ADR 0086 §2).` });
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Resolve kind/discipline like POST /tasks — a goal-authored task is a
|
|
1332
|
+
// first-class task, so classify it for routing/reward. Caller wins if valid;
|
|
1333
|
+
// otherwise the heuristic (confidence < 0.5 → 'unclassified' for triage).
|
|
1334
|
+
let kind = body.kind;
|
|
1335
|
+
if (kind && !VALID_KINDS.has(kind)) {
|
|
1336
|
+
return res.fail('bad_kind', 400, { valid: Array.from(VALID_KINDS) });
|
|
1337
|
+
}
|
|
1338
|
+
if (!kind || kind === 'unclassified') {
|
|
1339
|
+
const result = classifyTaskKind(body.title, body.description || '');
|
|
1340
|
+
kind = result.confidence >= 0.5 ? result.kind : 'unclassified';
|
|
1341
|
+
}
|
|
1342
|
+
const discipline = body.discipline ?? 'unclassified';
|
|
1343
|
+
if (!VALID_DISCIPLINES.has(discipline)) {
|
|
1344
|
+
return res.fail('bad_discipline', 400, { valid: Array.from(VALID_DISCIPLINES) });
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Create. version = the goal's (not client-supplied); status 'ready' so the
|
|
1348
|
+
// author can claim it immediately; requiresRank OMITTED → auto-derived
|
|
1349
|
+
// (ADR 0084), so a protected/sensitive task floors to Metic and a sub-Metic
|
|
1350
|
+
// author cannot claim it. goal_id is stamped. credits_reward omitted →
|
|
1351
|
+
// auto-assigned at creation (ADR 0096).
|
|
1352
|
+
const task = await db.createTask({
|
|
1353
|
+
versionId: goal.version_id,
|
|
1354
|
+
title: body.title,
|
|
1355
|
+
description: body.description || '',
|
|
1356
|
+
status: 'ready',
|
|
1357
|
+
touches,
|
|
1358
|
+
estMinutes: body.est_minutes ?? null,
|
|
1359
|
+
priority: body.priority ?? null,
|
|
1360
|
+
valueSummary: body.value_summary ?? null,
|
|
1361
|
+
kind,
|
|
1362
|
+
discipline,
|
|
1363
|
+
source: 'goal-authored',
|
|
1364
|
+
goalId: goal.id,
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
// Attach the sub-task parent (validated above; attachParent re-checks the
|
|
1368
|
+
// depth cap + version match atomically under row locks — the authority). A
|
|
1369
|
+
// failure here is a rare race (parent gained a parent / was deleted between
|
|
1370
|
+
// the pre-check and the attach): the task exists as a top-level goal task, so
|
|
1371
|
+
// surface the attach failure honestly rather than pretend success.
|
|
1372
|
+
let attachedParentId = null;
|
|
1373
|
+
if (parentTaskId !== null) {
|
|
1374
|
+
try {
|
|
1375
|
+
const updated = await hierarchy.attachParent(task.id, parentTaskId);
|
|
1376
|
+
attachedParentId = updated.parent_task_id != null ? Number(updated.parent_task_id) : null;
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
console.error('[gds] POST /goals/:id/tasks attachParent', err);
|
|
1379
|
+
return res.fail('parent_attach_failed', { status: 409, message: `Task #${task.id} was created under the goal, but attaching it under parent #${parentTaskId} failed (${(err && err.code) || 'unknown'}).`, details: { code: err && err.code, task } });
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return res.status(201).json({
|
|
1384
|
+
ok: true,
|
|
1385
|
+
task,
|
|
1386
|
+
goal_id: goal.id,
|
|
1387
|
+
parent_task_id: attachedParentId,
|
|
1388
|
+
credits_reward: task.credits_reward,
|
|
1389
|
+
requires_rank: task.requires_rank,
|
|
1390
|
+
});
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
console.error('[gds] POST /goals/:id/tasks', err);
|
|
1393
|
+
return res.fail('goal_task_create_failed', { status: 500, message: 'internal error' });
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
return router;
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
// BV1.R60 / ADR 0086 §3: the pure ordered gate for POST /goals/:id/tasks —
|
|
1401
|
+
// exported for tests/goal_routes.mjs (DB-free) and the BV1.R61 abuse matrix, and
|
|
1402
|
+
// for reuse. The shouldRefuseSessionClaim (claims.js) precedent.
|
|
1403
|
+
module.exports.authorizeGoalTaskCreate = authorizeGoalTaskCreate;
|
|
1404
|
+
// task 1735: the pure ordered gates for PATCH /goals/:id/members/:builderId and
|
|
1405
|
+
// POST /goals/:id/transfer — exported for tests/goal_member_roles.mjs (DB-free),
|
|
1406
|
+
// same precedent.
|
|
1407
|
+
module.exports.authorizeMemberRoleChange = authorizeMemberRoleChange;
|
|
1408
|
+
module.exports.authorizeOwnershipTransfer = authorizeOwnershipTransfer;
|
|
1409
|
+
// task 1736: the shared management prefix gate (conflict resolution) —
|
|
1410
|
+
// exported for tests/goal_conflicts.mjs (DB-free), same precedent.
|
|
1411
|
+
module.exports.authorizeGoalManagement = authorizeGoalManagement;
|
|
1412
|
+
module.exports.RESOLVE_ACTIONS = RESOLVE_ACTIONS;
|
|
1413
|
+
// task 1952 / ADR 0112: the private-goal invite/join-request pure gates +
|
|
1414
|
+
// the shared protected-scope admission rule — exported for
|
|
1415
|
+
// tests/goal_membership_requests.mjs (DB-free), same precedent.
|
|
1416
|
+
module.exports.protectedScopeAdmissionBlock = protectedScopeAdmissionBlock;
|
|
1417
|
+
module.exports.authorizeCreateJoinRequest = authorizeCreateJoinRequest;
|
|
1418
|
+
module.exports.authorizeInvitationResponse = authorizeInvitationResponse;
|
|
1419
|
+
module.exports.authorizeJoinRequestResponse = authorizeJoinRequestResponse;
|