@bongos/core 1.23.22 → 1.24.1
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 +902 -407
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +90 -0
- package/clients/bongos-client/index.cjs +90 -0
- package/clients/bongos-client/index.d.ts +149 -4
- package/clients/bongos-client/index.mjs +90 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +5 -1
- package/docs/api/openapi.json +8555 -5876
- package/docs/api-reference.md +63 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +420 -337
- package/docs/copy-registry.json +1321 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +5 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +55 -6
- package/docs/page-readings.json +1185 -1011
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +34 -0
- package/modules/government/docket.js +36 -0
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +22 -0
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +156 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +24 -0
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +220 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -852,8 +852,6 @@ function loggers(deps = {}) {
|
|
|
852
852
|
return { logFn: deps.log || console.log, errFn: deps.error || console.error };
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
-
// A single crash handler for the tick's fire-and-forget sweeps — same reason.
|
|
856
|
-
const crashHandler = (name) => (e) => loggers().errFn(`[publish-reconciler] ${name} crashed: ${e && e.message}`);
|
|
857
855
|
|
|
858
856
|
async function runCostSweep(deps = {}) {
|
|
859
857
|
const { logFn, errFn } = loggers(deps);
|
|
@@ -958,19 +956,23 @@ async function runPromotionSweep(deps = {}) {
|
|
|
958
956
|
|
|
959
957
|
// The craft-achievement backfill (task 1004438, WA6.RP08): the one badge a
|
|
960
958
|
// migration cannot backfill, "A Whole Surface", needs the page inventory, which is
|
|
961
|
-
// a file the database never sees.
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
|
|
959
|
+
// a file the database never sees. Runs HOURLY (task 1004965) through the reward
|
|
960
|
+
// port (economy is optional; no port, no sweep), and INCREMENTALLY: the first run
|
|
961
|
+
// of a process scans every craft-paid builder and hands back a watermark (the
|
|
962
|
+
// highest credit_log id it covered); every later run asks only about builders
|
|
963
|
+
// paid since that id, so a caught-up backfill is one indexed read that finds
|
|
964
|
+
// nothing. Pure DB and idempotent, so a restart that rescans unlocks nothing
|
|
965
|
+
// twice; a failure keeps the watermark (the next run retries the same span) and
|
|
966
|
+
// never touches the other sweeps.
|
|
967
|
+
let craftWatermark = 0;
|
|
966
968
|
async function runCraftAchievementBackfill(deps = {}) {
|
|
967
969
|
const { logFn, errFn } = loggers(deps);
|
|
968
|
-
if (craftBackfillDone && !deps.forceCraftBackfill) return { skipped: 'already_ran' };
|
|
969
|
-
craftBackfillDone = true;
|
|
970
970
|
const reward = deps.reward || seams.resolveOptional('reward');
|
|
971
971
|
if (!reward || typeof reward.backfillCraftAchievements !== 'function') return { skipped: 'unsupported' };
|
|
972
972
|
try {
|
|
973
|
-
const out = await reward.backfillCraftAchievements({});
|
|
973
|
+
const out = await reward.backfillCraftAchievements({ sinceId: craftWatermark });
|
|
974
|
+
const next = out ? Number(out.watermark) : NaN;
|
|
975
|
+
if (Number.isFinite(next) && next > craftWatermark) craftWatermark = next;
|
|
974
976
|
if (out && out.unlocked) logFn(`[publish-reconciler] craft achievements: ${out.unlocked} unlocked across ${out.checked} builder(s)`);
|
|
975
977
|
return out;
|
|
976
978
|
} catch (e) {
|
|
@@ -979,6 +981,57 @@ async function runCraftAchievementBackfill(deps = {}) {
|
|
|
979
981
|
}
|
|
980
982
|
}
|
|
981
983
|
|
|
984
|
+
// The sweeps one tick fires. Each is INDEPENDENT on purpose: the cost (task 1366)
|
|
985
|
+
// and criterion (task 1002069) sweeps are pure DB — no push credential — and a
|
|
986
|
+
// failure in either must never touch the confirmed→shipped work.
|
|
987
|
+
//
|
|
988
|
+
// CADENCE PER SWEEP (task 1004965): an entry's third element is its minimum gap
|
|
989
|
+
// between runs; none means every tick (INTERVAL_MS). The publish, cost and
|
|
990
|
+
// criterion sweeps keep the tick — a strand, an unpriced session and a newly
|
|
991
|
+
// satisfiable criterion are all things someone is waiting on. The promotion sweep
|
|
992
|
+
// and the craft backfill run HOURLY: the ship-time triggers already promote and
|
|
993
|
+
// unlock on the event, so these are backlog drains, and a drain an hour late
|
|
994
|
+
// costs nobody anything while running it every 5 minutes cost every instance
|
|
995
|
+
// ~288 scans a day.
|
|
996
|
+
const HOURLY_MS = 60 * 60_000;
|
|
997
|
+
const SWEEPS = [
|
|
998
|
+
['sweep', runSweep],
|
|
999
|
+
['cost sweep', runCostSweep],
|
|
1000
|
+
['criterion sweep', runCriterionSweep],
|
|
1001
|
+
['promotion sweep', runPromotionSweep, HOURLY_MS],
|
|
1002
|
+
['craft achievement backfill', runCraftAchievementBackfill, HOURLY_MS],
|
|
1003
|
+
];
|
|
1004
|
+
|
|
1005
|
+
// buildTick — one interval round, with a per-sweep overlap guard (task 1004924).
|
|
1006
|
+
// A sweep whose previous run has not settled is SKIPPED this tick, so a slow DB
|
|
1007
|
+
// or GitHub call can never stack rounds and hold several pool connections at
|
|
1008
|
+
// once. The flag is per sweep, so one slow sweep never starves the others, and
|
|
1009
|
+
// it clears whether the run resolves or rejects. `sweeps` is the test seam.
|
|
1010
|
+
//
|
|
1011
|
+
// A sweep with a cadence runs on the first tick, then again once its gap has
|
|
1012
|
+
// passed. Half a tick of slack, so timer jitter (a tick landing a few ms early)
|
|
1013
|
+
// never pushes an hourly sweep a whole tick late. `now` is the clock seam.
|
|
1014
|
+
function buildTick(deps = {}, sweeps = SWEEPS, { now = Date.now, tickMs = INTERVAL_MS } = {}) {
|
|
1015
|
+
const inFlight = new Set();
|
|
1016
|
+
const lastRun = new Map();
|
|
1017
|
+
return () => {
|
|
1018
|
+
const t = now();
|
|
1019
|
+
for (const [name, fn, everyMs] of sweeps) {
|
|
1020
|
+
if (everyMs && lastRun.has(name) && t - lastRun.get(name) < everyMs - tickMs / 2) continue; // not due yet
|
|
1021
|
+
if (inFlight.has(name)) {
|
|
1022
|
+
loggers(deps).logFn(`[publish-reconciler] ${name} still running from the last tick — skipped`);
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
lastRun.set(name, t);
|
|
1026
|
+
inFlight.add(name);
|
|
1027
|
+
Promise.resolve()
|
|
1028
|
+
.then(() => fn(deps))
|
|
1029
|
+
.catch((e) => loggers(deps).errFn(`[publish-reconciler] ${name} crashed: ${e && e.message}`))
|
|
1030
|
+
.finally(() => inFlight.delete(name));
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
982
1035
|
// Start the periodic sweep. Idempotent (a second call is a no-op). Kill-switched
|
|
983
1036
|
// by PUBLISH_RECONCILER_DISABLED=1 (purely additive bookkeeping, so it's ON by
|
|
984
1037
|
// default whenever a push credential is configured; the route's getPublishStatus
|
|
@@ -991,16 +1044,7 @@ function start(deps = {}) {
|
|
|
991
1044
|
}
|
|
992
1045
|
if (intervalTimer) return; // already started
|
|
993
1046
|
|
|
994
|
-
|
|
995
|
-
// (task 1002069) sweeps are pure DB — no push credential — and a failure in
|
|
996
|
-
// either must never touch the confirmed→shipped work.
|
|
997
|
-
const tick = () => {
|
|
998
|
-
runSweep(deps).catch(crashHandler('sweep'));
|
|
999
|
-
runCostSweep(deps).catch(crashHandler('cost sweep'));
|
|
1000
|
-
runCriterionSweep(deps).catch(crashHandler('criterion sweep'));
|
|
1001
|
-
runPromotionSweep(deps).catch(crashHandler('promotion sweep'));
|
|
1002
|
-
runCraftAchievementBackfill(deps).catch(crashHandler('craft achievement backfill'));
|
|
1003
|
-
};
|
|
1047
|
+
const tick = buildTick(deps);
|
|
1004
1048
|
|
|
1005
1049
|
firstTimer = setTimeout(tick, FIRST_SWEEP_DELAY_MS);
|
|
1006
1050
|
if (firstTimer.unref) firstTimer.unref();
|
|
@@ -1039,7 +1083,10 @@ module.exports = {
|
|
|
1039
1083
|
runPromotionSweep,
|
|
1040
1084
|
runCriterionSweep,
|
|
1041
1085
|
runCraftAchievementBackfill,
|
|
1086
|
+
buildTick,
|
|
1042
1087
|
couldBeStaleGenerated,
|
|
1043
1088
|
describeTarget,
|
|
1044
1089
|
INTERVAL_MS,
|
|
1090
|
+
HOURLY_MS,
|
|
1091
|
+
SWEEPS,
|
|
1045
1092
|
};
|
|
@@ -305,7 +305,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
305
305
|
enforceOverlap: enforceTouches === true,
|
|
306
306
|
autoPickup: autoPickup === true,
|
|
307
307
|
});
|
|
308
|
-
const { just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingOverlapWarning, ...claim } = claimResult;
|
|
308
|
+
const { just_unlocked: justUnlocked, overlap_warning: overlapWarning, sibling_overlap_warning: siblingOverlapWarning, page_notice: pageNotice, ...claim } = claimResult;
|
|
309
309
|
const task = await db.getTask(Number(taskId));
|
|
310
310
|
// task 1168: keep the evergreen newcomer-chore shelf full. If this claim
|
|
311
311
|
// took a chore instance, stamp a replacement NOW (before responding) so
|
|
@@ -339,7 +339,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
339
339
|
const p = rw && typeof rw.rewardConfig === 'function' ? rw.rewardConfig() : null;
|
|
340
340
|
if (p && typeof p.mode === 'string') rewardMode = { mode: p.mode, perTaskCredit: p.perTaskCredit !== false };
|
|
341
341
|
} catch (_) { /* display-only: a claim must never fail over this */ }
|
|
342
|
-
res.status(201).json({ claim, task, just_unlocked: justUnlocked ?? [], overlap_warning: overlapWarning ?? null, sibling_overlap_warning: siblingOverlapWarning ?? null, reward: rewardMode });
|
|
342
|
+
res.status(201).json({ claim, task, just_unlocked: justUnlocked ?? [], overlap_warning: overlapWarning ?? null, sibling_overlap_warning: siblingOverlapWarning ?? null, page_notice: pageNotice ?? [], reward: rewardMode });
|
|
343
343
|
} catch (err) {
|
|
344
344
|
if (err && err.code) {
|
|
345
345
|
const status =
|
|
@@ -388,6 +388,9 @@ module.exports = function buildClaimsRouter() {
|
|
|
388
388
|
// block, not an authorization refusal). The refusal object already
|
|
389
389
|
// carries `message` + `hint`, so no extra hint-building step needed.
|
|
390
390
|
err.code === 'RELEVANCE_REVIEW_PENDING' ? 409 :
|
|
391
|
+
// task 1004950 / ADR 0395: the task needs hands on the live server and
|
|
392
|
+
// the claimer holds no server grant — an authorization refusal, like rank.
|
|
393
|
+
err.code === 'NEEDS_SERVER_ACCESS' ? 403 :
|
|
391
394
|
// ADR 0371 D5/D6: a marked auto-pickup claim the project's switch, monthly
|
|
392
395
|
// cap or per-round limit refuses. The round waits for a person.
|
|
393
396
|
err.code === 'AUTO_PICKUP_NOT_A_TWEAK' ? 409 :
|
|
@@ -1333,6 +1333,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
1333
1333
|
discipline: { type: 'string', maxLength: 64 },
|
|
1334
1334
|
value_summary: { type: 'string', maxLength: LIMITS.VALUE_SUMMARY },
|
|
1335
1335
|
parent_task_id: { type: 'integer', min: 1 },
|
|
1336
|
+
needs: {}, // task 1004950 / ADR 0395 — ['owner'|'server']; a need narrows who is shown the task, it grants nothing
|
|
1336
1337
|
// Deliberately NOT read from the client (validateOrRespond ignores unknown
|
|
1337
1338
|
// fields, so any of these a client smuggles in are simply never used):
|
|
1338
1339
|
// • version_id — derived from the goal (a member can't author cross-version);
|
|
@@ -1343,6 +1344,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
1343
1344
|
})) return;
|
|
1344
1345
|
|
|
1345
1346
|
const body = req.body || {};
|
|
1347
|
+
const goalNeeds = api.taskNeeds.normalizeNeeds(body.needs); if (!goalNeeds.ok) return require('./task-route-helpers.js').badNeeds(res, goalNeeds.bad);
|
|
1346
1348
|
const touches = Array.isArray(body.touches) ? [...body.touches] : [];
|
|
1347
1349
|
|
|
1348
1350
|
try {
|
|
@@ -1425,7 +1427,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
1425
1427
|
kind,
|
|
1426
1428
|
discipline,
|
|
1427
1429
|
source: 'goal-authored',
|
|
1428
|
-
goalId: goal.id,
|
|
1430
|
+
goalId: goal.id, needs: goalNeeds.needs,
|
|
1429
1431
|
// task 1003278 — attribution from the authenticated request (never the body).
|
|
1430
1432
|
createdBy: req.builder.id,
|
|
1431
1433
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// modules/lifecycle/routes/notes.js — notes on a task or a goal, read grouped by
|
|
2
|
+
// purpose (task 1004745, goal 1000095).
|
|
3
|
+
//
|
|
4
|
+
// Four routes: read the notes on one task or goal (already grouped), add one, re-file
|
|
5
|
+
// one under another purpose, and delete one. The grouping, the subject rule, who may
|
|
6
|
+
// see a private goal's notes and who may change a note are all notes.js's; this surface
|
|
7
|
+
// only gathers the request and renders the verdict.
|
|
8
|
+
//
|
|
9
|
+
// A note nobody may see answers 404, exactly as a missing one does, so the answer
|
|
10
|
+
// never tells a non-member that a private goal has notes. Every write here is recorded
|
|
11
|
+
// by the server's audit middleware, so a Metic's delete leaves a trail.
|
|
12
|
+
//
|
|
13
|
+
// Reaches core only through the doorway (ADR 0083).
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const express = require('express');
|
|
18
|
+
const api = require('../../../src/module-api');
|
|
19
|
+
const notes = require('../notes');
|
|
20
|
+
|
|
21
|
+
const { asyncHandler, parseId, validateOrRespond } = api;
|
|
22
|
+
|
|
23
|
+
module.exports = function buildNotesRouter() {
|
|
24
|
+
const router = express.Router();
|
|
25
|
+
|
|
26
|
+
// rank: any-builder — notes are shared working information, readable by anyone
|
|
27
|
+
// signed in; a private goal's notes are narrowed to its members by notes.canSee.
|
|
28
|
+
router.get('/notes', api.requireBuilder, asyncHandler('GET /notes', async (req, res) => {
|
|
29
|
+
const subject = notes.subjectOf(req.query || {});
|
|
30
|
+
if (!subject) return res.fail('invalid_subject', 400, { hint: 'give exactly one of task_id or goal_id' });
|
|
31
|
+
if (!(await notes.canSee(req.builder, subject))) return res.fail('not_found', 404);
|
|
32
|
+
const rows = await notes.listNotes(subject);
|
|
33
|
+
return res.json({ count: rows.length, groups: notes.groupNotes(rows), kinds: notes.KINDS });
|
|
34
|
+
}, { errorCode: 'notes_read_failed' }));
|
|
35
|
+
|
|
36
|
+
// rank: any-builder — adding information is not a privileged act; the author is
|
|
37
|
+
// the caller and is never taken from the body.
|
|
38
|
+
router.post('/notes', api.requireBuilder, asyncHandler('POST /notes', async (req, res) => {
|
|
39
|
+
if (validateOrRespond(req, res, {
|
|
40
|
+
body: { required: true, type: 'string', minLength: 1, maxLength: notes.MAX_BODY },
|
|
41
|
+
kind: { type: 'string', maxLength: 16 },
|
|
42
|
+
task_id: { type: 'integer', min: 1 },
|
|
43
|
+
goal_id: { type: 'integer', min: 1 },
|
|
44
|
+
})) return;
|
|
45
|
+
const body = req.body || {};
|
|
46
|
+
const v = notes.validateNote(body);
|
|
47
|
+
if (!v.ok) return res.fail('invalid_note', 400, { errors: v.errors, kinds: notes.KIND_KEYS });
|
|
48
|
+
const subject = notes.subjectOf(body);
|
|
49
|
+
if (!(await notes.canSee(req.builder, subject))) return res.fail('not_found', 404);
|
|
50
|
+
if ((await notes.countNotes(subject)) >= notes.MAX_NOTES_PER_SUBJECT) {
|
|
51
|
+
return res.fail('note_limit', 409, { limit: notes.MAX_NOTES_PER_SUBJECT });
|
|
52
|
+
}
|
|
53
|
+
const note = await notes.addNote({
|
|
54
|
+
taskId: subject.taskId ?? null,
|
|
55
|
+
goalId: subject.goalId ?? null,
|
|
56
|
+
authorId: req.builder.id,
|
|
57
|
+
body: v.body,
|
|
58
|
+
kind: v.kind,
|
|
59
|
+
kindSource: v.kindSource,
|
|
60
|
+
});
|
|
61
|
+
return res.status(201).json({ ok: true, note });
|
|
62
|
+
}, { errorCode: 'note_create_failed' }));
|
|
63
|
+
|
|
64
|
+
// rank: any-builder, narrowed in the handler — the author re-files their own note;
|
|
65
|
+
// a Metic or Archon may re-file anyone's (the grouping is a shared view).
|
|
66
|
+
router.patch('/notes/:id', api.requireBuilder, asyncHandler('PATCH /notes/:id', async (req, res) => {
|
|
67
|
+
if (validateOrRespond(req, res, { kind: { required: true, type: 'string', maxLength: 16 } })) return;
|
|
68
|
+
const id = parseId(req.params.id);
|
|
69
|
+
const kind = req.body.kind;
|
|
70
|
+
if (id == null) return res.fail('invalid_id', 400);
|
|
71
|
+
if (!notes.KIND_KEYS.includes(kind)) return res.fail('invalid_note', 400, { errors: [{ field: 'kind', reason: 'pick one of the listed groups' }], kinds: notes.KIND_KEYS });
|
|
72
|
+
const existing = await notes.getNote(id);
|
|
73
|
+
if (!existing || !(await notes.canSee(req.builder, notes.subjectOf(existing)))) return res.fail('not_found', 404);
|
|
74
|
+
if (!notes.canChange(req.builder, existing)) return res.fail('not_yours', 403);
|
|
75
|
+
const row = await notes.setKind(id, kind);
|
|
76
|
+
return res.json({ ok: true, note: row });
|
|
77
|
+
}, { errorCode: 'note_update_failed' }));
|
|
78
|
+
|
|
79
|
+
// rank: any-builder, narrowed in the handler exactly as the re-file above.
|
|
80
|
+
router.delete('/notes/:id', api.requireBuilder, asyncHandler('DELETE /notes/:id', async (req, res) => {
|
|
81
|
+
const id = parseId(req.params.id);
|
|
82
|
+
if (id == null) return res.fail('invalid_id', 400);
|
|
83
|
+
const existing = await notes.getNote(id);
|
|
84
|
+
if (!existing || !(await notes.canSee(req.builder, notes.subjectOf(existing)))) return res.fail('not_found', 404);
|
|
85
|
+
if (!notes.canChange(req.builder, existing)) return res.fail('not_yours', 403);
|
|
86
|
+
await notes.deleteNote(id);
|
|
87
|
+
return res.json({ ok: true });
|
|
88
|
+
}, { errorCode: 'note_delete_failed' }));
|
|
89
|
+
|
|
90
|
+
return router;
|
|
91
|
+
};
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// WHY THIS ROUTE EXISTS. Goal 1000072's thesis is that this project is legible to
|
|
8
8
|
// Claude and illegible to a human: the system knows everything and exposes almost
|
|
9
9
|
// none of it in a shape a person can hold. The five values here — the rot timer, the
|
|
10
|
-
// category set, how credits are calculated,
|
|
11
|
-
//
|
|
10
|
+
// category set, how credits are calculated, what credits are (and are not) next to
|
|
11
|
+
// equity, and whether a credit-weighted pool can pay out — were each answerable only
|
|
12
|
+
// by reading source.
|
|
12
13
|
// This route is the TRANSLATION LAYER, not a new capability: it invents no value and
|
|
13
14
|
// stores nothing of its own.
|
|
14
15
|
//
|
|
@@ -20,10 +21,11 @@
|
|
|
20
21
|
// and say why.
|
|
21
22
|
//
|
|
22
23
|
// AND THE ONE THAT KEEPS IT HONEST: a fundamental with no knob behind it is reported
|
|
23
|
-
// as ABSENT, never as a plausible zero. `upmarket_percent`
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// as ABSENT or GATED, never as a plausible zero. `upmarket_percent` (shown as the
|
|
25
|
+
// credit-weighted pool) is that row today: no such knob exists, and ADR 0385 gates
|
|
26
|
+
// the pay type behind counsel and a signed rule. Rendering it as "0%" would be a
|
|
27
|
+
// fabricated fact about the project's economics; saying "not available, and here is
|
|
28
|
+
// what gates it" is the legible answer.
|
|
27
29
|
//
|
|
28
30
|
// MODULE BOUNDARY: reaches core only through the published doorway
|
|
29
31
|
// `../../../src/module-api`; `../goal-category` is an intra-module sibling.
|
|
@@ -118,58 +120,64 @@ function creditRow() {
|
|
|
118
120
|
// (ADR 0146). A page that showed the estimate without that sentence would be telling
|
|
119
121
|
// a builder they will earn a number this instance has decided not to pay.
|
|
120
122
|
const estimate = policy.perTaskCredit
|
|
121
|
-
? 'A task’s own credit estimate is also
|
|
122
|
-
: 'A task’s credit estimate is an advertised guide only — this instance
|
|
123
|
+
? 'A task’s own credit estimate is also awarded, scaled by its kind.'
|
|
124
|
+
: 'A task’s credit estimate is an advertised guide only — this instance awards cost-plus credits and nothing else.';
|
|
125
|
+
// Wording per ADR 0387: the formula's INPUT is USD, so the credits are "sized by"
|
|
126
|
+
// the session's cost — never "worth" dollars, and never money.
|
|
123
127
|
return {
|
|
124
128
|
key: 'credit_calculation',
|
|
125
129
|
title: 'Credit calculation',
|
|
126
130
|
editable: false,
|
|
127
131
|
state: 'live',
|
|
128
132
|
value: { marginPct: policy.marginPct, usdPeg: policy.usdPeg, mode: policy.mode, perTaskCredit: policy.perTaskCredit },
|
|
129
|
-
sentence: `A shipped session
|
|
133
|
+
sentence: `A shipped session earns round(true cost in USD × ${mult}${pegNote}) credits, sized by the session’s cost in USD. Credits measure contribution; they are not money. ${estimate}`,
|
|
130
134
|
source: 'branding pack → `reward`',
|
|
131
135
|
reads: { file: 'config/branding.json → reward', adr: 'ADR 0054, ADR 0146' },
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
// Equity.
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
139
|
+
// Equity. Bongos issues none, and credits are NOT equity. The owner's 2026-08-25
|
|
140
|
+
// decision (no legal equity issued) stands; the older ADR 0054 framing that a credit
|
|
141
|
+
// IS a unit of ownership was superseded on 2026-10-29 (ADR 0385 decisions 2-3, ADR
|
|
142
|
+
// 0387): credits are a project-scoped contribution meter, and equity, if a project
|
|
143
|
+
// offers any, is that project's own grant, one signature each. The page says that
|
|
144
|
+
// plainly — an owner reading their own settings page must not come away believing
|
|
145
|
+
// credits are shares or that a cap table exists. The key stays `equity_calculation`
|
|
146
|
+
// so existing readers keep working.
|
|
141
147
|
function equityRow() {
|
|
142
148
|
return {
|
|
143
149
|
key: 'equity_calculation',
|
|
144
|
-
title: 'Equity
|
|
150
|
+
title: 'Equity',
|
|
145
151
|
editable: false,
|
|
146
152
|
state: 'not-yet-real',
|
|
147
|
-
sentence: '
|
|
148
|
-
+ '
|
|
149
|
-
+ '
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
sentence: 'Credits are a contribution meter, not ownership: they measure work on this project '
|
|
154
|
+
+ 'and are never money, never redeemable and never equity. No legal equity is issued here. '
|
|
155
|
+
+ 'Equity, if any, is granted by the project separately, one signed grant at a time — '
|
|
156
|
+
+ 'there is no cap table and no share class behind this row.',
|
|
157
|
+
source: 'owner decisions, 2026-08-25 and 2026-10-29',
|
|
158
|
+
reads: { file: 'docs/adr/0054-session-token-reward-cost-plus.md', adr: 'ADR 0054 (superseded in part), ADR 0385, ADR 0387' },
|
|
152
159
|
};
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
// The
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
162
|
+
// The credit-weighted pool: a project dividing a pool of its OWN money by credit
|
|
163
|
+
// share (ADR 0387 decision 3). The row key stays `upmarket_percent` (its first name,
|
|
164
|
+
// "share of upmarket revenue paid on credits") so existing readers keep working.
|
|
165
|
+
// NO SUCH KNOB EXISTS, and ADR 0385's pay-type gates hold this one shut until the
|
|
166
|
+
// wording rule is in force, the model's inputs are visible and a signed payout rule
|
|
167
|
+
// exists — with counsel's answer on held item 2 first. So the row reports GATED,
|
|
168
|
+
// never a rendered "0%", which would be a fabricated economic claim.
|
|
161
169
|
function upmarketRow() {
|
|
162
170
|
return {
|
|
163
171
|
key: 'upmarket_percent',
|
|
164
|
-
title: '
|
|
172
|
+
title: 'Credit-weighted pool',
|
|
165
173
|
editable: false,
|
|
166
|
-
state: '
|
|
167
|
-
sentence: '
|
|
168
|
-
+ '
|
|
169
|
-
+ '
|
|
170
|
-
+ '
|
|
171
|
-
source: '
|
|
172
|
-
reads: { file: '
|
|
174
|
+
state: 'gated',
|
|
175
|
+
sentence: 'Not available (gated). A project could one day divide a pool of its own money by '
|
|
176
|
+
+ 'credit share, but that pay type stays switched off until counsel clears it and the '
|
|
177
|
+
+ 'project signs a payout rule. Bongos sets no share and no rate, and no dollar value is '
|
|
178
|
+
+ 'ever read off a credit.',
|
|
179
|
+
source: 'gated by ADR 0385 (pay-type gates)',
|
|
180
|
+
reads: { file: 'docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md', adr: 'ADR 0385, ADR 0387' },
|
|
173
181
|
};
|
|
174
182
|
}
|
|
175
183
|
|
|
@@ -251,10 +251,31 @@ function badKind(res) {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
// task 1004950 / ADR 0395: a task's needs are a subset of TASK_NEEDS. Shared by
|
|
255
|
+
// POST /tasks, PATCH /tasks/:id and POST /goals/:id/tasks so all three refuse alike.
|
|
256
|
+
function badNeeds(res, bad) {
|
|
257
|
+
const valid = api.taskNeeds.TASK_NEEDS;
|
|
258
|
+
return res.fail('bad_needs', { status: 400, message: `needs must list only: ${valid.join(', ')} (unknown: ${bad.join(', ')}). Most tasks need neither.`, details: { valid } });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ?needs= on GET /tasks and GET /tasks/claimable (task 1004950): one need
|
|
262
|
+
// ('owner' | 'server') or 'none'. Shared so the two lists agree on the vocabulary.
|
|
263
|
+
const NEEDS_FILTERS = new Set([...api.taskNeeds.TASK_NEEDS, 'none']);
|
|
264
|
+
function parseNeedsFilter(raw) {
|
|
265
|
+
if (raw === undefined || raw === '') return undefined;
|
|
266
|
+
return NEEDS_FILTERS.has(raw) ? raw : { bad: 'bad_needs' };
|
|
267
|
+
}
|
|
268
|
+
function badNeedsFilter(res) {
|
|
269
|
+
return res.fail('bad_needs', { status: 400, message: `needs must be one of ${[...NEEDS_FILTERS].join(' | ')}.` });
|
|
270
|
+
}
|
|
271
|
+
|
|
254
272
|
module.exports = {
|
|
273
|
+
badNeedsFilter,
|
|
274
|
+
parseNeedsFilter,
|
|
255
275
|
MIGRATION_KEYWORD_RE,
|
|
256
276
|
VALID_FITS,
|
|
257
277
|
badKind,
|
|
278
|
+
badNeeds,
|
|
258
279
|
deHashRefs,
|
|
259
280
|
detectsMigration,
|
|
260
281
|
gateTaskOwnership,
|
|
@@ -26,7 +26,7 @@ const { buildDependencyAdvisory } = require('../dependency-advisory');
|
|
|
26
26
|
const { classifyTaskKind, VALID_KINDS, WRITABLE_DISCIPLINES, resolveWriteDiscipline, VOTABLE_TASK_STATUSES, TASK_FIELD_BOUNDS } = require('../task-classifier');
|
|
27
27
|
const { isModuleKey, moduleKeys } = api.moduleScopeMap;
|
|
28
28
|
const { validateOrRespond, LIMITS, parseId } = api;
|
|
29
|
-
const { badKind, detectsMigration } = require('./task-route-helpers.js');
|
|
29
|
+
const { badKind, badNeeds, detectsMigration } = require('./task-route-helpers.js');
|
|
30
30
|
const { describeCheckViolation } = require('../pg-constraint-fail.js');
|
|
31
31
|
const { goalNotOpenRefusal } = require('./goal-route-authz.js');
|
|
32
32
|
const { resolveUpkeepHome, isUpkeepFiling, UPKEEP_HINT } = require('../upkeep-home.js'); // task 1004597
|
|
@@ -62,6 +62,7 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
62
62
|
newcomer_friendly: { type: 'boolean' }, // V3.R15 #239 — Xenos-claimable flag
|
|
63
63
|
security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
|
|
64
64
|
requires_rank: { type: 'string', maxLength: 16 }, // task 1498 / ADR 0084 — per-task rank floor (auto-derived; explicit value can only raise it)
|
|
65
|
+
needs: {}, // task 1004950 / ADR 0395 — ['owner'|'server'] or a comma list; checked inline
|
|
65
66
|
auto_detect_touches: { type: 'boolean' },
|
|
66
67
|
// task 438: link the new task to done-when criteria at creation time. Each
|
|
67
68
|
// element is a criterion_id slug (required on a multi-goal version — task
|
|
@@ -169,6 +170,9 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
169
170
|
return res.fail('bad_requires_rank', { status: 400, message: `requires_rank must be one of ${db.LIVE_RANK_LADDER.join(' | ')}. The floor auto-derives from touches + security_sensitive; an explicit value can only raise it.`, details: { valid: db.LIVE_RANK_LADDER } });
|
|
170
171
|
}
|
|
171
172
|
}
|
|
173
|
+
// task 1004950 / ADR 0395: what the task needs beyond the build loop.
|
|
174
|
+
const createNeeds = api.taskNeeds.normalizeNeeds(body.needs);
|
|
175
|
+
if (!createNeeds.ok) return badNeeds(res, createNeeds.bad);
|
|
172
176
|
// BV1.R53 (task 1422): validate module_key against MODULE_SCOPE_MAP.
|
|
173
177
|
if (body.module_key != null && !isModuleKey(body.module_key)) {
|
|
174
178
|
return res.fail('bad_module_key', { status: 400, message: `module_key must be one of the known module keys, or omitted. Valid keys: ${moduleKeys().join(', ')}.`, details: { valid: moduleKeys() } });
|
|
@@ -245,6 +249,7 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
245
249
|
requiresRank: body.requires_rank ?? null,
|
|
246
250
|
moduleKey: body.module_key ?? null,
|
|
247
251
|
goalId: body.goal_id ?? null,
|
|
252
|
+
needs: createNeeds.needs,
|
|
248
253
|
// task 1003278 — attribution, from the AUTHENTICATED request, never the
|
|
249
254
|
// body: who created a task is a fact about the caller, not an input.
|
|
250
255
|
createdBy: req.builder.id,
|
|
@@ -348,6 +353,7 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
348
353
|
xenos_claimable: { type: 'boolean' }, // pre-rename alias for newcomer_friendly
|
|
349
354
|
security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
|
|
350
355
|
automation_tag: { type: 'string', maxLength: 64 },
|
|
356
|
+
needs: {}, // task 1004950 — ['owner'|'server'], a comma list, or null/[] to clear; checked inline
|
|
351
357
|
needs_migration: { type: 'boolean' }, // task 1002648 — correct the create-time heuristic's flag (the claim-time allocator keys off it)
|
|
352
358
|
// task 1002647 (map s4.3): the task body is a load-bearing builder prompt
|
|
353
359
|
// and was settable only at create — no route edited it at any rank. Now
|
|
@@ -397,6 +403,7 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
397
403
|
const hasParallelSafe = Object.prototype.hasOwnProperty.call(body, 'parallel_safe');
|
|
398
404
|
const hasSecuritySensitive = Object.prototype.hasOwnProperty.call(body, 'security_sensitive');
|
|
399
405
|
const hasAutomationTag = Object.prototype.hasOwnProperty.call(body, 'automation_tag');
|
|
406
|
+
const hasNeeds = Object.prototype.hasOwnProperty.call(body, 'needs');
|
|
400
407
|
const hasNeedsMigration = Object.prototype.hasOwnProperty.call(body, 'needs_migration');
|
|
401
408
|
const hasRequiresRank = Object.prototype.hasOwnProperty.call(body, 'requires_rank');
|
|
402
409
|
const hasModuleKey = Object.prototype.hasOwnProperty.call(body, 'module_key');
|
|
@@ -416,8 +423,8 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
416
423
|
if (Object.prototype.hasOwnProperty.call(body, 'status')) {
|
|
417
424
|
return res.fail('status_not_patchable', { status: 400, message: STATUS_HINT });
|
|
418
425
|
}
|
|
419
|
-
if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId && !hasDescription && !hasTitle && !hasPriority && !hasValueSummary) {
|
|
420
|
-
return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, title, description, value_summary, priority, parallel_safe, security_sensitive, automation_tag, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
|
|
426
|
+
if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeeds && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId && !hasDescription && !hasTitle && !hasPriority && !hasValueSummary) {
|
|
427
|
+
return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, title, description, value_summary, priority, parallel_safe, security_sensitive, automation_tag, needs, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
|
|
421
428
|
}
|
|
422
429
|
// ---- task 1002609 (G-22): settle EVERY gate and validation before the ----
|
|
423
430
|
// ---- FIRST write. The previous shape validated-and-wrote per field in ----
|
|
@@ -476,6 +483,8 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
476
483
|
return res.fail('bad_automation_tag', { status: 400, message: 'automation_tag must be one of overnight-safe | live-only | needs-input, or null to clear', details: { valid: Array.from(VALID_AUTOMATION_TAGS) } });
|
|
477
484
|
}
|
|
478
485
|
}
|
|
486
|
+
const patchNeeds = hasNeeds ? api.taskNeeds.normalizeNeeds(body.needs) : null;
|
|
487
|
+
if (patchNeeds && !patchNeeds.ok) return badNeeds(res, patchNeeds.bad);
|
|
479
488
|
if (hasModuleKey) {
|
|
480
489
|
// BV1.R53 (task 1422): assign or clear the module affinity label. null clears.
|
|
481
490
|
if (body.module_key !== null && !isModuleKey(body.module_key)) {
|
|
@@ -577,6 +586,15 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
577
586
|
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
578
587
|
}
|
|
579
588
|
}
|
|
589
|
+
if (hasNeeds) {
|
|
590
|
+
try {
|
|
591
|
+
updated = await db.updateTaskNeeds(id, patchNeeds.needs);
|
|
592
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
log.error('[gds] PATCH /tasks/:id needs', err);
|
|
595
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
580
598
|
if (hasNeedsMigration) {
|
|
581
599
|
try {
|
|
582
600
|
updated = await db.updateTaskNeedsMigration(id, body.needs_migration);
|
|
@@ -91,7 +91,7 @@ const PUBLISH_BUNDLE_MAX_BYTES = 20 * 1024 * 1024; // 20 MB decoded bundle
|
|
|
91
91
|
// autolinks to GitHub's own issue space (dead links for GDS task ids — task 916).
|
|
92
92
|
// Mirrors ship.js sanitizeGitHubRefs (which lives in scripts/ and isn't requirable
|
|
93
93
|
// here).
|
|
94
|
-
const { MIGRATION_KEYWORD_RE, VALID_FITS, badKind, deHashRefs, detectsMigration, gateTaskOwnership, graderBypassEnabled, isVerifyTask, peerVoteNet, postGateAuthorTrust, rankAttestsGateTrust } = require('./task-route-helpers.js');
|
|
94
|
+
const { MIGRATION_KEYWORD_RE, VALID_FITS, badKind, badNeedsFilter, parseNeedsFilter, deHashRefs, detectsMigration, gateTaskOwnership, graderBypassEnabled, isVerifyTask, peerVoteNet, postGateAuthorTrust, rankAttestsGateTrust } = require('./task-route-helpers.js');
|
|
95
95
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
96
96
|
const log = api.logger('lifecycle');
|
|
97
97
|
const registerTaskWriteRoutes = require('./task-write-routes.js'); // task 1003206 — POST /tasks + PATCH /tasks/:id
|
|
@@ -176,10 +176,11 @@ module.exports = function buildTasksRouter() {
|
|
|
176
176
|
if (limit && limit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer up to 1000.' });
|
|
177
177
|
const offset = posInt(offsetRaw, 'bad_offset');
|
|
178
178
|
if (offset && offset.bad) return res.fail('bad_offset', { status: 400, message: 'offset must be a positive integer.' });
|
|
179
|
+
const needs = parseNeedsFilter(req.query.needs); if (needs && needs.bad) return badNeedsFilter(res); // task 1004950
|
|
179
180
|
|
|
180
181
|
const tasks = await db.listTasks({
|
|
181
182
|
versionId: version, status, kind, discipline, goalId,
|
|
182
|
-
createdBy, staleDays, limit, offset,
|
|
183
|
+
createdBy, staleDays, limit, offset, needs,
|
|
183
184
|
});
|
|
184
185
|
// ?include=deps (hall redesign task 1002439) — enrich each task with its
|
|
185
186
|
// dependency summary (`blocked_by[]` with per-target satisfied state, and a
|
|
@@ -216,16 +217,15 @@ module.exports = function buildTasksRouter() {
|
|
|
216
217
|
if (claimStaleDays && claimStaleDays.bad) return res.fail('bad_stale_days', { status: 400, message: 'stale_days must be a positive integer number of days.' });
|
|
217
218
|
// ?limit=N — opt-in, for a caller that renders N rows (goal 1000072). It slices the
|
|
218
219
|
// RETURNED feed, never the query — lifecycle/CLAUDE.md has the why.
|
|
220
|
+
const claimNeeds = parseNeedsFilter(req.query.needs); if (claimNeeds && claimNeeds.bad) return badNeedsFilter(res); // task 1004950
|
|
219
221
|
const claimLimit = posInt(req.query.limit, 'bad_limit', 1000);
|
|
220
222
|
if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' });
|
|
221
223
|
const [feed, active] = await Promise.all([
|
|
222
224
|
db.listClaimableTasks({
|
|
223
|
-
versionId: version || null,
|
|
224
|
-
|
|
225
|
-
discipline: discipline || null,
|
|
226
|
-
fit: fit || null,
|
|
225
|
+
versionId: version || null, builderId: req.builder.id,
|
|
226
|
+
discipline: discipline || null, fit: fit || null,
|
|
227
227
|
createdBy: claimCreatedBy ?? null,
|
|
228
|
-
staleDays: claimStaleDays ?? null,
|
|
228
|
+
staleDays: claimStaleDays ?? null, needs: claimNeeds ?? null,
|
|
229
229
|
}),
|
|
230
230
|
req.query.group === 'goal' ? [] : db.listActiveClaims(),
|
|
231
231
|
]);
|
|
@@ -507,11 +507,11 @@ module.exports = function buildTasksRouter() {
|
|
|
507
507
|
// the numeric done_when_criteria.id, mirroring DELETE deps/:depId).
|
|
508
508
|
// rank: metic+archon — removes a criterion↔task edge; task-shaping, Metic+ (ADR 0090).
|
|
509
509
|
router.delete('/tasks/:id/criteria/:criterionId', auth.requireBuilder, auth.requirePermission('task.criterion.manage'), async (req, res) => {
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
|
|
510
|
+
// task 1004984: whole positive ids only (Number.isFinite let 7.5 through).
|
|
511
|
+
const id = parseId(req, res, { code: 'bad_task_id', positiveInt: true });
|
|
512
|
+
if (id === null) return;
|
|
513
|
+
const criterionId = parseId(req, res, { param: 'criterionId', code: 'bad_criterion_id', positiveInt: true });
|
|
514
|
+
if (criterionId === null) return;
|
|
515
515
|
const removed = await db.removeTaskCriterion(id, criterionId);
|
|
516
516
|
res.json({ ok: true, removed });
|
|
517
517
|
});
|