@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
|
@@ -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
|
+
};
|
|
@@ -241,7 +241,28 @@ function line(r, { max = 3 } = {}) {
|
|
|
241
241
|
// Name the skill, not just the diagnosis: 19 of the last 30 days' session logs mention a strand,
|
|
242
242
|
// and the next claim is refused REBASE_REQUIRED until one is landed or released (task 1003548).
|
|
243
243
|
+ `Run /strand-fix ${worst.id} to walk it. `
|
|
244
|
-
+ 'A strand produces no signal of its own; this line is the only one.'
|
|
244
|
+
+ 'A strand produces no signal of its own; this line is the only one. '
|
|
245
|
+
// Agents kept relaying this to the owner on every session (owner, 2026-10-28; task 1004952).
|
|
246
|
+
+ RELAY_RULE;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// This line is context for the AGENT. Fixed text only — it is injected every session.
|
|
250
|
+
// "Fix it yourself" sent EVERY session at the same strand (task 1004956): /strand-fix now takes a
|
|
251
|
+
// lease first, so the rule says one session fixes it and the others are told who has it.
|
|
252
|
+
const RELAY_RULE = 'This note is for you, not the builder: do not repeat it to them. ONE session fixes a strand — '
|
|
253
|
+
+ '/strand-fix takes it first and stops if another session already has it; tell the builder only if it needs a person or they ask.';
|
|
254
|
+
|
|
255
|
+
// The lease clause, appended at PRINT time, never cached: the line text is shared across
|
|
256
|
+
// sessions for up to two hours (linePlan), while who holds a strand changes by the minute and
|
|
257
|
+
// differs per worktree. Fail-open: no lease folder, no clause.
|
|
258
|
+
function withLeases(text, { now = Date.now() } = {}) {
|
|
259
|
+
if (!text) return text;
|
|
260
|
+
try {
|
|
261
|
+
const lease = require('./strand-lease.js');
|
|
262
|
+
const me = lease.currentHolder();
|
|
263
|
+
const clause = lease.othersClause(lease.liveLeases({ dir: lease.leaseDir(), now }), me && me.holder, now);
|
|
264
|
+
return clause ? `${text} ${clause}` : text;
|
|
265
|
+
} catch { return text; }
|
|
245
266
|
}
|
|
246
267
|
|
|
247
268
|
function report(r) {
|
|
@@ -364,10 +385,10 @@ async function main() {
|
|
|
364
385
|
const rec = readLineRecord();
|
|
365
386
|
const plan = linePlan(rec, Date.now());
|
|
366
387
|
if (plan === 'fresh') {
|
|
367
|
-
if (rec.value) console.log(rec.value);
|
|
388
|
+
if (rec.value) console.log(withLeases(rec.value));
|
|
368
389
|
} else if (plan === 'stale') {
|
|
369
390
|
const one = labelStale(rec.value, Date.now() - rec.at);
|
|
370
|
-
if (one) console.log(one);
|
|
391
|
+
if (one) console.log(withLeases(one));
|
|
371
392
|
spawnRefresh();
|
|
372
393
|
} else {
|
|
373
394
|
const { cached } = require('./probe-cache.js');
|
|
@@ -376,7 +397,7 @@ async function main() {
|
|
|
376
397
|
// say how old, so a long-expired line is never passed off as current.
|
|
377
398
|
const shown = from === 'stale-while-locked' && rec && typeof rec.at === 'number'
|
|
378
399
|
? labelStale(one, Date.now() - rec.at) : one;
|
|
379
|
-
if (shown) console.log(shown);
|
|
400
|
+
if (shown) console.log(withLeases(shown));
|
|
380
401
|
}
|
|
381
402
|
} catch { /* no session, offline, unreadable, past the deadline — all silence */ }
|
|
382
403
|
process.exitCode = 0;
|
|
@@ -405,7 +426,7 @@ if (require.main === module) {
|
|
|
405
426
|
}
|
|
406
427
|
|
|
407
428
|
module.exports = {
|
|
408
|
-
check, line, report, classifyStrand, rankStrands, humanAge,
|
|
429
|
+
check, line, withLeases, report, classifyStrand, rankStrands, humanAge,
|
|
409
430
|
linePlan, labelStale, produceLine, LINE_FRESH_MS, LINE_STALE_MAX_MS, LINE_DEADLINE_MS,
|
|
410
431
|
CONFIRMED_STALE_MS, COMPLETED_STALE_MS, STRAND_REASON_PREFIX, WATCHED, PAGE_LIMIT,
|
|
411
432
|
};
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// WHAT IT CHECKS, narrowly: a version-shaped literal ASSIGNED TO A VERSION FIELD
|
|
13
13
|
// or used as a version DEFAULT, in an executable script. Not any mention of a
|
|
14
|
-
// version anywhere —
|
|
15
|
-
//
|
|
14
|
+
// version anywhere — a script may legitimately say "PMS-V2 P6 not live yet" in a
|
|
15
|
+
// comment or an echo, and flagging that would be the over-broad
|
|
16
16
|
// source assertion this repo has learned to avoid (learning 1000196). Comment
|
|
17
17
|
// lines are stripped before matching for the same reason.
|
|
18
18
|
//
|
package/scripts/discord/post.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
// modules/discord/ship-broadcast.js, whose kind filter decides which ships are
|
|
17
17
|
// news. What does pass through here is the project's own news in plain words,
|
|
18
18
|
// which is what #ship-news promises: a builder graduating
|
|
19
|
-
// (modules/onboarding/onboarding-state.js),
|
|
20
|
-
// (src/bongos/routes/builders.js), an outage heads-up
|
|
19
|
+
// (modules/onboarding/onboarding-state.js), an outage heads-up
|
|
21
20
|
// (modules/provisioning/pollers/liveness-notify.js) and the style-guide audit
|
|
22
|
-
// (scripts/cron/style_guide_audit.sh).
|
|
21
|
+
// (scripts/cron/style_guide_audit.sh). A rank change posts the same #ship-news line
|
|
22
|
+
// in-process through the discord.post port instead (task 1004962,
|
|
23
|
+
// src/bongos/routes/builders.js). Builder housekeeping names its channel:
|
|
23
24
|
// scripts/bongos/pulse.js's reclaim nudge passes --channel ship_feed.
|
|
24
25
|
|
|
25
26
|
const fs = require('node:fs');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/seed-tasks.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/seed-tasks.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
'use strict';
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'seed-tasks.js');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// scripts/gds/
|
|
2
|
+
// scripts/gds/strand-lease.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
3
|
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
-
// The real script is scripts/bongos/
|
|
4
|
+
// The real script is scripts/bongos/strand-lease.js. This forwarder keeps callers that still name
|
|
5
5
|
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
6
|
'use strict';
|
|
7
|
-
require('../bongos/forwarder.js').forward(module, '
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'strand-lease.js');
|
|
@@ -156,6 +156,15 @@ const FIXTURE_FALLBACKS = [
|
|
|
156
156
|
// a store version's how-to page (task 1004366): one reading serves any key and
|
|
157
157
|
// version, since the Modules tab's how-to view shows one document.
|
|
158
158
|
[/^store\/modules\/[a-z0-9-]+\/versions\/[^/]+\/howto$/, 'store__modules__howto'],
|
|
159
|
+
// the studio's AI pages (task 1004921): one request per STATE, picked by id,
|
|
160
|
+
// so the waiting page, a result on the balance, a result on a Gemini key and a
|
|
161
|
+
// failed run each render: 1002 is running, 1003 failed, 1004 ran on the
|
|
162
|
+
// builder's own Gemini key, and any other id is the finished result on the
|
|
163
|
+
// balance.
|
|
164
|
+
[/^ai-studio\/requests\/1002$/, 'ai-studio__requests__running'],
|
|
165
|
+
[/^ai-studio\/requests\/1003$/, 'ai-studio__requests__failed'],
|
|
166
|
+
[/^ai-studio\/requests\/1004$/, 'ai-studio__requests__own'],
|
|
167
|
+
[/^ai-studio\/requests\/\d+$/, 'ai-studio__requests__done'],
|
|
159
168
|
];
|
|
160
169
|
|
|
161
170
|
// CANNED WRITES (task 1004321). The tweak editor's whole flow is writes (the
|
|
@@ -186,6 +195,11 @@ const CANNED_WRITES = [
|
|
|
186
195
|
// Settings → Software update's "Ask for this upgrade" (task 1004595), so the
|
|
187
196
|
// pending line the panel draws after the press renders here too.
|
|
188
197
|
['POST', /^software-update\/request$/, 'software-update__request.post'],
|
|
198
|
+
// the studio's AI pages (task 1004921): a file uploads, a request is sent
|
|
199
|
+
// (it answers as 1002, the running one), and money is added.
|
|
200
|
+
['POST', /^ai-studio\/uploads$/, 'ai-studio__uploads.post'],
|
|
201
|
+
['POST', /^ai-studio\/requests$/, 'ai-studio__requests.post'],
|
|
202
|
+
['POST', /^ai-studio\/topup$/, 'ai-studio__topup.post'],
|
|
189
203
|
];
|
|
190
204
|
|
|
191
205
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
|
@@ -316,6 +330,13 @@ app.use((req, res, next) => {
|
|
|
316
330
|
// Studio's review card, where the picture IS the card — previews as a broken
|
|
317
331
|
// frame, so the one thing being reviewed is the one thing you cannot see.
|
|
318
332
|
// A generated stand-in; see scripts/hall-preview/task-visual.js.
|
|
333
|
+
// The studio's AI files (task 1004921) are images too: an upload's thumbnail
|
|
334
|
+
// and the pictures a request made. The same generated stand-in.
|
|
335
|
+
if (/^ai-studio\/files\/\d+$/.test(apiPath) && req.method === 'GET') {
|
|
336
|
+
const png = require('./task-visual').placeholderPng();
|
|
337
|
+
res.set({ 'content-type': 'image/png', 'cache-control': 'no-store' });
|
|
338
|
+
return res.end(png);
|
|
339
|
+
}
|
|
319
340
|
if (/^task-visuals\/[A-Za-z0-9._-]+$/.test(apiPath) && req.method === 'GET') {
|
|
320
341
|
// HALL_PREVIEW_VISUALS=<dir> serves a REAL image of that name from <dir>
|
|
321
342
|
// instead (task 1004323): the Approval queue's review sheet shows the renders
|