@bongos/core 1.23.21 → 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 +983 -428
- 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 +6 -3
- 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 +94 -0
- package/clients/bongos-client/index.cjs +94 -0
- package/clients/bongos-client/index.d.ts +154 -4
- package/clients/bongos-client/index.mjs +94 -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/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- 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 +10 -1
- package/docs/api/openapi.json +8619 -5885
- package/docs/api-reference.md +68 -9
- 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 +4 -0
- package/docs/module-api-changelog.md +7 -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/CLAUDE.md +15 -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/module.json +26 -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 +252 -0
- 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 +108 -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 +50 -18
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/grading/grader-subagent.js +30 -11
- 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/city.html +15 -15
- 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-analytics.js +10 -3
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +48 -18
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/db-versions.js +21 -8
- 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/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- 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/onboarding/howto-moments.js +8 -1
- 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 +8 -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 +286 -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 +4 -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 +129 -12
- 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 +16 -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/src/platform-server.js +27 -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/batched_writes_parity.mjs +340 -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/conductor_main_e2e.mjs +4 -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/per-row-loops-legacy.cjs +245 -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_archive_disposition.mjs +5 -5
- 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 +146 -23
- package/tests/grader.mjs +31 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_city.mjs +8 -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/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- 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/module_skills_hook.mjs +69 -0
- 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/public_landing_projects.mjs +37 -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/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- 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 +58 -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_close_route.mjs +1 -1
- 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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/bongos/seed-tasks.js — file one planning session's tasks from a JSON spec (task 1004979).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Every planning session used to copy a whole seed SCRIPT (seed-bv1-profiles-tasks.js,
|
|
5
|
+
// seed-bongos-v1-idea-routing-tasks.js, seed-bv2-ideator-surface-tasks.js) and edit its TASKS
|
|
6
|
+
// array: about 700 lines of three near-identical HTTP loops, each with its own drift — one
|
|
7
|
+
// tallied replays twice, one never verified its criterion links, one never promoted anything.
|
|
8
|
+
// The only part that differs per session is DATA, so this is the one seeder and a session now
|
|
9
|
+
// writes a spec. The three old scripts were replaced by their specs, and
|
|
10
|
+
// tests/seed_tasks.mjs proves this filer sends exactly the same writes they did.
|
|
11
|
+
//
|
|
12
|
+
// THE SPEC (see scripts/bongos/seed-tasks.example.json):
|
|
13
|
+
// version_id, goal_id where every task lands
|
|
14
|
+
// source_ref_prefix each task's source_ref is `<prefix>-<ref>`; POST /tasks is
|
|
15
|
+
// idempotent on (source, source_ref), so a re-run replays
|
|
16
|
+
// source default 'planning-session'
|
|
17
|
+
// tasks[] { ref, title, description, kind, discipline, priority,
|
|
18
|
+
// criterion_ids, deps?, est_minutes?, touches?, requires_rank?,
|
|
19
|
+
// manual_degree?, credits_reward?, security_sensitive? }
|
|
20
|
+
// promote[] refs to POST /tasks/:id/promote after the edges are wired
|
|
21
|
+
// links[] { method, path, body } follow-up writes (an idea marked
|
|
22
|
+
// promoted, a meta task moved into the goal); a body value
|
|
23
|
+
// { "$task": "R11" } becomes that ref's task id, and a link
|
|
24
|
+
// whose ref did not land is skipped
|
|
25
|
+
// verify_criteria default true — see below
|
|
26
|
+
// allow_positional_criteria default false — only the historical bv1-profiles spec sets it
|
|
27
|
+
//
|
|
28
|
+
// THE ORDER is the old scripts' order: baseline the goal, create every task, wire every
|
|
29
|
+
// dependency edge, promote, follow-up links, then verify. Verification is
|
|
30
|
+
// seed-verify-criteria.js's contract (task 1003076, task 1004668): baseline BEFORE the first
|
|
31
|
+
// write, tally only the criterion links this run CREATED (a replay is already in the baseline),
|
|
32
|
+
// and exit 1 loudly on any mismatch.
|
|
33
|
+
//
|
|
34
|
+
// Positional "Cn" criterion refs are refused at validation. They rank over the whole VERSION's
|
|
35
|
+
// criteria and mislinked all 15 tasks of goal 1000071 (learning 1000188); the server now
|
|
36
|
+
// refuses them on a multi-goal version too, but a spec should fail before its first write.
|
|
37
|
+
//
|
|
38
|
+
// Usage:
|
|
39
|
+
// node scripts/bongos/seed-tasks.js <spec.json> --dry-run # validate + print the plan; files nothing
|
|
40
|
+
// node scripts/bongos/seed-tasks.js <spec.json> # file it
|
|
41
|
+
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
const { sleep } = require('../../src/sleep');
|
|
47
|
+
const { baselineSeedCriteria, expectedCriterionCounts, verifySeedCriteriaLinks } = require('./seed-verify-criteria');
|
|
48
|
+
|
|
49
|
+
// Optional per-task fields that POST /tasks accepts and a spec may carry through as-is.
|
|
50
|
+
const OPTIONAL_FIELDS = ['est_minutes', 'touches', 'requires_rank', 'manual_degree', 'credits_reward', 'security_sensitive'];
|
|
51
|
+
const POSITIONAL_CRITERION = /^C\d+$/;
|
|
52
|
+
|
|
53
|
+
/** Every problem with a spec, as plain sentences. An empty list means it can be filed. */
|
|
54
|
+
function validateSpec(spec) {
|
|
55
|
+
const errs = [];
|
|
56
|
+
if (!spec || typeof spec !== 'object') return ['the spec is not a JSON object'];
|
|
57
|
+
if (typeof spec.version_id !== 'string' || !spec.version_id) errs.push('version_id must be a non-empty string');
|
|
58
|
+
if (!Number.isInteger(Number(spec.goal_id)) || Number(spec.goal_id) <= 0) errs.push('goal_id must be a positive integer');
|
|
59
|
+
if (typeof spec.source_ref_prefix !== 'string' || !spec.source_ref_prefix) errs.push('source_ref_prefix must be a non-empty string');
|
|
60
|
+
if (!Array.isArray(spec.tasks) || !spec.tasks.length) { errs.push('tasks must be a non-empty array'); return errs; }
|
|
61
|
+
const refs = new Set();
|
|
62
|
+
for (const [i, t] of spec.tasks.entries()) {
|
|
63
|
+
const at = `tasks[${i}]${t && t.ref ? ` (${t.ref})` : ''}`;
|
|
64
|
+
if (!t || typeof t.ref !== 'string' || !t.ref) { errs.push(`${at}: ref must be a non-empty string`); continue; }
|
|
65
|
+
if (refs.has(t.ref)) errs.push(`${at}: duplicate ref`);
|
|
66
|
+
refs.add(t.ref);
|
|
67
|
+
for (const f of ['title', 'description', 'kind', 'discipline']) {
|
|
68
|
+
if (typeof t[f] !== 'string' || !t[f]) errs.push(`${at}: ${f} must be a non-empty string`);
|
|
69
|
+
}
|
|
70
|
+
if (!Number.isInteger(t.priority)) errs.push(`${at}: priority must be an integer`);
|
|
71
|
+
if (!Array.isArray(t.criterion_ids) || !t.criterion_ids.length) errs.push(`${at}: criterion_ids must name at least one criterion`);
|
|
72
|
+
else if (!spec.allow_positional_criteria) {
|
|
73
|
+
for (const c of t.criterion_ids) {
|
|
74
|
+
if (POSITIONAL_CRITERION.test(String(c))) errs.push(`${at}: "${c}" is a positional criterion ref — use the criterion_id slug (learning 1000188)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (t.deps !== undefined && !Array.isArray(t.deps)) errs.push(`${at}: deps must be an array of refs`);
|
|
78
|
+
}
|
|
79
|
+
for (const t of spec.tasks) {
|
|
80
|
+
for (const d of (t && Array.isArray(t.deps) ? t.deps : [])) {
|
|
81
|
+
if (d === t.ref) errs.push(`${t.ref}: depends on itself`);
|
|
82
|
+
else if (!refs.has(d)) errs.push(`${t.ref}: depends on unknown ref ${d}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const r of spec.promote || []) if (!refs.has(r)) errs.push(`promote: unknown ref ${r}`);
|
|
86
|
+
for (const [i, l] of (spec.links || []).entries()) {
|
|
87
|
+
if (!l || !['PATCH', 'POST'].includes(l.method)) errs.push(`links[${i}]: method must be PATCH or POST`);
|
|
88
|
+
if (!l || typeof l.path !== 'string' || !l.path.startsWith('/')) errs.push(`links[${i}]: path must start with /`);
|
|
89
|
+
for (const r of linkRefs(l && l.body)) if (!refs.has(r)) errs.push(`links[${i}]: unknown ref ${r}`);
|
|
90
|
+
}
|
|
91
|
+
return errs;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function linkRefs(body) {
|
|
95
|
+
return Object.values(body || {}).filter((v) => v && typeof v === 'object' && typeof v.$task === 'string').map((v) => v.$task);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The POST /tasks body for one spec task — the field order the old scripts sent. */
|
|
99
|
+
function taskBody(spec, t) {
|
|
100
|
+
const body = {
|
|
101
|
+
version_id: spec.version_id, goal_id: Number(spec.goal_id), title: t.title, description: t.description,
|
|
102
|
+
kind: t.kind, discipline: t.discipline, priority: t.priority,
|
|
103
|
+
};
|
|
104
|
+
for (const f of OPTIONAL_FIELDS) if (t[f] !== undefined) body[f] = t[f];
|
|
105
|
+
body.criterion_ids = t.criterion_ids;
|
|
106
|
+
body.source = spec.source || 'planning-session';
|
|
107
|
+
body.source_ref = `${spec.source_ref_prefix}-${t.ref}`;
|
|
108
|
+
return body;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The dry run: what would be filed, in order. Sends nothing. */
|
|
112
|
+
function describePlan(spec, log = console.log) {
|
|
113
|
+
log(`DRY RUN — would seed ${spec.tasks.length} task(s) into goal ${spec.goal_id} (${spec.version_id}), source_ref ${spec.source_ref_prefix}-<ref>:`);
|
|
114
|
+
for (const t of spec.tasks) {
|
|
115
|
+
const deps = t.deps && t.deps.length ? ` after ${t.deps.join(',')}` : '';
|
|
116
|
+
log(` ${t.ref} ${t.kind}/${t.discipline} P${t.priority} [${t.criterion_ids.join(',')}]${deps} ${t.title}`);
|
|
117
|
+
}
|
|
118
|
+
if ((spec.promote || []).length) log(` then promote: ${spec.promote.join(', ')}`);
|
|
119
|
+
for (const l of spec.links || []) log(` then ${l.method} ${l.path}`);
|
|
120
|
+
log(` then ${spec.verify_criteria === false ? 'skip' : 'verify'} the criterion links.`);
|
|
121
|
+
log('Nothing was filed.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* File a spec. `api(method, path, body)` resolves to { status, ok, json } for a path under
|
|
126
|
+
* /api/bongos; it is injected so the equivalence test can point this at a fake server.
|
|
127
|
+
*/
|
|
128
|
+
async function seedTasks(spec, { api, log = console.log, exit = process.exit } = {}) {
|
|
129
|
+
const goalId = Number(spec.goal_id);
|
|
130
|
+
const verify = spec.verify_criteria !== false;
|
|
131
|
+
const fetchGoal = (gid) => api('GET', `/goals/${gid}`);
|
|
132
|
+
const baseline = verify ? await baselineSeedCriteria({ goalId, fetchGoal, log, exit }) : null;
|
|
133
|
+
|
|
134
|
+
const idByRef = {};
|
|
135
|
+
const criterionIdListsPerTask = [];
|
|
136
|
+
let created = 0, replayed = 0, failed = 0;
|
|
137
|
+
log(`Seeding ${spec.tasks.length} task(s) into goal ${goalId} (${spec.version_id})…`);
|
|
138
|
+
for (const t of spec.tasks) {
|
|
139
|
+
const r = await api('POST', '/tasks', taskBody(spec, t));
|
|
140
|
+
const task = r.ok && r.json ? (r.json.task || r.json) : null;
|
|
141
|
+
if (!task || task.id == null) { failed++; log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.json).slice(0, 300)}`); continue; }
|
|
142
|
+
idByRef[t.ref] = task.id;
|
|
143
|
+
if (r.json.idempotent) replayed++;
|
|
144
|
+
else { created++; criterionIdListsPerTask.push(t.criterion_ids); }
|
|
145
|
+
log(` ${t.ref} → task ${task.id}${r.json.idempotent ? ' (already there)' : ''} ${t.title.slice(0, 80)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
log('Wiring dependencies…');
|
|
149
|
+
let edges = 0;
|
|
150
|
+
for (const t of spec.tasks) {
|
|
151
|
+
if (!t.deps || !t.deps.length) continue;
|
|
152
|
+
const id = idByRef[t.ref];
|
|
153
|
+
const depIds = t.deps.map((d) => idByRef[d]).filter((x) => x != null);
|
|
154
|
+
if (id == null || depIds.length !== t.deps.length) { log(` ${t.ref}: a task in this edge did not land — skipped`); continue; }
|
|
155
|
+
const r = await api('POST', `/tasks/${id}/dependencies`, { depends_on_task_ids: depIds });
|
|
156
|
+
if (!r.ok) { failed++; log(` ${t.ref}: dependency FAILED ${r.status} ${JSON.stringify(r.json).slice(0, 180)}`); continue; }
|
|
157
|
+
edges += (r.json && r.json.added) || depIds.length;
|
|
158
|
+
log(` ${t.ref} depends on ${t.deps.join(',')}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ((spec.promote || []).length) log('Promoting…');
|
|
162
|
+
for (const ref of spec.promote || []) {
|
|
163
|
+
const id = idByRef[ref];
|
|
164
|
+
if (id == null) continue;
|
|
165
|
+
const r = await api('POST', `/tasks/${id}/promote`, {});
|
|
166
|
+
log(` ${ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.json).slice(0, 120)}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if ((spec.links || []).length) log('Follow-up links…');
|
|
170
|
+
for (const l of spec.links || []) {
|
|
171
|
+
const missing = linkRefs(l.body).filter((ref) => idByRef[ref] == null);
|
|
172
|
+
if (missing.length) { log(` ${l.method} ${l.path}: skipped, ${missing.join(',')} did not land`); continue; }
|
|
173
|
+
const body = {};
|
|
174
|
+
for (const [k, v] of Object.entries(l.body || {})) body[k] = v && typeof v === 'object' && typeof v.$task === 'string' ? Number(idByRef[v.$task]) : v;
|
|
175
|
+
const r = await api(l.method, l.path, body);
|
|
176
|
+
log(` ${l.method} ${l.path}: ${r.ok ? 'OK' : 'note ' + JSON.stringify(r.json).slice(0, 160)}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
log(`\nDONE. created=${created} replayed=${replayed} failed=${failed} depEdges=${edges}.`);
|
|
180
|
+
log('ref→id: ' + JSON.stringify(idByRef));
|
|
181
|
+
if (verify) {
|
|
182
|
+
log('Verifying criterion links against the plan…');
|
|
183
|
+
await verifySeedCriteriaLinks({ goalId, expectedCounts: expectedCriterionCounts(criterionIdListsPerTask), baseline, fetchGoal, log, exit });
|
|
184
|
+
}
|
|
185
|
+
return { idByRef, created, replayed, failed };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The real transport: the CLI session, a base throttle, and the 429 back-off every seed needs. */
|
|
189
|
+
function cliApi({ throttleMs = 400 } = {}) {
|
|
190
|
+
const { apiCall } = require('./cli-lib');
|
|
191
|
+
return async function api(method, p, body, tries = 0) {
|
|
192
|
+
await sleep(throttleMs);
|
|
193
|
+
const r = await apiCall(method, `/api/bongos${p}`, body || null);
|
|
194
|
+
const json = r.data || {};
|
|
195
|
+
if (r.status === 429 && tries < 8) {
|
|
196
|
+
const wait = ((json.error && json.error.details && json.error.details.retry_after_seconds) || 30) + 1;
|
|
197
|
+
console.log(` …rate-limited, waiting ${wait}s`);
|
|
198
|
+
await sleep(wait * 1000);
|
|
199
|
+
return api(method, p, body, tries + 1);
|
|
200
|
+
}
|
|
201
|
+
return { status: r.status, ok: r.ok, json };
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function main(argv) {
|
|
206
|
+
const file = argv.find((a) => !a.startsWith('--'));
|
|
207
|
+
if (!file) { console.error('usage: node scripts/bongos/seed-tasks.js <spec.json> [--dry-run]'); return 2; }
|
|
208
|
+
let spec;
|
|
209
|
+
try { spec = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8')); }
|
|
210
|
+
catch (e) { console.error(`seed-tasks: cannot read ${file}: ${e.message}`); return 2; }
|
|
211
|
+
const errs = validateSpec(spec);
|
|
212
|
+
if (errs.length) { console.error(`seed-tasks: ${file} is not a valid spec:\n ${errs.join('\n ')}`); return 1; }
|
|
213
|
+
if (argv.includes('--dry-run')) { describePlan(spec); return 0; }
|
|
214
|
+
const out = await seedTasks(spec, { api: cliApi() });
|
|
215
|
+
return out.failed ? 1 : 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (require.main === module) {
|
|
219
|
+
main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (e) => { console.error(e); process.exitCode = 1; });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { validateSpec, taskBody, describePlan, seedTasks, cliApi, main };
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
// names. A seed is resumable (POST /tasks is idempotent on source_ref and replays
|
|
28
28
|
// an existing task with `idempotent: true`), and a replayed task's links are
|
|
29
29
|
// already inside the baseline — tally it again and a correct re-run expects them
|
|
30
|
-
// twice. So a baselined seed skips replays when it tallies (see
|
|
31
|
-
// seed-bv2-ideator-surface-tasks.js).
|
|
30
|
+
// twice. So a baselined seed skips replays when it tallies (see seed-tasks.js).
|
|
32
31
|
|
|
33
32
|
// countsByCriterion — { criterion_id: linked_tasks } from GET /goals/:id rows.
|
|
34
33
|
function countsByCriterion(criteriaRows) {
|
|
@@ -217,6 +217,8 @@ async function finishShip({ taskId, summary, advanceToMerge, skipMerge, postGrad
|
|
|
217
217
|
// task 1719: pass the reconciled status so a just-landed ship shows SHIPPED (and
|
|
218
218
|
// writes no breadcrumb), not the stale pre-merge 'confirmed'.
|
|
219
219
|
await printCompletionCard({ taskId, postGradeStatus: landedStatus, card, usage, land });
|
|
220
|
+
// task 1004956: a landed strand needs no fixer — clear whoever held its lease.
|
|
221
|
+
if (landedStatus === 'shipped') require('./strand-lease.js').clearOnExit(taskId);
|
|
220
222
|
// task 932: reconcile the working tree — ONLY on a successful land. A
|
|
221
223
|
// grade-failed ship stays at 'completed'; leave that tree untouched so the
|
|
222
224
|
// builder can fix findings + re-ship.
|
|
@@ -186,6 +186,8 @@ async function resumeMerge(taskId, { summary, skipMerge, force, task, dbOnly = f
|
|
|
186
186
|
try {
|
|
187
187
|
const t = await (await cliClient()).tasks.getTasksId({ id: taskId });
|
|
188
188
|
const status = (t.ok && t.data && t.data.task) ? t.data.task.status : 'shipped';
|
|
189
|
+
// task 1004956: a landed strand needs no fixer — clear whoever held its lease.
|
|
190
|
+
if (t.ok && status === 'shipped') require('./strand-lease.js').clearOnExit(taskId);
|
|
189
191
|
await printCompletionCard({
|
|
190
192
|
taskId, postGradeStatus: status, usage, land,
|
|
191
193
|
card: { title: task?.title, valueSummary: effSummary, notes: '', creditsAwarded: null },
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
//
|
|
4
|
+
// scripts/bongos/strand-lease.js — ONE session fixes a stuck ship; the others are told who
|
|
5
|
+
// has it (task 1004956).
|
|
6
|
+
//
|
|
7
|
+
// THE GAP. A builder runs several sessions at once, one worktree each. When a ship strands,
|
|
8
|
+
// every session start prints the same strand note, and that note told every one of them to
|
|
9
|
+
// fix it. So N sessions ran /strand-fix on the SAME task: duplicated paid work, sessions
|
|
10
|
+
// colliding on one branch, and the owner watching all of them recommend the same fix
|
|
11
|
+
// (owner, 2026-10-29: "maybe all sessions point to one session to fix").
|
|
12
|
+
//
|
|
13
|
+
// THE LEASE. Fixing a strand takes a short-lived mark: "this worktree is on task N until T".
|
|
14
|
+
// /strand-fix takes it first and stops if another live session holds it; the strand note and
|
|
15
|
+
// the claim refusal name the holder, so every other session carries on with its own work.
|
|
16
|
+
//
|
|
17
|
+
// WHY A FILE IN THE COMMON GIT DIR, NOT A SERVER ROW. Every session a builder runs on one
|
|
18
|
+
// machine is a worktree of one repo, and they all share one common git dir — the same reason
|
|
19
|
+
// merge-lock.js keeps the auto-merge mutex there. That is exactly the set that collides, it
|
|
20
|
+
// needs no migration and no route, and it still works when the API is unreachable. A session
|
|
21
|
+
// on another machine cannot see it; that machine's sessions share their own.
|
|
22
|
+
//
|
|
23
|
+
// KEYED ON THE WORKTREE, NOT THE BUILDER. One builder, many sessions, is the normal case here,
|
|
24
|
+
// so a builder-keyed lease would make every one of their sessions look like the owner. The
|
|
25
|
+
// claim gate keys on the worktree for the same reason (routes/claims.js,
|
|
26
|
+
// gateOneActiveClaimPerSession).
|
|
27
|
+
//
|
|
28
|
+
// EXPIRY IS THE CRASH PATH. A session that just stops never releases. The lease lives
|
|
29
|
+
// LEASE_TTL_MS from its last take; /strand-fix re-takes it at each step to renew it, so a live
|
|
30
|
+
// fixer keeps it and a dead one loses it on its own. Landing (ship.js) and releasing
|
|
31
|
+
// (release.js) clear it at once.
|
|
32
|
+
//
|
|
33
|
+
// Usage:
|
|
34
|
+
// node scripts/bongos/strand-lease.js take <task-id> # take or renew; exit 3 if another session holds it
|
|
35
|
+
// node scripts/bongos/strand-lease.js release <task-id> # drop this worktree's lease
|
|
36
|
+
// node scripts/bongos/strand-lease.js show [<task-id>] # who holds what
|
|
37
|
+
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const path = require('node:path');
|
|
40
|
+
|
|
41
|
+
const LEASE_TTL_MS = 45 * 60 * 1000;
|
|
42
|
+
const HELD_EXIT = 3;
|
|
43
|
+
const DIR_NAME = 'otb-strand-leases';
|
|
44
|
+
|
|
45
|
+
const safeId = (v) => (/^\d{1,19}$/.test(String(v)) ? String(v) : null);
|
|
46
|
+
|
|
47
|
+
// One spelling per folder. Windows can hand the cwd in 8.3 short form (GMK1_P~1), so the same
|
|
48
|
+
// folder reached two ways would otherwise read as two holders.
|
|
49
|
+
function canonical(p) {
|
|
50
|
+
try { return fs.realpathSync.native(p); } catch { return path.resolve(p); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find this checkout WITHOUT spawning git. strand-watch --line calls this on the SessionStart
|
|
54
|
+
// path, whose cost was cut from 8.2 s by caching (task 1004928), so no subprocess belongs here.
|
|
55
|
+
// Walk up to `.git`: a directory is a main checkout (it IS the common dir); a file is a linked
|
|
56
|
+
// worktree, `gitdir: <common>/worktrees/<name>`, whose `commondir` file names the common dir.
|
|
57
|
+
// Memoised per cwd for the life of the process.
|
|
58
|
+
const located = new Map();
|
|
59
|
+
function locate(cwd) {
|
|
60
|
+
const start = path.resolve(cwd);
|
|
61
|
+
if (located.has(start)) return located.get(start);
|
|
62
|
+
let found = null;
|
|
63
|
+
for (let d = start; ; d = path.dirname(d)) {
|
|
64
|
+
const g = path.join(d, '.git');
|
|
65
|
+
let st = null;
|
|
66
|
+
try { st = fs.statSync(g); } catch { /* keep walking */ }
|
|
67
|
+
if (st && st.isDirectory()) { found = { top: d, common: g }; break; }
|
|
68
|
+
if (st && st.isFile()) {
|
|
69
|
+
const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(g, 'utf8'));
|
|
70
|
+
if (!m) break;
|
|
71
|
+
const gitdir = path.resolve(d, m[1]);
|
|
72
|
+
let common = gitdir;
|
|
73
|
+
try { common = path.resolve(gitdir, fs.readFileSync(path.join(gitdir, 'commondir'), 'utf8').trim()); } catch { /* not a linked worktree's gitdir */ }
|
|
74
|
+
found = { top: d, common };
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (path.dirname(d) === d) break;
|
|
78
|
+
}
|
|
79
|
+
if (found) found = { top: canonical(found.top), common: canonical(found.common) };
|
|
80
|
+
located.set(start, found);
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The lease folder: shared by every worktree of this repo. Null when not in a git checkout.
|
|
85
|
+
function leaseDir({ cwd = process.cwd() } = {}) {
|
|
86
|
+
const at = locate(cwd);
|
|
87
|
+
return at ? path.join(at.common, DIR_NAME) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Who "this session" is: its worktree. The label is what other sessions are shown.
|
|
91
|
+
function currentHolder({ cwd = process.cwd() } = {}) {
|
|
92
|
+
const at = locate(cwd);
|
|
93
|
+
if (!at) return null;
|
|
94
|
+
return { holder: at.top.toLowerCase().replace(/\\/g, '/'), label: path.basename(at.top) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function leaseFile(dir, taskId) { return path.join(dir, `${taskId}.json`); }
|
|
98
|
+
|
|
99
|
+
function readRaw(dir, taskId) {
|
|
100
|
+
try { return JSON.parse(fs.readFileSync(leaseFile(dir, taskId), 'utf8')); } catch { return null; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isLive(lease, now) {
|
|
104
|
+
return !!lease && Number.isFinite(lease.expires_at) && lease.expires_at > now;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The live lease on a task, or null (none, expired, or unreadable).
|
|
108
|
+
function read(taskId, { dir, now = Date.now() } = {}) {
|
|
109
|
+
const id = safeId(taskId);
|
|
110
|
+
if (!id || !dir) return null;
|
|
111
|
+
const l = readRaw(dir, id);
|
|
112
|
+
return isLive(l, now) ? l : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A temp file older than this was left by a process that died mid-write; nothing will rename it.
|
|
116
|
+
const TMP_STALE_MS = 60 * 1000;
|
|
117
|
+
function sweepTmp(dir, now) {
|
|
118
|
+
let names = [];
|
|
119
|
+
try { names = fs.readdirSync(dir); } catch { return; }
|
|
120
|
+
for (const n of names) {
|
|
121
|
+
if (!n.endsWith('.tmp')) continue;
|
|
122
|
+
const f = path.join(dir, n);
|
|
123
|
+
try { if (now - fs.statSync(f).mtimeMs > TMP_STALE_MS) fs.unlinkSync(f); } catch { /* raced */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Take or renew. { ok: true, lease, renewed } when this holder has it now; { ok: false, lease }
|
|
128
|
+
// naming the live holder otherwise. A FREE lease is created exclusively (flag 'wx'), so of two
|
|
129
|
+
// sessions taking it at the same instant the filesystem lets exactly one in. Renewing, and taking
|
|
130
|
+
// over an EXPIRED lease, replace the file atomically (temp + rename) and read it back: a takeover
|
|
131
|
+
// race can only happen after the holder has already been gone for the whole TTL.
|
|
132
|
+
function take(taskId, { dir, holder, label, now = Date.now(), ttlMs = LEASE_TTL_MS } = {}) {
|
|
133
|
+
const id = safeId(taskId);
|
|
134
|
+
if (!id) throw new Error(`not a task id: ${taskId}`);
|
|
135
|
+
if (!dir || !holder) throw new Error('no lease folder or holder (not inside a git checkout?)');
|
|
136
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
137
|
+
sweepTmp(dir, now);
|
|
138
|
+
let cur = readRaw(dir, id);
|
|
139
|
+
const lease = (taken) => ({
|
|
140
|
+
task_id: id, holder, label: label || path.basename(holder), taken_at: taken, expires_at: now + ttlMs,
|
|
141
|
+
});
|
|
142
|
+
if (!cur) {
|
|
143
|
+
try {
|
|
144
|
+
const fresh = lease(now);
|
|
145
|
+
fs.writeFileSync(leaseFile(dir, id), JSON.stringify(fresh), { flag: 'wx' });
|
|
146
|
+
return { ok: true, lease: fresh, renewed: false };
|
|
147
|
+
} catch (e) {
|
|
148
|
+
if (e.code !== 'EEXIST') throw e;
|
|
149
|
+
cur = readRaw(dir, id); // another session got there first
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (isLive(cur, now) && cur.holder !== holder) return { ok: false, lease: cur };
|
|
153
|
+
const renewed = isLive(cur, now) && cur.holder === holder;
|
|
154
|
+
const tmp = `${leaseFile(dir, id)}.${process.pid}.${now}.tmp`;
|
|
155
|
+
try {
|
|
156
|
+
fs.writeFileSync(tmp, JSON.stringify(lease(renewed ? cur.taken_at : now)));
|
|
157
|
+
fs.renameSync(tmp, leaseFile(dir, id));
|
|
158
|
+
} finally {
|
|
159
|
+
try { fs.unlinkSync(tmp); } catch { /* renamed away: the normal case */ }
|
|
160
|
+
}
|
|
161
|
+
const back = readRaw(dir, id);
|
|
162
|
+
if (!back || back.holder !== holder) return { ok: false, lease: back };
|
|
163
|
+
return { ok: true, lease: back, renewed };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Drop the lease if this holder has it (or always, with force — landing clears it whoever
|
|
167
|
+
// held it). Returns true when a lease file was removed.
|
|
168
|
+
function release(taskId, { dir, holder, force = false } = {}) {
|
|
169
|
+
const id = safeId(taskId);
|
|
170
|
+
if (!id || !dir) return false;
|
|
171
|
+
const cur = readRaw(dir, id);
|
|
172
|
+
if (!cur) return false;
|
|
173
|
+
if (!force && cur.holder !== holder) return false;
|
|
174
|
+
try { fs.unlinkSync(leaseFile(dir, id)); return true; } catch { return false; }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Every live lease, oldest first.
|
|
178
|
+
function liveLeases({ dir, now = Date.now() } = {}) {
|
|
179
|
+
if (!dir) return [];
|
|
180
|
+
let names;
|
|
181
|
+
try { names = fs.readdirSync(dir); } catch { return []; }
|
|
182
|
+
return names
|
|
183
|
+
.map((n) => /^(\d{1,19})\.json$/.exec(n))
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.map((m) => readRaw(dir, m[1]))
|
|
186
|
+
.filter((l) => isLive(l, now))
|
|
187
|
+
.sort((a, b) => a.taken_at - b.taken_at);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function minutes(ms) { return `${Math.max(1, Math.round(ms / 60000))}m`; }
|
|
191
|
+
|
|
192
|
+
// "session <label> (on it 12m)" — the words every other session is shown. The label is a
|
|
193
|
+
// worktree folder name, so it is safe to inject (no builder-authored text).
|
|
194
|
+
function describe(lease, now = Date.now()) {
|
|
195
|
+
const label = String(lease.label || '').replace(/[^\w.-]/g, '').slice(0, 80) || 'another session';
|
|
196
|
+
return `session ${label} (on it ${minutes(now - lease.taken_at)})`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The clause the strand note appends: the strands OTHER sessions hold, so this one leaves
|
|
200
|
+
// them alone. Null when nobody else holds one.
|
|
201
|
+
function othersClause(leases, holder, now = Date.now()) {
|
|
202
|
+
const others = (leases || []).filter((l) => l.holder !== holder);
|
|
203
|
+
if (!others.length) return null;
|
|
204
|
+
const list = others.map((l) => `task ${safeId(l.task_id)} by ${describe(l, now)}`).join('; ');
|
|
205
|
+
return `Already being fixed: ${list}. Leave ${others.length === 1 ? 'that one' : 'those'} to that session and carry on with your own work.`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Drop the lease for a task that has left the strand board (landed or released). Never throws:
|
|
209
|
+
// a cleanup that fails must not fail the ship or release it rides on.
|
|
210
|
+
function clearOnExit(taskId, { cwd = process.cwd() } = {}) {
|
|
211
|
+
try { return release(taskId, { dir: leaseDir({ cwd }), force: true }); } catch { return false; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function main(argv = process.argv.slice(2)) {
|
|
215
|
+
const [cmd, rawId] = argv;
|
|
216
|
+
const dir = leaseDir();
|
|
217
|
+
const me = currentHolder();
|
|
218
|
+
if (!dir || !me) { console.error('strand-lease: not inside a git checkout'); return 2; }
|
|
219
|
+
if (cmd === 'take') {
|
|
220
|
+
const r = take(rawId, { dir, holder: me.holder, label: me.label });
|
|
221
|
+
if (r.ok) {
|
|
222
|
+
console.log(`strand-lease: ${r.renewed ? 'renewed' : 'took'} task ${r.lease.task_id} for this session until ${new Date(r.lease.expires_at).toISOString()}.`);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
console.log(`strand-lease: task ${safeId(rawId)} is already being fixed by ${describe(r.lease)}.`);
|
|
226
|
+
console.log(' Leave it to that session and carry on with your own work. The hold runs out on its own if that session stops.');
|
|
227
|
+
return HELD_EXIT;
|
|
228
|
+
}
|
|
229
|
+
if (cmd === 'release') {
|
|
230
|
+
console.log(release(rawId, { dir, holder: me.holder })
|
|
231
|
+
? `strand-lease: released task ${safeId(rawId)}.`
|
|
232
|
+
: `strand-lease: this session held no lease on task ${safeId(rawId)}.`);
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
if (cmd === 'show') {
|
|
236
|
+
const leases = rawId ? [read(rawId, { dir })].filter(Boolean) : liveLeases({ dir });
|
|
237
|
+
if (!leases.length) { console.log('strand-lease: nobody is fixing a stuck ship right now.'); return 0; }
|
|
238
|
+
for (const l of leases) console.log(` task ${l.task_id} ${describe(l)}${l.holder === me.holder ? ' ← this session' : ''}`);
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
console.error('usage: node scripts/bongos/strand-lease.js take|release|show [<task-id>]');
|
|
242
|
+
return 2;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (require.main === module) process.exitCode = main();
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
LEASE_TTL_MS, HELD_EXIT, leaseDir, currentHolder, take, release, read, liveLeases,
|
|
249
|
+
describe, othersClause, clearOnExit,
|
|
250
|
+
};
|