flyloft 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {flyloft-0.2.1 → flyloft-0.2.2}/.github/workflows/ci.yml +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/PKG-INFO +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/ARCHITECTURE.md +7 -3
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/NEW_PROJECT.md +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/agent-interface.md +8 -8
- flyloft-0.2.2/docs/design/cpu-partition.md +122 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/tasks.md +1 -1
- flyloft-0.2.2/docs/plans/cpu-partition.md +319 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/clusters/slinky.toml +2 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/_version.py +2 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/client/main.py +41 -26
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/config/load.py +12 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/config/models.py +14 -0
- flyloft-0.2.2/src/flyloft/migrations/0028_cpu_runs.sql +9 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/actions.py +30 -3
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/launch.py +2 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/models.py +7 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/store.py +12 -4
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/staging.py +22 -5
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/service/tick.py +8 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/skill/SKILL.md +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/skill/handoff.md +3 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/tools/server.py +19 -3
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/assembly.py +3 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/_cj_implementer_rules.md.j2 +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/_cj_record.md.j2 +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/app.py +4 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/compute.py +2 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/schemas.py +4 -0
- flyloft-0.2.1/src/flyloft/web/static/assets/index-D0_YC9Sj.js → flyloft-0.2.2/src/flyloft/web/static/assets/index-CEib_UXZ.js +30 -30
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/static/index.html +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/conftest.py +19 -3
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_agent_interface.py +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_investigate.py +93 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_live.py +12 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_tasks.py +27 -1
- flyloft-0.2.2/tests/scripts/cpu_investigator.py +36 -0
- flyloft-0.2.2/tests/scripts/cpu_tasker.py +26 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/integrator.py +13 -17
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/integrator_spoil.py +3 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/investigator.py +13 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/live_investigator.py +7 -1
- flyloft-0.2.2/tests/unit/test_compute_view.py +58 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_config.py +64 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_migrate.py +1 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_runs.py +44 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_skill.py +49 -12
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_web_app.py +3 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/api/types.ts +12 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/StartFromAgent.tsx +2 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/crew.test.ts +5 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/crew.ts +3 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/launch.test.ts +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Launch.tsx +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Run.tsx +4 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/campaign/Runs.tsx +1 -1
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/launch/steps.tsx +14 -2
- {flyloft-0.2.1 → flyloft-0.2.2}/.github/workflows/python-publish.yaml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/.gitignore +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/.python-version +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/AGENTS.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/CLAUDE.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/README.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/NEW_TEMPLATE.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/VISION.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-1.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-2.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-3.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-4.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-5/campaign.html +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-5/campaigns.html +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-5/launch.html +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-5/pages.html +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/board-5.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/budget-billing.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/campaign-compute.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/compute/compute.html +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/compute.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/criterion-removal.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/flyloft-up.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/investigate.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/multi-user.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/new-project.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/projects.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/replication.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-1.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-2.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-3a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-3b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-4a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-4b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-5a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-5b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-5c.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-5d.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-6a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-6b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/stage-7.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/tier-one-lockstep.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/design/validation.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/environments/deeplearning_v2026-09-15.txt +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/agent-interface.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/board-1.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/board-2.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/board-3.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/board-4.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/board-5.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/budget-billing.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/campaign-compute.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/compute.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/criterion-removal.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/flyloft-up.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/investigate.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/multi-user-a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/multi-user-b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/new-project.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/projects.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/replication-a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/replication-b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/replication-c.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-1.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-2.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-3a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-3b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-4a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-4b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-5a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-5b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-5c.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-5d.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-6a.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-6b.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/stage-7.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/tasks.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/tier-one-lockstep.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/docs/plans/validation.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/flyloft.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/builder.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/claude.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/coordinator.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/first_campaign.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/generator.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/happy_path.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/integrator.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/lenient_critic.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/librarian.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/opencode.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/orchestrator.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/proximity.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/reporter.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/harnesses/strict_critic.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/projects/toy-live-4b.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/projects/toy-live-5a.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/examples/live-config/projects/toy-live.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/pyproject.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/budget/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/budget/compare.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/budget/ledger.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/cli/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/cli/main.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/client/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/client/api.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/client/handoff.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/client/render.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/config/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/config/projects.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/config/toml.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/github.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/host/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/host/doctor.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/host/harnesses/claude.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/host/harnesses/opencode.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/host/init.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/host/units.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/host/up.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/knowledge/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/knowledge/memory.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0001_initial.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0002_finding_state.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0003_several_implementers.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0004_entry_order_within_a_transaction.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0005_population.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0006_memory.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0007_islands.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0008_blocks.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0009_export.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0010_failures.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0011_unblock.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0012_indefinite.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0013_pull_requests.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0014_run_elapsed.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0015_campaign_types.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0016_island_aim.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0017_campaign_project_config.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0018_fresh_record.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0019_run_tree.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0020_users.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0021_token_refused.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0022_drop_criterion.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0023_campaign_compute.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0024_optional_cpus_mem.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0025_validation.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0026_measurement.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/migrations/0027_investigation.sql +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/_findings.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/_findings_critic_judged.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/_head.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/_head_critic_judged.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/_macros.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/body.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/export.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/gh.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/pr_body.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/promotion_pr.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/publisher.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/record.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/publication/record_pr.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/consistency.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/db.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/islands.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/migrate.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/researcher.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/submit.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/templates.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/users.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/validation.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/record/views.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/askpass.sh +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/capacity.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/client.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/collect.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/observe.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/runs/ssh.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/service/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/service/factory.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/skill/contract.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/tools/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/tools/__main__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/launch.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/_cj_critic_runs.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/_plan.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/bounded.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/coordinator.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/critic.investigate.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/critic.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/critic.task.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/direction.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/generator.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/implementer.investigate.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/implementer.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/implementer.task.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/integrator.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/librarian.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/memory.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/orchestrator.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/proximity.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/reporter.investigate.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/reporter.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/roles/reporter.task.md.j2 +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/substrates/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/substrates/claude_code.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/substrates/opencode.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/substrates/scripted.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/turns/wake.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/version.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/artifacts.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/auth.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/events.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/openapi.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/read.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/web/static/assets/index-y1dUR_aG.css +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/workspace/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/src/flyloft/workspace/git.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/fixtures/opencode-turn.jsonl +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_acting_as_owner.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_board1.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_board2.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_board3.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_board4.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_board5.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_campaign_compute.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_compute.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_projects.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_replication.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_replication_board.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage1.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage2.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage3.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage3b.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage4a.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage4b.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage5a.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage5b.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage5c.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage5d.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage6a.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage6b.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_stage7.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_users.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/e2e/test_validation.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fakes/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fakes/cluster.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fakes/github.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fakes/publisher.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fixtures/slinky/capacity.txt +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fixtures/slinky/cluster.toml +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fixtures/toyproject/BRIEF.md +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fixtures/toyproject/config.json +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/fixtures/toyproject/train.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/host/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/host/test_host.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/builder.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/common.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/coordinator.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/crash.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/crew.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/failing_job.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/first_campaign.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/flaky_builder.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/generator.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/happy_path.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/investigator_quiet.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/lenient_critic.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/librarian.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/missing_results.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/near_far.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/orchestrator.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/plateau.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/post_then_crash.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/proximity.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/replication.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/reporter.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/retracting.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/silent_exit.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/strict_critic.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/task_critic.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/scripts/tasker.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/__init__.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/conftest.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/island_helpers.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/memory_helpers.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_actions.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_assembly.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_budget.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_capacity.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_claude_code.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_cli.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_client.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_client_main.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_consistency.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_format_metrics.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_gh.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_github.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_host_doctor.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_host_init.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_host_units.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_host_up.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_investigation.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_islands.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_knowledge.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_launch.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_measurement.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_opencode.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_population.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_projects.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_publication.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_scripted.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_service.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_ssh.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_store.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_submit.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_tasks.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_templates.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_tools.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_toyproject.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_transcript.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_users.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_wake.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_web_artifacts.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_web_compare.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_web_events.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_web_read.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/test_workspace.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/tests/unit/web_helpers.py +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/uv.lock +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/components.json +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/index.html +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/package-lock.json +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/package.json +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/App.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/api/client.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/api/client.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/api/live.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/api/queries.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Actions.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/AnswerValue.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Breadcrumbs.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/CampaignCard.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/CampaignFilter.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ChangeMarkers.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/DotBanner.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/EntityLink.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ErrorNote.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/FinishedRow.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Freshness.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/InvestigationCard.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/LogList.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ProgressChart.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/RoleChips.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/RoleNode.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Selectable.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Sidebar.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Step.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Story.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/SwarmMap.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Timeline.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Timestamp.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/Transcript.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ui/alert-dialog.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ui/button.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ui/popover.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ui/sheet.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ui/tabs.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/components/ui/tooltip.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/campaign.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/campaign.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/chart.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/chart.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/compute.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/compute.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/format.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/format.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/launch.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/projects.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/roles.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/roles.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/runs.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/runs.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/seen.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/seen.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/story.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/story.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/swarm.test.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/swarm.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/lib/utils.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/main.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Admin.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Compare.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Compute.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Home.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Login.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Projects.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/Turn.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/admin/Host.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/admin/Users.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/campaign/Activity.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/campaign/Frame.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/campaign/Hypotheses.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/campaign/Overview.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/campaign/context.ts +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/project/NewProject.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/project/ProjectPage.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/pages/project/ValidationPanel.tsx +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/src/styles.css +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/tsconfig.json +0 -0
- {flyloft-0.2.1 → flyloft-0.2.2}/web/vite.config.ts +0 -0
|
@@ -86,4 +86,4 @@ jobs:
|
|
|
86
86
|
- uses: astral-sh/setup-uv@v6
|
|
87
87
|
- run: uv venv client-venv --python ${{ matrix.python }} && uv pip install --python client-venv/bin/python .
|
|
88
88
|
- run: client-venv/bin/flyloft --help
|
|
89
|
-
- run: client-venv/bin/flyloft install
|
|
89
|
+
- run: client-venv/bin/flyloft install --harness claude
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# flyloft Architecture
|
|
2
2
|
|
|
3
|
-
Status: approved 2026-09-21. Stage 1 complete on 2026-09-23 (see [design/stage-1.md](design/stage-1.md)); stage 2 complete on 2026-09-23 (tiers one, two, and three passed; see [design/stage-2.md](design/stage-2.md)); stage 3a complete (see [design/stage-3a.md](design/stage-3a.md)). Stage 3b complete (see [design/stage-3b.md](design/stage-3b.md)). Stage 4a complete on 2026-09-25 (tiers one, two, and three passed; see [design/stage-4a.md](design/stage-4a.md)). Stage 4b complete on 2026-09-25 (tiers one, two, and three passed; see [design/stage-4b.md](design/stage-4b.md)). Stage 5a complete on 2026-09-27 (tiers one, two, and three passed; see [design/stage-5a.md](design/stage-5a.md)). Stage 5b complete on 2026-09-27 (tiers one and three passed; see [design/stage-5b.md](design/stage-5b.md)). Stage 5c complete on 2026-09-28 (tiers one and three passed; see [design/stage-5c.md](design/stage-5c.md)). Stage 5d complete on 2026-09-28 (tiers one and three passed; see [design/stage-5d.md](design/stage-5d.md)). Stage 6a complete on 2026-09-29 (tiers one and three passed; see [design/stage-6a.md](design/stage-6a.md)). Stage 6b complete on 2026-09-30 (tiers one and three passed; see [design/stage-6b.md](design/stage-6b.md)). Stage 7 complete on 2026-09-30 (tiers one, two, and three passed; see [design/stage-7.md](design/stage-7.md)). Every staged part is built. The vision's revision of 2026-10-13 widens flyloft to two modes, investigate and optimize, with a lab-wide toolshed; what comes next is in §6, *What comes next*. The Projects rework's board flow and template repositories are built and closed on 2026-10-19 (tiers one, two, and three passed; see [design/projects.md](design/projects.md)); the investigate mode is next, in four parts, the first built (see [design/investigate.md](design/investigate.md)); then the agent interface, where the researcher's own coding agent starts work and the board is where they come back to (see [design/agent-interface.md](design/agent-interface.md)).
|
|
3
|
+
Status: approved 2026-09-21. Stage 1 complete on 2026-09-23 (see [design/stage-1.md](design/stage-1.md)); stage 2 complete on 2026-09-23 (tiers one, two, and three passed; see [design/stage-2.md](design/stage-2.md)); stage 3a complete (see [design/stage-3a.md](design/stage-3a.md)). Stage 3b complete (see [design/stage-3b.md](design/stage-3b.md)). Stage 4a complete on 2026-09-25 (tiers one, two, and three passed; see [design/stage-4a.md](design/stage-4a.md)). Stage 4b complete on 2026-09-25 (tiers one, two, and three passed; see [design/stage-4b.md](design/stage-4b.md)). Stage 5a complete on 2026-09-27 (tiers one, two, and three passed; see [design/stage-5a.md](design/stage-5a.md)). Stage 5b complete on 2026-09-27 (tiers one and three passed; see [design/stage-5b.md](design/stage-5b.md)). Stage 5c complete on 2026-09-28 (tiers one and three passed; see [design/stage-5c.md](design/stage-5c.md)). Stage 5d complete on 2026-09-28 (tiers one and three passed; see [design/stage-5d.md](design/stage-5d.md)). Stage 6a complete on 2026-09-29 (tiers one and three passed; see [design/stage-6a.md](design/stage-6a.md)). Stage 6b complete on 2026-09-30 (tiers one and three passed; see [design/stage-6b.md](design/stage-6b.md)). Stage 7 complete on 2026-09-30 (tiers one, two, and three passed; see [design/stage-7.md](design/stage-7.md)). Every staged part is built. The vision's revision of 2026-10-13 widens flyloft to two modes, investigate and optimize, with a lab-wide toolshed; what comes next is in §6, *What comes next*. The Projects rework's board flow and template repositories are built and closed on 2026-10-19 (tiers one, two, and three passed; see [design/projects.md](design/projects.md)); the investigate mode is next, in four parts, the first built (see [design/investigate.md](design/investigate.md)); then the agent interface, where the researcher's own coding agent starts work and the board is where they come back to (see [design/agent-interface.md](design/agent-interface.md)). A critic-judged campaign's 0-GPU runs go to its cluster's CPU partition, sized within a CPU-run limit (see [design/cpu-partition.md](design/cpu-partition.md)).
|
|
4
4
|
|
|
5
5
|
This document settles the shape of flyloft: the record that holds its state, the lifecycle and authority rules, the agent turn, the subsystem boundaries, and the stages by which the system grows. It fixes what is expensive to change later and leaves everything else to the design document of the stage that needs it. [VISION.md](VISION.md) says what flyloft is for; [AGENTS.md](../AGENTS.md) says how to work on it. This document is the bridge between them.
|
|
6
6
|
|
|
@@ -197,7 +197,7 @@ Two processes write to the record from the first turn, the service and the tool
|
|
|
197
197
|
|
|
198
198
|
**The Compute page.** After campaign-level compute, its own change ([design/compute.md](design/compute.md)): a sidebar page where each cluster says what each GPU type its file lists has (free GPUs, flyloft's running and queued jobs, nodes, CPUs, memory, longest run, and most free on a node), cached for 30 seconds and asked per cluster over the runs module's SSH client; the launch page's GPU type select shows each type's free count.
|
|
199
199
|
|
|
200
|
-
**What comes next.** The vision's revision of 2026-10-13 widens flyloft from optimization to two modes, investigate and optimize, and adds a toolshed shared across the lab. Stages 1 through 7 built the optimize mode. What follows is staged like everything before it, each part with its own scenario and a design document written when it begins: the Projects rework, which makes bringing a project one flow on the board and turns templates into GitHub template repositories registered with flyloft (see [design/projects.md](design/projects.md)); then the investigate mode, in four parts: a solo, exploratory investigation whose project may be only a brief ([design/investigate.md](design/investigate.md)); a test fixed in advance, making findings confirmed; the toolshed; and investigation at swarm size. Then the agent interface ([design/agent-interface.md](design/agent-interface.md)): the researcher's coding agent is flyloft's front door (sign-in from a laptop, `submit` of a brief that flyloft turns into a repository and a campaign, a `flyloft` client with every initiating action, a skill), the host's commands move under `flyloft host` and admin actions under `flyloft admin`, and the board becomes where people monitor and steer; its second part adds tasks, a third label on the investigation engine ([design/tasks.md](design/tasks.md)). Their mechanics are decided in their design documents, not here.
|
|
200
|
+
**What comes next.** The vision's revision of 2026-10-13 widens flyloft from optimization to two modes, investigate and optimize, and adds a toolshed shared across the lab. Stages 1 through 7 built the optimize mode. What follows is staged like everything before it, each part with its own scenario and a design document written when it begins: the Projects rework, which makes bringing a project one flow on the board and turns templates into GitHub template repositories registered with flyloft (see [design/projects.md](design/projects.md)); then the investigate mode, in four parts: a solo, exploratory investigation whose project may be only a brief ([design/investigate.md](design/investigate.md)); a test fixed in advance, making findings confirmed; the toolshed; and investigation at swarm size. Then the agent interface ([design/agent-interface.md](design/agent-interface.md)): the researcher's coding agent is flyloft's front door (sign-in from a laptop, `submit` of a brief that flyloft turns into a repository and a campaign, a `flyloft` client with every initiating action, a skill), the host's commands move under `flyloft host` and admin actions under `flyloft admin`, and the board becomes where people monitor and steer; its second part adds tasks, a third label on the investigation engine ([design/tasks.md](design/tasks.md)); both are built and closed (2026-10-22). Their mechanics are decided in their design documents, not here.
|
|
201
201
|
|
|
202
202
|
Recovery is built when a stage loses work, most likely stage three, when a service restart first interrupts many turns at once. It is not scheduled.
|
|
203
203
|
|
|
@@ -382,12 +382,16 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
|
|
|
382
382
|
| An "Empty project" choice on New project that writes the brief on the board and creates the repository | Superseded by `flyloft submit`: see [design/agent-interface.md](design/agent-interface.md) |
|
|
383
383
|
| The toolshed (part 3 of investigate) and a data equivalent: where the lab's datasets live, how they are versioned, what has been fetched and prepared | Near-term, after the agent interface; each its own design |
|
|
384
384
|
| A Claude Code plugin and marketplace for the flyloft skill (the skill's directory plus a manifest) | When a lab member wants it from a marketplace, or a hook or second component is needed |
|
|
385
|
-
| The flyloft skill for Codex and OpenCode |
|
|
385
|
+
| The flyloft skill for Codex and OpenCode | Done 2026-10-22: `flyloft install` serves every harness found |
|
|
386
386
|
| `flyloft logout`, several hosts per laptop, refreshing a session before it expires | When one is missed |
|
|
387
387
|
| Deleting the board's initiation flows (New project, registration, the launch wizard) and the database-direct `flyloft host campaign` and `host project` commands | When the agent interface is seen to cover what people and operators use them for |
|
|
388
388
|
| flyloft as a monitor and troubleshooter of the lab's compute, from what it already observes (node and job states, failure diagnostics, capacity) | When the agent interface and tasks are in use |
|
|
389
389
|
| HTTPS for the board and the client (a 30-day session crosses the intranet in the clear: an accepted risk for the first version, see [design/agent-interface.md](design/agent-interface.md) §3) | When the host leaves netbird or a lab policy asks |
|
|
390
390
|
| An MCP catalogue of knowledge sources (bioRxiv, PubMed, ChEMBL) for flyloft's own agents: per-role permissions, credentials, and what a source returned on the record; with the toolshed, in one lab-owned repository | With the toolshed, when the first investigation needs a source its agents cannot reach |
|
|
391
|
+
| A CPU row on the Compute page and in `flyloft options` ([design/cpu-partition.md](design/cpu-partition.md) §6) | When a launch goes wrong for want of it |
|
|
392
|
+
| A CPU budget ([design/cpu-partition.md](design/cpu-partition.md) §6) | When CPU work on the shared node needs a bound beyond run slots and the CPU-run limit |
|
|
393
|
+
| Choosing a GPU type per run within a campaign; budgeting must then weigh the types ([design/cpu-partition.md](design/cpu-partition.md) §6) | When a campaign needs two GPU types |
|
|
394
|
+
| The request and the render of a CPU run disagreeing when `cpu_partition` changes while a run is preparing; the fix is to key `_is_cpu_run` on `run.cpus is not None` | When it is observed |
|
|
391
395
|
|
|
392
396
|
## References
|
|
393
397
|
|
|
@@ -81,7 +81,7 @@ only its brief: the agents write everything else on the campaign branch. The bri
|
|
|
81
81
|
- what agents must not do (no training on a held-out set, no more than one GPU per run).
|
|
82
82
|
|
|
83
83
|
The usual path is `flyloft submit` from the researcher's own agent: on the researcher's laptop, `pip install flyloft`,
|
|
84
|
-
`flyloft login --host <board URL>`, and `flyloft install
|
|
84
|
+
`flyloft login --host <board URL>`, and `flyloft install` give the agent (Claude Code, Codex, or OpenCode) the flyloft skill,
|
|
85
85
|
whose `handoff.md` holds the brief template. The agent writes the brief and an investigation or task file, runs
|
|
86
86
|
`flyloft submit <directory> --name <name> --campaign <file> --check` until it reports no problem, and submits: flyloft
|
|
87
87
|
creates the repository under the lab's organization, registers the project, and launches the campaign. A task is submitted the same way: `task.toml` with `type = "task"`, and the brief says what needs doing and what done looks like.
|
|
@@ -24,7 +24,7 @@ VISION.md was revised with the human section by section on 2026-10-20: the openi
|
|
|
24
24
|
|
|
25
25
|
## 2. Scenario
|
|
26
26
|
|
|
27
|
-
*Once per researcher.* `pip install flyloft` on the laptop; `flyloft install
|
|
27
|
+
*Once per researcher.* `pip install flyloft` on the laptop; `flyloft install` (the skill, into every harness found: Claude Code, Codex, OpenCode); `flyloft login --host http://<board>`: a device code appears, they approve on GitHub, and the client prints "signed in to flyloft as briney". Someone not on the allowlist is refused with who to ask.
|
|
28
28
|
|
|
29
29
|
*The slow cooker.*
|
|
30
30
|
|
|
@@ -57,7 +57,7 @@ The laptop holds only a flyloft session, never a GitHub token. The GitHub token
|
|
|
57
57
|
|
|
58
58
|
**Transport.** The host's network is the lab's intranet, and the board stays plain HTTP: an accepted risk for the first version, since a 30-day session crosses that network in the clear. HTTPS is on the deferred list, to be picked up when the host leaves netbird or a lab policy asks.
|
|
59
59
|
|
|
60
|
-
**Versions.** Every request carries `X-Flyloft-Client: <version>` and every reply `X-Flyloft-Host: <version>`, both the one package's version. The host holds `MIN_CLIENT`, bumped whenever the API changes incompatibly; an older client gets 426 with "your flyloft client is 1.4.0; this host needs 1.6.0 or newer: `pip install -U flyloft`", which the skill treats as an instruction. A client newer than the host prints one warning line ("this host runs 1.5.0; some commands may not work") and continues; the API rejects unknown request fields rather than dropping them, so a new field never vanishes quietly. `install
|
|
60
|
+
**Versions.** Every request carries `X-Flyloft-Client: <version>` and every reply `X-Flyloft-Host: <version>`, both the one package's version. The host holds `MIN_CLIENT`, bumped whenever the API changes incompatibly; an older client gets 426 with "your flyloft client is 1.4.0; this host needs 1.6.0 or newer: `pip install -U flyloft`", which the skill treats as an instruction. A client newer than the host prints one warning line ("this host runs 1.5.0; some commands may not work") and continues; the API rejects unknown request fields rather than dropping them, so a new field never vanishes quietly. `install` writes the client's version into `SKILL.md`, and the client warns once per command when the installed skill's version differs ("run `flyloft install`"). No compatibility framework beyond this: one supported pairing, the newest of each.
|
|
61
61
|
|
|
62
62
|
## 4. `submit` on the host
|
|
63
63
|
|
|
@@ -101,7 +101,7 @@ The reply carries the project, the repository URL, the campaign id, and the boar
|
|
|
101
101
|
| `stop ID` | `POST /api/campaigns/{id}/stop` |
|
|
102
102
|
| `admin user list / add LOGIN [--admin] / remove LOGIN / promote LOGIN / demote LOGIN` | `GET/POST/PATCH/DELETE /api/users…`, 403 for a non-admin |
|
|
103
103
|
| `admin user logout LOGIN` | `DELETE /api/users/{login}/sessions` |
|
|
104
|
-
| `install
|
|
104
|
+
| `install [--harness claude\|codex\|opencode]` | copies the packaged skill, with the client's version written in, to `~/.claude/skills/flyloft/` for Claude Code and `~/.agents/skills/flyloft/` for Codex and OpenCode (both read it); without `--harness`, every harness whose CLI is on `PATH`; overwriting |
|
|
105
105
|
|
|
106
106
|
`status` with no id lists the researcher's campaigns, one line each (state, kind, project, what is running or the latest finding, budget); with an id it shows the experiments or runs, the findings, the budget, the notes, and the account once written.
|
|
107
107
|
|
|
@@ -120,12 +120,12 @@ The client ships in the existing `flyloft` package. The researcher's commands ar
|
|
|
120
120
|
5. **Checking in**: `status`, then `status ID`, leading with the account when there is one. `steer` and `stop` only when asked.
|
|
121
121
|
6. **Graduating**, when an account recommends an optimization and the researcher wants it: the investigation's code is on its branch behind a draft pull request, so have the researcher review and merge it first; then clone, wire the contract, push, `project set`, `validate` (read the report; fix what it names), `launch` with the metric and target the account named.
|
|
122
122
|
7. **Bringing an existing repository**: the contract, `register`, `validate`, `launch`.
|
|
123
|
-
8. **Refusals** are the host's own sentences: fix what they name and retry; never work around one, such as by creating a repository by hand. A 426 means the client is too old: run `pip install -U flyloft` and retry; a skill-version warning means `flyloft install
|
|
123
|
+
8. **Refusals** are the host's own sentences: fix what they name and retry; never work around one, such as by creating a repository by hand. A 426 means the client is too old: run `pip install -U flyloft` and retry; a skill-version warning means `flyloft install`.
|
|
124
124
|
9. **Administering**, only when the researcher is an admin and asks: `flyloft admin user …`; a 403 means they are not.
|
|
125
125
|
|
|
126
126
|
## 7. The board, `flyloft host`, and `flyloft admin`
|
|
127
127
|
|
|
128
|
-
The board shows every project and campaign and lets people steer and stop. This part adds a "Start new work from your agent" panel under the home page's campaign list (also its empty state): `pip install flyloft`, `flyloft login --host <this board's URL>`, `flyloft install
|
|
128
|
+
The board shows every project and campaign and lets people steer and stop. This part adds a "Start new work from your agent" panel under the home page's campaign list (also its empty state): `pip install flyloft`, `flyloft login --host <this board's URL>`, `flyloft install`, with the URL filled in; the launch page's first line points to it. The Admin page gains "end every session" per user. New project, registration, and the launch wizard stay as they are.
|
|
129
129
|
|
|
130
130
|
`flyloft host` holds every host-side operator command, unchanged in behavior: `serve`, `web`, `up`, `init`, `doctor`, `db migrate`, host `status`, the operator's `login`, the database-direct `campaign` and `project` groups, and `user`, which is the bootstrap: `flyloft host user add --admin LOGIN` makes the first admin on a fresh host, before anyone can sign in; afterwards admins use `flyloft admin user …` over the API, from anywhere. `flyloft admin` holds only what needs the admin role.
|
|
131
131
|
|
|
@@ -135,7 +135,7 @@ The board shows every project and campaign and lets people steer and stop. This
|
|
|
135
135
|
|
|
136
136
|
**Tier one** (`tests/e2e/test_agent_interface.py`: the fake cluster, the stand-in GitHub with device endpoints and organization repositories, scripted agents; the client through `CliRunner` against the board app served over HTTP, as the browser tests serve it):
|
|
137
137
|
|
|
138
|
-
- `login` (the stand-in approves; a `slow_down` is honored), `options`, `install
|
|
138
|
+
- `login` (the stand-in approves; a `slow_down` is honored), `options`, `install` into a temporary home, `submit --check` (every problem at once, the manifest), `submit` of a brief and plan: the stand-in holds `brineylab/<name>` created with the researcher's token, the implementer's assignment carries the plan, and the scripted investigator runs it to an account that `status` shows; `steer` and `stop` on a second submission; `logout`, after which the next command is a 401.
|
|
139
139
|
- Graduation: merge the investigation's pull request on the stand-in, clone, add the toy project's run contract, push; `project set`, `validate`, `launch` an optimization run by the scripted crew.
|
|
140
140
|
- `register` of the toy repository, `validate`, `launch`.
|
|
141
141
|
- Refusals, each leaving nothing on the stand-in GitHub: no brief; a refused campaign file; a name taken (the refusal names the existing project and campaign); a researcher not on the allowlist; an expired session; an optimization on a brief-only project; a `.env` in the directory (not sent), a tracked file holding a `ghp_` token (refused with its line), a symlink (not followed), a 25 MB file; a client below `MIN_CLIENT` (426 with the upgrade line); a client newer than the host (one warning line, then the output).
|
|
@@ -143,11 +143,11 @@ The board shows every project and campaign and lets people steer and stop. This
|
|
|
143
143
|
- The board's panel shows this board's URL, and the Admin page ends a user's sessions (browser).
|
|
144
144
|
- The skill: every command and flag it names exists in the client, and its text states the budget's overshoot and names `submit --check` and the merge-before-graduation step.
|
|
145
145
|
- The host's commands under `host`: the existing suite passes with its invocations moved; a unit test writes units with the old names, upgrades, and finds them rewritten and the aliases answering.
|
|
146
|
-
- CI installs the package without extras and runs `flyloft --help`, `flyloft install
|
|
146
|
+
- CI installs the package without extras and runs `flyloft --help`, `flyloft install`, and the client's unit tests, so a server import in the client fails there, not on a researcher's laptop.
|
|
147
147
|
|
|
148
148
|
**Tier two** (live, no model tokens): `submit` against real GitHub creates a private `brineylab/flyloft-tier2-<date>` (the human deletes it afterward: flyloft's token has no `delete_repo`); a scripted investigator runs one job on slinky; `status` reads it back. The session is created on the host, since `login` needs a person at GitHub.
|
|
149
149
|
|
|
150
|
-
**Tier three:** the human, with Claude Code: `pip install flyloft`, `install
|
|
150
|
+
**Tier three:** the human, with Claude Code: `pip install flyloft`, `install`, `login`, the ESM-C conversation, `submit`, and `status` from the harness days later. It is investigate part 1's tier three as well.
|
|
151
151
|
|
|
152
152
|
## 9. Part 2: tasks
|
|
153
153
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# The CPU Partition: Design
|
|
2
|
+
|
|
3
|
+
Status: approved 2026-10-22; widened the same day to tasks after [tasks.md](tasks.md) merged; built on branch `feature/cpu-partition`. Tier one passed (625 passed, 9 skipped; two `test_client` failures are environmental on the build host). Tier two passed on bcr: the setup run went to partition `cpu` with `cpu=8, mem=16G` and no GPU; the GPU run went to `l40s` with its share of the node (`cpu=48, gres/gpu=1, mem=179G`) and imported the environment the CPU run built. Follows investigate, part 1 ([investigate.md](investigate.md)) and tasks ([tasks.md](tasks.md)): it sends the 0-GPU runs of both critic-judged types to a cluster's CPU nodes.
|
|
4
|
+
|
|
5
|
+
Throughout, *critic-judged* means an investigation or a task (`Campaign.critic_judged`), the two types whose implementer sizes its own runs. Optimize campaigns give every run the campaign's resources and at least one GPU, so they have no 0-GPU runs and nothing here touches them.
|
|
6
|
+
|
|
7
|
+
A critic-judged campaign's implementer already sizes each run: its command, GPUs from zero to the campaign's ceiling, and time limit. A run that asks for no GPUs is a setup run, but it still goes to the partition of the campaign's GPU type and gets the campaign's CPUs and memory. On a GPU partition with no CPUs set, that is one CPU. That is useless for real data work, and it takes CPUs and memory from GPU nodes.
|
|
8
|
+
|
|
9
|
+
This change sends a no-GPU run to the cluster's CPU partition, when the cluster names one. It gives such a run its own CPUs and memory, within a CPU-run limit the researcher sets at launch. GPU runs are untouched. Their CPUs and memory stay what the campaign names, or their share of the node, so that a GPU job never takes the CPUs its node's other GPUs need.
|
|
10
|
+
|
|
11
|
+
## 1. Scenario
|
|
12
|
+
|
|
13
|
+
The researcher retests curriculum learning for antibody language models on held-out donors. The paired data is an existing release. The unpaired data must be rebuilt first: a fresh download of OAS unpaired sequences, filtered, held-out donors removed, and clustered at 95% with mmseqs `linclust`. That is a CPU job of hours that needs on the order of 100 CPUs and several hundred GB of memory.
|
|
14
|
+
|
|
15
|
+
1. **The cluster names its CPU partition.** bcr's file has `cpu_partition = "cpu"`: one node, 256 CPUs, about 1 TB, shared by the lab.
|
|
16
|
+
2. **The researcher launches an investigation** on bcr:
|
|
17
|
+
- GPU type `l40s`, each run up to 4 GPUs for 48 hours;
|
|
18
|
+
- each CPU run up to 128 CPUs and 384G, half the CPU node.
|
|
19
|
+
3. **The implementer builds the data first.** It commits the build code and requests a setup run with 0 GPUs, 96 CPUs, 256G and 24 hours.
|
|
20
|
+
- The job goes to `cpu` with those numbers.
|
|
21
|
+
- It writes the release under `$FLYLOFT_PROJECT_WORK/data/` and a summary to `$FLYLOFT_OUT`.
|
|
22
|
+
- It is charged nothing against the GPU-hour budget, and it holds one run slot while it runs.
|
|
23
|
+
4. **Then it trains.** Its training runs ask for GPUs and go to `l40s` exactly as today, with the campaign's CPUs and memory or the cluster's share for each GPU.
|
|
24
|
+
5. **Refusals:**
|
|
25
|
+
- a CPU run asking for 200 CPUs, or for 512G, is refused with the limit it exceeded;
|
|
26
|
+
- so is a CPU run that names no CPUs or memory;
|
|
27
|
+
- so is a GPU run that names CPUs or memory.
|
|
28
|
+
|
|
29
|
+
## 2. What changes
|
|
30
|
+
|
|
31
|
+
**The cluster file.** `ClusterConfig` gains `cpu_partition: str | None`.
|
|
32
|
+
- It differs from `partition`, which is the fallback for every job whose GPU type names none. `cpu_partition` is only for jobs that ask for no GPUs.
|
|
33
|
+
- A cluster without one behaves as today.
|
|
34
|
+
- bcr's file is `clusters/bcr.toml` in the lab's `brineylab/flyloft-config` repository. It gains `cpu_partition = "cpu"` in a pull request there once this change is deployed, not in this repository.
|
|
35
|
+
- `examples/live-config/clusters/slinky.toml` gains a commented `cpu_partition` line, and the misleading comment on its `partition` line is corrected.
|
|
36
|
+
|
|
37
|
+
**The CPU node runs the same jobs.** On bcr the `cpu` node is x86_64 like the GPU nodes. It mounts the same `/mnt/data`, where `remote_work_dir` and the project work directory live, and every partition has the same images under `/mnt/data/containers/`. So:
|
|
38
|
+
- a CPU run takes the cluster's one `container_image` and `container_mounts`;
|
|
39
|
+
- an environment a setup run builds on `cpu` serves a GPU run after it.
|
|
40
|
+
|
|
41
|
+
The existing tier-two test `test_live_an_investigation_sizes_its_runs_and_keeps_its_environment` checks exactly that, and once the live config names `cpu_partition` it exercises this path. Per-partition containers are not needed.
|
|
42
|
+
|
|
43
|
+
**The CPU-run limit.**
|
|
44
|
+
- **Where it is set:** the campaign file's `resources` gains `cpu_run_cpus: int | None` and `cpu_run_mem: str | None`, stored on the campaign by migration `0028_cpu_runs.sql`. They are the most one CPU run may ask for. They are separate from `cpus` and `mem`, which keep their meaning for every GPU run.
|
|
45
|
+
- **`validate_campaign`:**
|
|
46
|
+
- refuses either field on a campaign that is not critic-judged ("only an investigation or a task takes a CPU-run limit");
|
|
47
|
+
- refuses either field on a cluster with no `cpu_partition`;
|
|
48
|
+
- refuses a `cpu_run_cpus` below 1;
|
|
49
|
+
- refuses a `cpu_run_mem` that does not parse.
|
|
50
|
+
- **Left empty:** both are optional. Left empty, a CPU run may ask for whatever Slurm will grant on the node.
|
|
51
|
+
|
|
52
|
+
**A CPU run's request.** `RunRequestBody` gains `cpus: int | None` and `mem: str | None`, and the migration adds nullable `run.cpus`, `run.mem` and `run.partition`. In `_request_investigation_run`:
|
|
53
|
+
- **A run with 0 GPUs on a cluster with a `cpu_partition` is a CPU run.**
|
|
54
|
+
- It must name `cpus` (at least 1) and `mem`.
|
|
55
|
+
- `mem` is in Slurm's form: a whole number with an optional `K`, `M`, `G` or `T`, megabytes when bare.
|
|
56
|
+
- Each is at most the campaign's CPU-run limit when one is set. Memory is compared in megabytes.
|
|
57
|
+
- **A run with GPUs, or a 0-GPU run on a cluster without a `cpu_partition`, may not name `cpus` or `mem`.** It gets the campaign's, as every run does today.
|
|
58
|
+
|
|
59
|
+
**Where a run goes.** `render_job_script` sends a CPU run to `cpu_partition` with its own `--cpus-per-task` and `--mem`. Every other job is rendered exactly as today. Only a critic-judged campaign's runs can ask for no GPUs, so optimize campaigns, validations and measurements are unchanged. The partition a run was submitted to is stored on the run (`run.partition`, written by the service when it submits). The board then reports where the run went, even if the cluster's file changes later.
|
|
60
|
+
|
|
61
|
+
**The agent's tool.** `investigation_request_run` gains optional `cpus` and `mem`. Its description says:
|
|
62
|
+
- a run with 0 GPUs goes to the cluster's CPU nodes when it has them, and then names its CPUs and memory;
|
|
63
|
+
- a GPU run names neither.
|
|
64
|
+
|
|
65
|
+
The implementer's assignment, through the shared `_cj_implementer_rules.md.j2` and `_cj_record.md.j2` partials used by both types, shows the CPU-run limit beside the GPU and time ceilings, when the cluster has a CPU partition. It says that data work belongs on a CPU run, sized to what it needs.
|
|
66
|
+
|
|
67
|
+
**The budget.** Unchanged. A run is charged its own GPUs times its time, so a CPU run costs nothing. Run slots and the CPU-run limit are the only bounds on CPU work. As today, no run, a CPU run included, is accepted once the GPU-hour budget is spent: the investigation ends at its budget anyway.
|
|
68
|
+
|
|
69
|
+
## 3. The board
|
|
70
|
+
|
|
71
|
+
- **The host's API.** `ClusterView` gains `cpu_partition`, so the launch page knows whether to offer a CPU-run limit.
|
|
72
|
+
- **Launch.** When the chosen cluster has a CPU partition, an investigation's or a task's Compute step adds "each CPU run may ask for up to" CPUs and memory, both optional. The existing CPUs and memory fields keep their meaning for GPU runs.
|
|
73
|
+
- **The run and its page.** A CPU run shows its request as CPUs, memory and time, "on cpu". A GPU run is shown as today. `npm run types` regenerates `web/src/api/types.ts`.
|
|
74
|
+
|
|
75
|
+
## 4. Tests
|
|
76
|
+
|
|
77
|
+
**Tier one** (`tests/e2e/test_investigate.py`, extended). The fake cluster's file names a `cpu_partition`, and the campaign sets a CPU-run limit.
|
|
78
|
+
- **A CPU run:** the scripted investigator requests a 0-GPU setup run with its own CPUs and memory. Its job script has the CPU partition, `--cpus-per-task` and `--mem`, and no `--gres`.
|
|
79
|
+
- **A GPU run in the same campaign** renders exactly as before: the GPU type's partition and the campaign's CPUs and memory.
|
|
80
|
+
- **Refusals, each with its reason:**
|
|
81
|
+
- a CPU run over the CPU limit, and one over the memory limit;
|
|
82
|
+
- a CPU run that names no CPUs or memory;
|
|
83
|
+
- a GPU run that names CPUs.
|
|
84
|
+
- **Campaign files that are refused:** a CPU-run limit on an optimize campaign, and one on a cluster without a `cpu_partition`.
|
|
85
|
+
- **A cluster without a `cpu_partition`:** a 0-GPU run renders as today.
|
|
86
|
+
- **A task with a 0-GPU ceiling** on the cluster with a CPU partition: its run names its CPUs and memory, goes to the CPU partition, and is charged nothing. The existing task scenario (`tests/e2e/test_tasks.py`, cluster `local`) passes unchanged.
|
|
87
|
+
- **In the browser:** launching on that cluster offers the CPU-run fields, and the run page shows the CPU run's request.
|
|
88
|
+
|
|
89
|
+
**A unit test** for the memory parser and comparison: `512`, `8G`, `384G`, `1T`, and refusals of `8GB`, `1.5G` and `-1G`.
|
|
90
|
+
|
|
91
|
+
**Tier two** (live on slinky, scripted, no model tokens). An investigation on bcr with a CPU-run limit of 16 CPUs and 32G requests a 0-GPU setup run with 8 CPUs and 16G that writes `nproc` and the job's memory limit to `$FLYLOFT_OUT`.
|
|
92
|
+
- Checked: `sacct` shows it ran on `cpu` with 8 CPUs and 16G, and its `out/` came back.
|
|
93
|
+
- The test skips with a reason when the live config's cluster names no `cpu_partition`. The tester's live config gains `cpu_partition = "cpu"`.
|
|
94
|
+
|
|
95
|
+
**Tier three** is the curriculum retest's data build.
|
|
96
|
+
|
|
97
|
+
## 5. Code layout
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
src/flyloft/migrations/0028_cpu_runs.sql campaign.cpu_run_cpus, cpu_run_mem; run.cpus, mem, partition
|
|
101
|
+
src/flyloft/config/models.py ClusterConfig.cpu_partition; ResourceProfile cpu_run_cpus,
|
|
102
|
+
cpu_run_mem; Slurm memory parsing
|
|
103
|
+
src/flyloft/config/load.py the CPU-run limit's rules
|
|
104
|
+
src/flyloft/record/models.py RunRequestBody and Run cpus, mem; Campaign's CPU-run limit
|
|
105
|
+
src/flyloft/record/launch.py, record/store.py store the CPU-run limit; insert and read run.cpus, mem, partition
|
|
106
|
+
src/flyloft/record/actions.py a CPU run's request and its limit
|
|
107
|
+
src/flyloft/runs/staging.py a CPU run to cpu_partition with its own cpus, mem
|
|
108
|
+
src/flyloft/service/tick.py record the partition a run was submitted to
|
|
109
|
+
src/flyloft/tools/server.py investigation_request_run takes cpus, mem
|
|
110
|
+
src/flyloft/turns/roles/implementer.investigate.md.j2 the CPU-run limit; data work on a CPU run
|
|
111
|
+
src/flyloft/web/schemas.py, record/views.py ClusterView.cpu_partition; a CPU run's request and partition
|
|
112
|
+
web/src/... the launch's CPU-run fields and the run's request
|
|
113
|
+
examples/live-config/clusters/slinky.toml cpu_partition, and the partition comment corrected
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 6. Out of scope
|
|
117
|
+
|
|
118
|
+
- **Choosing a GPU type per run** within a campaign. It would change budgeting (a B200-hour is not an L40S-hour), and this scenario trains on one type.
|
|
119
|
+
- **Resizing a GPU run's CPUs or memory.** A GPU job keeps the campaign's values or its share of the node, so it never takes CPUs its node's other GPUs need.
|
|
120
|
+
- **A separate CPU-run limit per type.** Investigations and tasks share one rule.
|
|
121
|
+
- **A CPU row on the Compute page and in `flyloft options`.** The researcher sets the limit from what they know of the node; added when a launch goes wrong for want of it.
|
|
122
|
+
- **A CPU budget.** Run slots and the CPU-run limit bound CPU work, at the researcher's call.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tasks: Design
|
|
2
2
|
|
|
3
|
-
Status: approved 2026-10-21; built the same day on branch `tasks`. Tier one passed (599 passed, 9 skipped; the task scenario is `tests/e2e/test_tasks.py`). No tier two (§6). Tier three,
|
|
3
|
+
Status: approved 2026-10-21; built the same day on branch `tasks`. Tier one passed (599 passed, 9 skipped; the task scenario is `tests/e2e/test_tasks.py`). No tier two (§6). Tier three passed on 2026-10-22: `v0.2.1` released and deployed, a task talked through in Claude Code with the flyloft skill, submitted, and run. Closed. Part 2 of the agent interface ([agent-interface.md](agent-interface.md) §9 decided its shape); the third kind of work in [VISION.md](../VISION.md).
|
|
4
4
|
|
|
5
5
|
A *task* is work that needs doing, whose product is its output: benchmark these five models on this suite, rerun the analysis a reviewer asked for, process this dataset into that format. There is no hypothesis and nothing to judge worth pursuing. The agents do all of what was asked and nothing beyond it, and the product is the output, reported faithfully with the runs that produced it. A task runs on the investigation engine built in part 1 of investigate: one implementer that sizes its own runs in the project work directory, one critic, one reporter, a brief-only project, and `submit` from the researcher's agent. What differs is the framing the three roles are given, how a task completes, and the words the board and the pull request use.
|
|
6
6
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# The CPU Partition Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** An investigation's run that asks for no GPUs goes to its cluster's CPU partition, when the cluster names one, with CPUs and memory the implementer sizes within a CPU-run limit set at launch. GPU runs are unchanged.
|
|
6
|
+
|
|
7
|
+
**Architecture:**
|
|
8
|
+
- `ClusterConfig.cpu_partition` names the partition.
|
|
9
|
+
- The campaign file's `resources` gains an optional CPU-run limit, stored on the campaign. A 0-GPU run on such a cluster is a *CPU run*: its request carries `cpus` and `mem`, the rules in `record/actions.py` check them against the limit, and `render_job_script` sends it to `cpu_partition` with its own numbers.
|
|
10
|
+
- The service records the partition each run was submitted to. The board offers the limit at launch and shows a CPU run's request.
|
|
11
|
+
|
|
12
|
+
**Tech Stack:** Python 3.12, psycopg, pydantic v2, Jinja2, MCP Python SDK, pytest (tier one: fake cluster, scripted substrate; tier two: slinky), React/TypeScript board with Playwright.
|
|
13
|
+
|
|
14
|
+
**Spec:** `docs/design/cpu-partition.md`
|
|
15
|
+
|
|
16
|
+
## Global Constraints
|
|
17
|
+
|
|
18
|
+
- **Where the work happens:** branch `feature/cpu-partition`, which holds the approved design.
|
|
19
|
+
- **The term "CPU run":** an investigation's run with `gpus == 0` whose cluster has a `cpu_partition`. Nothing else is one.
|
|
20
|
+
- **Field names, exactly:**
|
|
21
|
+
- `ClusterConfig.cpu_partition`;
|
|
22
|
+
- `ResourceProfile.cpu_run_cpus`, `ResourceProfile.cpu_run_mem`;
|
|
23
|
+
- campaign columns `cpu_run_cpus`, `cpu_run_mem`;
|
|
24
|
+
- run columns `cpus`, `mem`, `partition`;
|
|
25
|
+
- `RunRequestBody.cpus`, `RunRequestBody.mem`;
|
|
26
|
+
- tool arguments `cpus`, `mem`;
|
|
27
|
+
- `ClusterView.cpu_partition`.
|
|
28
|
+
- **The migration:** `src/flyloft/migrations/0028_cpu_runs.sql` adds nullable `campaign.cpu_run_cpus integer`, `campaign.cpu_run_mem text`, `run.cpus integer`, `run.mem text` and `run.partition text`. Nothing else in the schema changes.
|
|
29
|
+
- **Slurm memory form:** a positive whole number with an optional suffix `K`, `M`, `G` or `T` (either case), megabytes when bare. K is 1/1024 MB, G is 1024 MB, T is 1024² MB. Anything else is refused with "write memory as Slurm does, such as 16G or 512M".
|
|
30
|
+
- **GPU runs, optimize campaigns, validations and measurements render exactly as before.** Existing job-script tests must pass unchanged.
|
|
31
|
+
- **Every rule lives in `record/actions.py` and `config/load.py`.** The tool server only translates arguments.
|
|
32
|
+
- **Commits:** one sentence stating the behavior, in the style of `git log`. **No `Co-Authored-By` trailer or any other agent attribution** (the repository owner's rule overrides the trailer earlier plans used).
|
|
33
|
+
- **Before each commit:** `uv run ruff format .`, `uv run ruff check .`, `uv run ty check src`. For board changes, also `npm run typecheck` in `web/`, and after an API change `npm run types`.
|
|
34
|
+
- **Tests:** `uv run pytest -n 4`, with `FLYLOFT_TEST_DATABASE_URL` unset.
|
|
35
|
+
|
|
36
|
+
## Review Focus
|
|
37
|
+
|
|
38
|
+
1. **Lowercase memory, as agents write it (`"16g"`):** accepted, and the same as `"16G"`. Pinned in Task 1.
|
|
39
|
+
2. **Zero or negative sizes (`cpus=0`, `mem="0"`, `cpu_run_cpus = 0`):** refused, with the reason. Pinned in Tasks 1 and 3.
|
|
40
|
+
3. **A 0-GPU run that names `cpus` on a cluster without a CPU partition:** refused with "this cluster has no CPU partition", not silently ignored. Pinned in Task 3.
|
|
41
|
+
4. **The limit at exactly the requested value (`cpus == cpu_run_cpus`, `mem` equal in another unit, `"1T"` against `"1024G"`):** accepted. Pinned in Tasks 1 and 3.
|
|
42
|
+
5. **A 0-GPU setup run that names no CPUs or memory, on a cluster without a CPU partition** (every investigation today): accepted and rendered exactly as before. Pinned in Task 3, where the existing `investigator.py` setup request on cluster `local` is unchanged and still accepted.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Task 1: Cluster CPU partition, the CPU-run limit in the campaign file, and Slurm memory
|
|
47
|
+
|
|
48
|
+
**Files:**
|
|
49
|
+
- Modify: `src/flyloft/config/models.py` (`ResourceProfile`, `ClusterConfig`, new `slurm_mem_mb`)
|
|
50
|
+
- Modify: `src/flyloft/config/load.py` (`validate_campaign`, beside the existing resource checks near line 148)
|
|
51
|
+
- Modify: `examples/live-config/clusters/slinky.toml`
|
|
52
|
+
- Test: `tests/unit/test_config.py` (create it if absent, following `tests/unit/` style)
|
|
53
|
+
|
|
54
|
+
**Interfaces:**
|
|
55
|
+
- Produces:
|
|
56
|
+
- `slurm_mem_mb(mem: str) -> int` in `flyloft.config.models`, raising `ValueError` with the copy from the Global Constraints;
|
|
57
|
+
- `ClusterConfig.cpu_partition: str | None = None`;
|
|
58
|
+
- `ResourceProfile.cpu_run_cpus: int | None = None`;
|
|
59
|
+
- `ResourceProfile.cpu_run_mem: str | None = None`.
|
|
60
|
+
|
|
61
|
+
- [ ] **Step 1: Write the failing tests**
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
@pytest.mark.parametrize("mem,mb", [("512", 512), ("512M", 512), ("8G", 8192), ("16g", 16384),
|
|
65
|
+
("384G", 393216), ("1T", 1048576), ("2048K", 2)])
|
|
66
|
+
def test_slurm_mem_mb_reads_slurm_memory(mem, mb):
|
|
67
|
+
assert slurm_mem_mb(mem) == mb
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize("mem", ["8GB", "1.5G", "-1G", "0", "0G", "", "16 G"])
|
|
70
|
+
def test_slurm_mem_mb_refuses_other_forms(mem):
|
|
71
|
+
with pytest.raises(ValueError, match="such as 16G"):
|
|
72
|
+
slurm_mem_mb(mem)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then a `validate_campaign` test parametrized over campaign files written to a temporary config directory. The cluster has `cpu_partition = "cpu"` unless noted:
|
|
76
|
+
- refused, matching "only an investigation": `cpu_run_cpus = 64` on a `budgeted` campaign;
|
|
77
|
+
- refused, matching "has no CPU partition": `cpu_run_mem = "64G"` on an investigation whose cluster has no `cpu_partition`;
|
|
78
|
+
- refused, matching "at least 1": `cpu_run_cpus = 0`;
|
|
79
|
+
- refused, matching "such as 16G": `cpu_run_mem = "64GB"`;
|
|
80
|
+
- accepted: `cpu_run_cpus = 128, cpu_run_mem = "384G"` on an investigation;
|
|
81
|
+
- accepted: an investigation with neither field.
|
|
82
|
+
|
|
83
|
+
Use the existing helpers that build `Config` from a directory, as in `tests/unit/test_runs.py`.
|
|
84
|
+
|
|
85
|
+
- [ ] **Step 2: Run them to verify they fail**
|
|
86
|
+
|
|
87
|
+
Run: `uv run pytest tests/unit/test_config.py -v`
|
|
88
|
+
Expected: FAIL (`slurm_mem_mb` not defined; extra fields forbidden).
|
|
89
|
+
|
|
90
|
+
- [ ] **Step 3: Implement**
|
|
91
|
+
|
|
92
|
+
- Add `slurm_mem_mb` as one `re.fullmatch(r"([1-9]\d*)([KMGTkmgt]?)", mem)` with a unit table.
|
|
93
|
+
- Add the three fields.
|
|
94
|
+
- In `validate_campaign`, after the cluster is known to exist, apply the four refusals in the order the test lists them. The copy:
|
|
95
|
+
- "a CPU-run limit belongs only to an investigation";
|
|
96
|
+
- "cluster '<name>' has no CPU partition";
|
|
97
|
+
- "cpu_run_cpus must be at least 1";
|
|
98
|
+
- the parser's message.
|
|
99
|
+
- `campaign.mem` (the GPU runs' memory) is not newly validated. That is out of scope.
|
|
100
|
+
- In `examples/live-config/clusters/slinky.toml`, correct the comment on `partition` to say it is the partition for jobs whose GPU type names none. Add `# cpu_partition = "cpu" # jobs that ask for no GPUs go here`.
|
|
101
|
+
|
|
102
|
+
- [ ] **Step 4: Run them to verify they pass**
|
|
103
|
+
|
|
104
|
+
Run: `uv run pytest tests/unit/test_config.py -v`
|
|
105
|
+
Expected: PASS.
|
|
106
|
+
|
|
107
|
+
- [ ] **Step 5: Commit**
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git add src/flyloft/config tests/unit/test_config.py examples/live-config/clusters/slinky.toml
|
|
111
|
+
git commit -m "A cluster can name a CPU partition and an investigation can set a CPU-run limit, in Slurm's memory form"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Task 2: Store the limit and a run's CPUs, memory and partition; render a CPU run
|
|
115
|
+
|
|
116
|
+
**Files:**
|
|
117
|
+
- Create: `src/flyloft/migrations/0028_cpu_runs.sql`
|
|
118
|
+
- Modify: `src/flyloft/record/models.py` (`Campaign`, `Run`, `RunRequestBody`)
|
|
119
|
+
- Modify: `src/flyloft/record/store.py` (`create_campaign`, `create_run`)
|
|
120
|
+
- Modify: `src/flyloft/record/launch.py` (pass the limit)
|
|
121
|
+
- Modify: `src/flyloft/runs/staging.py` (`render_job_script`, new `job_partition`)
|
|
122
|
+
- Modify: `src/flyloft/service/tick.py` (submit path, about line 186)
|
|
123
|
+
- Test: `tests/unit/test_runs.py`
|
|
124
|
+
|
|
125
|
+
**Interfaces:**
|
|
126
|
+
- Consumes (Task 1): `ClusterConfig.cpu_partition`, `ResourceProfile.cpu_run_cpus`, `ResourceProfile.cpu_run_mem`.
|
|
127
|
+
- Produces:
|
|
128
|
+
- `Campaign.cpu_run_cpus: int | None = None`, `Campaign.cpu_run_mem: str | None = None`;
|
|
129
|
+
- `Run.cpus: int | None = None`, `Run.mem: str | None = None`, `Run.partition: str | None = None`;
|
|
130
|
+
- `RunRequestBody.cpus: int | None = None`, `RunRequestBody.mem: str | None = None`;
|
|
131
|
+
- `store.create_campaign(..., cpu_run_cpus: int | None = None, cpu_run_mem: str | None = None)`;
|
|
132
|
+
- `store.create_run(..., cpus: int | None = None, mem: str | None = None)`;
|
|
133
|
+
- `job_partition(campaign: Campaign, cluster: ClusterConfig, run: Run | None = None) -> str | None` in `flyloft.runs.staging`: the partition the job script names, or `None` when it names none.
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 1: Write the failing tests** in `tests/unit/test_runs.py`, using its `campaign(...)` and `cluster(...)` helpers (extend `cluster` to take `cpu_partition`):
|
|
136
|
+
- `test_a_cpu_run_goes_to_the_cpu_partition_with_its_own_request`: a cluster with `cpu_partition="cpu"` and GPU type `l40s` on partition `l40s`; an investigation campaign with `cpus=None, mem=None`; a `Run` with `command="bash build.sh"`, `gpus=0`, `cpus=96`, `mem="256G"`, `time_limit=86400`. The script contains `#SBATCH --partition=cpu`, `#SBATCH --cpus-per-task=96`, `#SBATCH --mem=256G` and `#SBATCH --time=24:00:00`, and no line starting `#SBATCH --gres`. `job_partition(...) == "cpu"`.
|
|
137
|
+
- `test_a_gpu_run_on_a_cluster_with_a_cpu_partition_renders_as_before`: the same cluster, the campaign with `cpus=8, mem="64G"`, and a run with `gpus=1, cpus=None, mem=None`. The script is identical to the one rendered by a cluster without `cpu_partition`.
|
|
138
|
+
- `test_a_zero_gpu_run_without_a_cpu_partition_renders_as_before`: a cluster without `cpu_partition` and a run with `gpus=0`. `#SBATCH --partition=l40s`, and no `--cpus-per-task` beyond the campaign's.
|
|
139
|
+
- The tick's recording of `run.partition` is asserted in Task 3's scenario test, which has a service. It is written in the same `update_run` call that sets `state="queued"`, so a failed submit never writes it.
|
|
140
|
+
|
|
141
|
+
- [ ] **Step 2: Run them to verify they fail**
|
|
142
|
+
|
|
143
|
+
Run: `uv run pytest tests/unit/test_runs.py -v -k "cpu_partition or cpu_run"`
|
|
144
|
+
Expected: FAIL.
|
|
145
|
+
|
|
146
|
+
- [ ] **Step 3: Implement**
|
|
147
|
+
|
|
148
|
+
- **Migration:** `0028_cpu_runs.sql` with the five `ADD COLUMN`s from the Global Constraints, and a two-line header comment naming `docs/design/cpu-partition.md`, as 0027 does.
|
|
149
|
+
- **Models and store:** add the fields. `create_campaign` and `create_run` take and insert the new columns. `launch.py` passes `cf.resources.cpu_run_cpus` and `cf.resources.cpu_run_mem`.
|
|
150
|
+
- **`job_partition`:** returns `cluster.cpu_partition` when `run` is an investigation's run (`run.command is not None`), `run.gpus == 0` and `cluster.cpu_partition`. Otherwise it returns `kind.partition or cluster.partition`, as today.
|
|
151
|
+
- **`render_job_script`:** uses `job_partition` for the partition line. For a CPU run it writes `run.cpus` and `run.mem` in place of the campaign's.
|
|
152
|
+
- **Tick:** after a successful `client.submit`, `store.update_run(..., partition=job_partition(campaign, cluster, run))` in the same call that sets `state="queued"`.
|
|
153
|
+
|
|
154
|
+
- [ ] **Step 4: Run them to verify they pass**
|
|
155
|
+
|
|
156
|
+
Run: `uv run pytest tests/unit/test_runs.py -v` and `uv run pytest -n 4 tests/e2e/test_campaign_compute.py tests/e2e/test_validation.py`
|
|
157
|
+
Expected: PASS, with existing job-script assertions unchanged.
|
|
158
|
+
|
|
159
|
+
- [ ] **Step 5: Commit**
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git add src/flyloft tests/unit/test_runs.py
|
|
163
|
+
git commit -m "A CPU run renders to the cluster's CPU partition with its own CPUs and memory, and every run records the partition it was submitted to"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Task 3: The CPU run's request rules, the agent's tool, and the implementer's assignment
|
|
167
|
+
|
|
168
|
+
> **Revised 2026-10-22 after rebasing onto `c197bcf` (#64, tasks).** Read these over the text below where they differ:
|
|
169
|
+
> - **Scope:** a CPU run is a critic-judged campaign's run (`campaign.critic_judged`, an investigation or a task) with `gpus == 0` on a cluster with a `cpu_partition`. The request rules sit in `_request_investigation_run`, which both types already use.
|
|
170
|
+
> - **Task 1's rule:** fix it in `src/flyloft/config/load.py`. It references the removed name `investigation`; use #64's `judged` (`cf.critic_judged`). Its copy becomes "only an investigation or a task takes a CPU-run limit". Update `tests/unit/test_config.py` to match, and add accepted cases for a `task` with a CPU-run limit, including `gpus = 0`.
|
|
171
|
+
> - **The template:** the shared partials `src/flyloft/turns/roles/_cj_implementer_rules.md.j2` (the run-sizing line) and `_cj_record.md.j2` (the run line) replace `implementer.investigate.md.j2` lines 14 and 52.
|
|
172
|
+
> - **An added scenario:** in `tests/e2e/test_tasks.py`'s style, a task with `resources = { gpus = 0, time = "01:00:00", cpu_run_cpus = 4, cpu_run_mem = "2G" }` on `local-cpu`. Its implementer's 0-GPU run with `cpus=2, mem="1G"` renders to the CPU partition and is charged 0. The existing `test_tasks.py` scenarios pass unchanged.
|
|
173
|
+
> - **Prior attempt:** a paused attempt from before the rebase is saved as `.superpowers/sdd/cpu-partition/task-3-wip.patch` (plus `task-3-wip-cpu_investigator.py`). It is reference only and will not apply cleanly. It also left three failures to avoid repeating: `test_templates` trailing newline, `test_web_app` cluster listing (update its expected list for `local-cpu`), and `test_agent_interface`.
|
|
174
|
+
|
|
175
|
+
**Files:**
|
|
176
|
+
- Modify: `src/flyloft/record/actions.py` (`request_run`, `_request_investigation_run`)
|
|
177
|
+
- Modify: `src/flyloft/tools/server.py` (`investigation_request_run`, about line 347)
|
|
178
|
+
- Modify: `src/flyloft/turns/roles/implementer.investigate.md.j2` (line 14, and the run line at 52)
|
|
179
|
+
- Modify: `src/flyloft/turns/assembly.py` only if the template needs the cluster's `cpu_partition` in its context
|
|
180
|
+
- Modify: `tests/conftest.py` (a second fake cluster `local-cpu` with `cpu_partition = "local-cpu"`)
|
|
181
|
+
- Create: `tests/scripts/cpu_investigator.py`
|
|
182
|
+
- Test: `tests/e2e/test_investigate.py`
|
|
183
|
+
|
|
184
|
+
**Interfaces:**
|
|
185
|
+
- Consumes (Tasks 1 and 2): `slurm_mem_mb`, `RunRequestBody.cpus/mem`, `Campaign.cpu_run_cpus/mem`, `store.create_run(cpus=, mem=)`, `job_partition`.
|
|
186
|
+
- Produces:
|
|
187
|
+
- `actions.request_run(..., cpu_partition: str | None = None)`. The tool server passes `config.clusters[campaign.cluster].cpu_partition`.
|
|
188
|
+
- Tool `request_run` in an investigation gains `cpus: int | None = None` and `mem: str | None = None`.
|
|
189
|
+
|
|
190
|
+
- [ ] **Step 1: Write the failing scenario test** `test_an_investigation_builds_its_data_on_the_cpu_partition` in `tests/e2e/test_investigate.py`:
|
|
191
|
+
- **Setup:** the campaign file is `investigation_file`'s with `cluster = "local-cpu"` and `resources = { gpus = 1, time = "01:00:00", cpu_run_cpus = 4, cpu_run_mem = "2G" }`. The implementer is `tests/scripts/cpu_investigator.py`, registered as a harness in `conftest.py`'s list.
|
|
192
|
+
- **Refused requests**, each via the `refused` helper with the reason checked through the record's refusal text, as the existing investigator does:
|
|
193
|
+
- `gpus=0, cpus=8, mem="1G"`: matches "at most 4 CPUs";
|
|
194
|
+
- `gpus=0, cpus=2, mem="4G"`: matches "at most 2G";
|
|
195
|
+
- `gpus=0` with neither: matches "names its CPUs and memory";
|
|
196
|
+
- `gpus=0, cpus=0, mem="1G"`: matches "at least 1";
|
|
197
|
+
- `gpus=1, cpus=2, mem="1G"`: matches "a GPU run takes the campaign's CPUs and memory".
|
|
198
|
+
- **The CPU run:** `gpus=0, cpus=4, mem="2048M"` is accepted (the limit exactly, memory in another unit). Its `job.sh` has `--partition=local-cpu`, `--cpus-per-task=4` and `--mem=2048M`. The run's `cpus == 4`, `mem == "2048M"` and `partition == "local-cpu"`.
|
|
199
|
+
- **The GPU run:** a `gpus=1` test run's `job.sh` has no `--cpus-per-task`, and its `partition` is the cluster's GPU partition, or `None` when the fake cluster names none.
|
|
200
|
+
- **The budget:** the CPU run's charge is 0 (`actions.charged`).
|
|
201
|
+
- **A cluster without a CPU partition:** in the existing `test_an_investigation_runs_its_experiments_and_ends_in_an_account` (cluster `local`), add one refused request to `investigator.py`, `gpus=0, cpus=2, mem="1G"`, matching "has no CPU partition".
|
|
202
|
+
- **Assignment:** the implementer's assignment (`store` turn row's assignment text) contains "CPU runs: up to 4 CPUs and 2G".
|
|
203
|
+
|
|
204
|
+
- [ ] **Step 2: Run it to verify it fails**
|
|
205
|
+
|
|
206
|
+
Run: `uv run pytest -n 4 tests/e2e/test_investigate.py -v`
|
|
207
|
+
Expected: FAIL (the tool takes no `cpus`).
|
|
208
|
+
|
|
209
|
+
- [ ] **Step 3: Implement**
|
|
210
|
+
|
|
211
|
+
- **`_request_investigation_run`**, before the run-slot check, in this order:
|
|
212
|
+
1. If `body.gpus > 0`, or `cpu_partition` is `None`, and `body.cpus` or `body.mem` is set: refuse with "a GPU run takes the campaign's CPUs and memory; name cpus and mem only on a run with 0 GPUs" when GPUs are asked, else "this cluster has no CPU partition; a run with 0 GPUs takes the campaign's CPUs and memory".
|
|
213
|
+
2. For a CPU run, refuse a missing `cpus` or `mem` with "a run with 0 GPUs names its CPUs and memory".
|
|
214
|
+
3. Refuse `cpus < 1` with "cpus must be at least 1".
|
|
215
|
+
4. Parse `mem` with `slurm_mem_mb`, refusing with its message.
|
|
216
|
+
5. Refuse `cpus > campaign.cpu_run_cpus` with "a CPU run may ask for at most {n} CPUs".
|
|
217
|
+
6. Refuse `slurm_mem_mb(mem) > slurm_mem_mb(campaign.cpu_run_mem)` with "a CPU run may ask for at most {campaign.cpu_run_mem}".
|
|
218
|
+
7. Pass `cpus` and `mem` to `_create_run`, and on to `store.create_run`.
|
|
219
|
+
- **`request_run`:** add the `cpu_partition` keyword and thread it through.
|
|
220
|
+
- **Tool server:** `investigation_request_run(summary, command, gpus, time_limit, hypothesis_id=None, cpus=None, mem=None)`. Add one sentence to its docstring: "With 0 GPUs, on a cluster with CPU nodes, the run goes there and names its `cpus` and `mem` (such as 16G); a GPU run names neither."
|
|
221
|
+
- **Template:** when the cluster has a CPU partition, line 14 gains a sentence: "CPU runs: up to {{ campaign.cpu_run_cpus or "all the node's" }} CPUs and {{ campaign.cpu_run_mem or "all the node's memory" }}; put data work (downloads, filtering, clustering) on a run with 0 GPUs, sized to what it needs." Keep the exact prefix "CPU runs: up to 4 CPUs and 2G" for the test's values. The run line (52) adds `, {{ r.cpus }} CPUs, {{ r.mem }} on {{ r.partition }}` when `r.cpus` is set.
|
|
222
|
+
- **`cpu_investigator.py`:** follows `investigator.py`'s shape. It makes the refusals above, then the accepted CPU run and a GPU test run, then concludes as the quiet investigator does.
|
|
223
|
+
|
|
224
|
+
- [ ] **Step 4: Run it to verify it passes**
|
|
225
|
+
|
|
226
|
+
Run: `uv run pytest -n 4 tests/e2e/test_investigate.py tests/unit -v`
|
|
227
|
+
Expected: PASS.
|
|
228
|
+
|
|
229
|
+
- [ ] **Step 5: Commit**
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
git add src/flyloft tests
|
|
233
|
+
git commit -m "An investigation's implementer sizes a CPU run within the campaign's CPU-run limit, and a GPU run keeps the campaign's CPUs and memory"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Task 4: The board: the limit at launch and a CPU run's request
|
|
237
|
+
|
|
238
|
+
> **Revised 2026-10-22 after #64:** the CPU-run fields show for both critic-judged types. Use the board's `criticJudged(c)` and the launch page's task mode, not an `investigate` check alone.
|
|
239
|
+
|
|
240
|
+
**Files:**
|
|
241
|
+
- Modify: `src/flyloft/web/schemas.py` (`ClusterView`, `RunView`)
|
|
242
|
+
- Modify: `src/flyloft/web/app.py` (line 468, `ClusterView(...)`; wherever `RunView` is built)
|
|
243
|
+
- Modify: `web/src/lib/crew.ts` (`Compute`, and `campaignFile`'s `resources`)
|
|
244
|
+
- Modify: `web/src/pages/launch/steps.tsx` (the Compute step, about lines 250–300)
|
|
245
|
+
- Modify: `web/src/pages/Run.tsx` (line 99), `web/src/pages/campaign/Runs.tsx` (line 67)
|
|
246
|
+
- Regenerate: `web/src/api/types.ts`
|
|
247
|
+
- Test: `tests/e2e/test_investigate.py` (browser test beside `test_a_researcher_launches_an_investigation_and_reads_it_on_the_board`)
|
|
248
|
+
|
|
249
|
+
**Interfaces:**
|
|
250
|
+
- Consumes (Tasks 1–3): `ClusterConfig.cpu_partition`, `Run.cpus/mem/partition`.
|
|
251
|
+
- Produces:
|
|
252
|
+
- `ClusterView.cpu_partition: str | None = None`;
|
|
253
|
+
- `RunView.cpus: int | None = None`, `RunView.mem: str | None = None`, `RunView.partition: str | None = None`;
|
|
254
|
+
- `Compute.cpuRunCpus: number | null` and `Compute.cpuRunMem: string`, written to the campaign file as `cpu_run_cpus` and `cpu_run_mem` only when set.
|
|
255
|
+
|
|
256
|
+
- [ ] **Step 1: Write the failing browser test** `test_a_researcher_sets_a_cpu_run_limit_and_reads_a_cpu_run`:
|
|
257
|
+
- Launching an investigation on `local-cpu` shows two fields labelled "CPUs per CPU run, at most" and "Memory per CPU run, at most", under the hint "Each run with no GPUs goes to the CPU nodes and may ask for up to these."
|
|
258
|
+
- Filling 4 and 2G and starting stores `cpu_run_cpus == 4` and `cpu_run_mem == "2G"` on the campaign.
|
|
259
|
+
- On cluster `local` the two fields are absent.
|
|
260
|
+
- After the scripted CPU run finishes, its run page reads "0 GPUs · 4 CPUs · 2048M for up to" and "on local-cpu".
|
|
261
|
+
|
|
262
|
+
- [ ] **Step 2: Run it to verify it fails**
|
|
263
|
+
|
|
264
|
+
Run: `uv run pytest tests/e2e/test_investigate.py -v -k cpu_run_limit`
|
|
265
|
+
Expected: FAIL (it skips if `web/` is not built: run `npm ci && npm run build` in `web/` first).
|
|
266
|
+
|
|
267
|
+
- [ ] **Step 3: Implement**
|
|
268
|
+
|
|
269
|
+
- Add the API fields, then run `npm run types`.
|
|
270
|
+
- In the Compute step, render the two fields only when `investigate` and the chosen cluster's `cpu_partition` is set, in a second row below the existing four, with the hint above. The existing "CPUs per run" and "Memory" fields keep their labels.
|
|
271
|
+
- In `Run.tsx` and `Runs.tsx`, when `r.cpus != null`, show `{r.gpus} GPUs · {r.cpus} CPUs · {r.mem}` in place of the GPU-only text. Add `on {r.partition}` when `r.partition` is set.
|
|
272
|
+
|
|
273
|
+
- [ ] **Step 4: Run it to verify it passes**
|
|
274
|
+
|
|
275
|
+
Run: `npm run typecheck` in `web/`, `npm run build`, then `uv run pytest -n 4 tests/e2e/test_investigate.py`
|
|
276
|
+
Expected: PASS, with no drift in `web/src/api/types.ts`.
|
|
277
|
+
|
|
278
|
+
- [ ] **Step 5: Commit**
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
git add src/flyloft/web web/src
|
|
282
|
+
git commit -m "The launch page offers a CPU-run limit on a cluster with CPU nodes, and a CPU run's page shows its CPUs, memory and partition"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Task 5: Tier two on bcr, and the design's close
|
|
286
|
+
|
|
287
|
+
**Files:**
|
|
288
|
+
- Modify: `tests/scripts/live_investigator.py`
|
|
289
|
+
- Modify: `tests/e2e/test_live.py` (`test_live_an_investigation_sizes_its_runs_and_keeps_its_environment`, line 376)
|
|
290
|
+
- Modify: `docs/design/cpu-partition.md` (status line), `docs/ARCHITECTURE.md` (status paragraph: one clause naming this change and its design)
|
|
291
|
+
|
|
292
|
+
**Interfaces:**
|
|
293
|
+
- Consumes (Tasks 1–4): everything above.
|
|
294
|
+
|
|
295
|
+
- [ ] **Step 1: Change the live test.** When the live cluster names a `cpu_partition`:
|
|
296
|
+
- the campaign file adds `cpu_run_cpus = 16, cpu_run_mem = "32G"`;
|
|
297
|
+
- the script's setup run asks for `cpus=8, mem="16G"` and also writes `nproc` to `$FLYLOFT_OUT/nproc.txt`;
|
|
298
|
+
- the test asserts `setup.partition == cluster's cpu_partition`, `nproc.txt` reads `8`, and `sacct -o Partition,AllocTRES` for the setup job shows the CPU partition, `cpu=8` and `mem=16G`.
|
|
299
|
+
|
|
300
|
+
When the cluster names none, the test runs as today. A separate assertion skips with the reason "the live cluster names no cpu_partition". The script reads whether the campaign has a CPU-run limit (`campaign.cpu_run_cpus`) to decide which request to make.
|
|
301
|
+
|
|
302
|
+
- [ ] **Step 2: Run tier two** with the tester's live config naming `cpu_partition = "cpu"` on bcr:
|
|
303
|
+
|
|
304
|
+
Run: `FLYLOFT_LIVE=1 uv run pytest -m live tests/e2e/test_live.py -v -k sizes_its_runs`
|
|
305
|
+
Expected: PASS. The printed `AllocTRES` for the setup job shows `cpu=8,mem=16G` and no `gres/gpu`, and the GPU run imports the environment the CPU run built. Record the output for the design's status line.
|
|
306
|
+
|
|
307
|
+
- [ ] **Step 3: Update the status lines.** In `cpu-partition.md`: "approved 2026-10-22; built on branch `feature/cpu-partition`. Tier one passed (N passed, M skipped). Tier two passed on bcr: …" with what Step 2 showed. Also add one clause to `ARCHITECTURE.md`'s status paragraph pointing to the design.
|
|
308
|
+
|
|
309
|
+
- [ ] **Step 4: Run the whole suite**
|
|
310
|
+
|
|
311
|
+
Run: `uv run pytest -n 4`, then `uv run ruff format . && uv run ruff check . && uv run ty check src` and `npm run typecheck` in `web/`
|
|
312
|
+
Expected: all pass. Report the counts.
|
|
313
|
+
|
|
314
|
+
- [ ] **Step 5: Commit**
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
git add tests docs
|
|
318
|
+
git commit -m "Tier two runs an investigation's CPU run on bcr's CPU node and its GPU run uses what it built"
|
|
319
|
+
```
|