flyloft 0.2.2__tar.gz → 0.3.0__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.
Files changed (511) hide show
  1. {flyloft-0.2.2 → flyloft-0.3.0}/PKG-INFO +1 -1
  2. {flyloft-0.2.2 → flyloft-0.3.0}/docs/ARCHITECTURE.md +28 -8
  3. {flyloft-0.2.2 → flyloft-0.3.0}/docs/NEW_PROJECT.md +11 -2
  4. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/investigate.md +2 -2
  5. flyloft-0.3.0/docs/design/preemption.md +218 -0
  6. flyloft-0.3.0/docs/design/run-storage.md +158 -0
  7. flyloft-0.3.0/docs/design/toolshed.md +211 -0
  8. flyloft-0.3.0/docs/plans/preemption.md +325 -0
  9. flyloft-0.3.0/docs/plans/run-storage.md +498 -0
  10. flyloft-0.3.0/docs/plans/toolshed-b.md +982 -0
  11. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/clusters/slinky.toml +2 -0
  12. {flyloft-0.2.2 → flyloft-0.3.0}/pyproject.toml +1 -1
  13. flyloft-0.3.0/scripts/preemption_drill.sh +14 -0
  14. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/_version.py +2 -2
  15. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/cli/main.py +3 -1
  16. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/client/main.py +1 -1
  17. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/client/render.py +17 -4
  18. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/config/load.py +2 -0
  19. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/config/models.py +14 -1
  20. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/github.py +56 -3
  21. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/host/doctor.py +3 -1
  22. flyloft-0.3.0/src/flyloft/migrations/0029_preemption.sql +3 -0
  23. flyloft-0.3.0/src/flyloft/migrations/0030_archive.sql +4 -0
  24. flyloft-0.3.0/src/flyloft/migrations/0031_toolshed.sql +27 -0
  25. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/body.py +10 -1
  26. flyloft-0.3.0/src/flyloft/publication/gh.py +120 -0
  27. flyloft-0.3.0/src/flyloft/publication/publisher.py +29 -0
  28. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/record.md.j2 +14 -0
  29. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/actions.py +203 -9
  30. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/consistency.py +31 -0
  31. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/launch.py +10 -0
  32. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/models.py +57 -4
  33. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/store.py +67 -2
  34. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/views.py +18 -0
  35. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/runs/client.py +2 -0
  36. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/runs/collect.py +7 -3
  37. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/runs/observe.py +1 -1
  38. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/runs/ssh.py +35 -12
  39. flyloft-0.3.0/src/flyloft/runs/staging.py +169 -0
  40. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/service/tick.py +364 -13
  41. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/skill/SKILL.md +4 -2
  42. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/skill/contract.md +3 -1
  43. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/tools/server.py +65 -1
  44. flyloft-0.3.0/src/flyloft/toolshed.py +102 -0
  45. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/assembly.py +89 -10
  46. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/launch.py +26 -5
  47. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/__init__.py +2 -2
  48. flyloft-0.3.0/src/flyloft/turns/roles/_cj_critic_runs.md.j2 +22 -0
  49. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/_cj_implementer_rules.md.j2 +3 -1
  50. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/_cj_record.md.j2 +1 -1
  51. flyloft-0.3.0/src/flyloft/turns/roles/_toolshed.md.j2 +24 -0
  52. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/critic.investigate.md.j2 +1 -1
  53. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/critic.md.j2 +1 -1
  54. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/critic.task.md.j2 +1 -1
  55. flyloft-0.3.0/src/flyloft/turns/roles/critic.tool.md.j2 +52 -0
  56. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/implementer.investigate.md.j2 +2 -0
  57. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/implementer.md.j2 +1 -0
  58. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/implementer.task.md.j2 +2 -0
  59. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/integrator.md.j2 +4 -1
  60. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/substrates/__init__.py +11 -2
  61. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/substrates/claude_code.py +9 -1
  62. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/substrates/opencode.py +7 -1
  63. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/substrates/scripted.py +13 -3
  64. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/wake.py +15 -4
  65. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/app.py +7 -1
  66. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/read.py +6 -0
  67. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/schemas.py +20 -0
  68. flyloft-0.2.2/src/flyloft/web/static/assets/index-CEib_UXZ.js → flyloft-0.3.0/src/flyloft/web/static/assets/index-D4irDcTX.js +33 -33
  69. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/static/index.html +1 -1
  70. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/workspace/git.py +20 -10
  71. {flyloft-0.2.2 → flyloft-0.3.0}/tests/conftest.py +31 -0
  72. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_campaign_compute.py +2 -0
  73. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_investigate.py +7 -0
  74. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_live.py +228 -1
  75. flyloft-0.3.0/tests/e2e/test_preemption.py +122 -0
  76. flyloft-0.3.0/tests/e2e/test_run_storage.py +167 -0
  77. flyloft-0.3.0/tests/e2e/test_toolshed.py +134 -0
  78. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_validation.py +12 -0
  79. flyloft-0.3.0/tests/fakes/bin/s5cmd +55 -0
  80. flyloft-0.3.0/tests/fakes/bin/srun +5 -0
  81. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fakes/cluster.py +88 -6
  82. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fakes/github.py +29 -0
  83. flyloft-0.3.0/tests/fakes/publisher.py +109 -0
  84. flyloft-0.3.0/tests/fixtures/toolshed/README.md +1 -0
  85. flyloft-0.3.0/tests/fixtures/toolshed/scripts/check.py +322 -0
  86. flyloft-0.3.0/tests/fixtures/toolshed/scripts/record +25 -0
  87. flyloft-0.3.0/tests/scripts/archive_investigator.py +19 -0
  88. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/investigator.py +3 -1
  89. flyloft-0.3.0/tests/scripts/live_archiver.py +23 -0
  90. flyloft-0.3.0/tests/scripts/live_tool_critic.py +34 -0
  91. flyloft-0.3.0/tests/scripts/live_tool_tasker.py +120 -0
  92. flyloft-0.3.0/tests/scripts/tool_critic.py +58 -0
  93. flyloft-0.3.0/tests/scripts/tool_tasker.py +116 -0
  94. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_actions.py +19 -0
  95. flyloft-0.3.0/tests/unit/test_archive_script.py +110 -0
  96. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_assembly.py +212 -1
  97. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_claude_code.py +9 -0
  98. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_client.py +25 -2
  99. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_config.py +22 -1
  100. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_gh.py +28 -0
  101. flyloft-0.3.0/tests/unit/test_github.py +49 -0
  102. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_host_doctor.py +15 -0
  103. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_investigation.py +49 -2
  104. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_migrate.py +5 -0
  105. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_publication.py +41 -1
  106. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_runs.py +82 -12
  107. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_service.py +23 -0
  108. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_ssh.py +43 -0
  109. flyloft-0.3.0/tests/unit/test_toolshed.py +731 -0
  110. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_web_app.py +10 -2
  111. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_web_read.py +39 -0
  112. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/api/queries.ts +1 -1
  113. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/api/types.ts +51 -0
  114. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Run.tsx +12 -1
  115. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/campaign/Frame.tsx +5 -0
  116. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/campaign/Overview.tsx +31 -0
  117. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/campaign/Runs.tsx +1 -0
  118. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/project/ValidationPanel.tsx +6 -2
  119. flyloft-0.2.2/src/flyloft/publication/gh.py +0 -64
  120. flyloft-0.2.2/src/flyloft/publication/publisher.py +0 -15
  121. flyloft-0.2.2/src/flyloft/runs/staging.py +0 -94
  122. flyloft-0.2.2/src/flyloft/turns/roles/_cj_critic_runs.md.j2 +0 -22
  123. flyloft-0.2.2/tests/fakes/publisher.py +0 -41
  124. flyloft-0.2.2/tests/unit/test_github.py +0 -26
  125. {flyloft-0.2.2 → flyloft-0.3.0}/.github/workflows/ci.yml +0 -0
  126. {flyloft-0.2.2 → flyloft-0.3.0}/.github/workflows/python-publish.yaml +0 -0
  127. {flyloft-0.2.2 → flyloft-0.3.0}/.gitignore +0 -0
  128. {flyloft-0.2.2 → flyloft-0.3.0}/.python-version +0 -0
  129. {flyloft-0.2.2 → flyloft-0.3.0}/AGENTS.md +0 -0
  130. {flyloft-0.2.2 → flyloft-0.3.0}/CLAUDE.md +0 -0
  131. {flyloft-0.2.2 → flyloft-0.3.0}/README.md +0 -0
  132. {flyloft-0.2.2 → flyloft-0.3.0}/docs/NEW_TEMPLATE.md +0 -0
  133. {flyloft-0.2.2 → flyloft-0.3.0}/docs/VISION.md +0 -0
  134. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/agent-interface.md +0 -0
  135. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-1.md +0 -0
  136. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-2.md +0 -0
  137. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-3.md +0 -0
  138. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-4.md +0 -0
  139. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-5/campaign.html +0 -0
  140. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-5/campaigns.html +0 -0
  141. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-5/launch.html +0 -0
  142. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-5/pages.html +0 -0
  143. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/board-5.md +0 -0
  144. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/budget-billing.md +0 -0
  145. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/campaign-compute.md +0 -0
  146. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/compute/compute.html +0 -0
  147. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/compute.md +0 -0
  148. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/cpu-partition.md +0 -0
  149. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/criterion-removal.md +0 -0
  150. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/flyloft-up.md +0 -0
  151. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/multi-user.md +0 -0
  152. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/new-project.md +0 -0
  153. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/projects.md +0 -0
  154. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/replication.md +0 -0
  155. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-1.md +0 -0
  156. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-2.md +0 -0
  157. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-3a.md +0 -0
  158. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-3b.md +0 -0
  159. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-4a.md +0 -0
  160. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-4b.md +0 -0
  161. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-5a.md +0 -0
  162. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-5b.md +0 -0
  163. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-5c.md +0 -0
  164. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-5d.md +0 -0
  165. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-6a.md +0 -0
  166. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-6b.md +0 -0
  167. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/stage-7.md +0 -0
  168. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/tasks.md +0 -0
  169. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/tier-one-lockstep.md +0 -0
  170. {flyloft-0.2.2 → flyloft-0.3.0}/docs/design/validation.md +0 -0
  171. {flyloft-0.2.2 → flyloft-0.3.0}/docs/environments/deeplearning_v2026-09-15.txt +0 -0
  172. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/agent-interface.md +0 -0
  173. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/board-1.md +0 -0
  174. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/board-2.md +0 -0
  175. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/board-3.md +0 -0
  176. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/board-4.md +0 -0
  177. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/board-5.md +0 -0
  178. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/budget-billing.md +0 -0
  179. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/campaign-compute.md +0 -0
  180. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/compute.md +0 -0
  181. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/cpu-partition.md +0 -0
  182. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/criterion-removal.md +0 -0
  183. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/flyloft-up.md +0 -0
  184. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/investigate.md +0 -0
  185. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/multi-user-a.md +0 -0
  186. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/multi-user-b.md +0 -0
  187. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/new-project.md +0 -0
  188. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/projects.md +0 -0
  189. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/replication-a.md +0 -0
  190. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/replication-b.md +0 -0
  191. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/replication-c.md +0 -0
  192. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-1.md +0 -0
  193. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-2.md +0 -0
  194. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-3a.md +0 -0
  195. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-3b.md +0 -0
  196. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-4a.md +0 -0
  197. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-4b.md +0 -0
  198. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-5a.md +0 -0
  199. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-5b.md +0 -0
  200. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-5c.md +0 -0
  201. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-5d.md +0 -0
  202. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-6a.md +0 -0
  203. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-6b.md +0 -0
  204. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/stage-7.md +0 -0
  205. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/tasks.md +0 -0
  206. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/tier-one-lockstep.md +0 -0
  207. {flyloft-0.2.2 → flyloft-0.3.0}/docs/plans/validation.md +0 -0
  208. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/flyloft.toml +0 -0
  209. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/builder.toml +0 -0
  210. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/claude.toml +0 -0
  211. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/coordinator.toml +0 -0
  212. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/first_campaign.toml +0 -0
  213. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/generator.toml +0 -0
  214. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/happy_path.toml +0 -0
  215. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/integrator.toml +0 -0
  216. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/lenient_critic.toml +0 -0
  217. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/librarian.toml +0 -0
  218. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/opencode.toml +0 -0
  219. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/orchestrator.toml +0 -0
  220. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/proximity.toml +0 -0
  221. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/reporter.toml +0 -0
  222. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/harnesses/strict_critic.toml +0 -0
  223. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/projects/toy-live-4b.toml +0 -0
  224. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/projects/toy-live-5a.toml +0 -0
  225. {flyloft-0.2.2 → flyloft-0.3.0}/examples/live-config/projects/toy-live.toml +0 -0
  226. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/__init__.py +0 -0
  227. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/budget/__init__.py +0 -0
  228. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/budget/compare.py +0 -0
  229. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/budget/ledger.py +0 -0
  230. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/cli/__init__.py +0 -0
  231. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/client/__init__.py +0 -0
  232. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/client/api.py +0 -0
  233. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/client/handoff.py +0 -0
  234. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/config/__init__.py +0 -0
  235. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/config/projects.py +0 -0
  236. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/config/toml.py +0 -0
  237. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/host/__init__.py +0 -0
  238. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/host/harnesses/claude.toml +0 -0
  239. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/host/harnesses/opencode.toml +0 -0
  240. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/host/init.py +0 -0
  241. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/host/units.py +0 -0
  242. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/host/up.py +0 -0
  243. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/knowledge/__init__.py +0 -0
  244. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/knowledge/memory.py +0 -0
  245. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0001_initial.sql +0 -0
  246. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0002_finding_state.sql +0 -0
  247. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0003_several_implementers.sql +0 -0
  248. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0004_entry_order_within_a_transaction.sql +0 -0
  249. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0005_population.sql +0 -0
  250. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0006_memory.sql +0 -0
  251. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0007_islands.sql +0 -0
  252. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0008_blocks.sql +0 -0
  253. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0009_export.sql +0 -0
  254. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0010_failures.sql +0 -0
  255. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0011_unblock.sql +0 -0
  256. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0012_indefinite.sql +0 -0
  257. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0013_pull_requests.sql +0 -0
  258. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0014_run_elapsed.sql +0 -0
  259. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0015_campaign_types.sql +0 -0
  260. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0016_island_aim.sql +0 -0
  261. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0017_campaign_project_config.sql +0 -0
  262. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0018_fresh_record.sql +0 -0
  263. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0019_run_tree.sql +0 -0
  264. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0020_users.sql +0 -0
  265. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0021_token_refused.sql +0 -0
  266. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0022_drop_criterion.sql +0 -0
  267. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0023_campaign_compute.sql +0 -0
  268. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0024_optional_cpus_mem.sql +0 -0
  269. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0025_validation.sql +0 -0
  270. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0026_measurement.sql +0 -0
  271. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0027_investigation.sql +0 -0
  272. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/migrations/0028_cpu_runs.sql +0 -0
  273. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/__init__.py +0 -0
  274. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/_findings.md.j2 +0 -0
  275. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/_findings_critic_judged.md.j2 +0 -0
  276. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/_head.md.j2 +0 -0
  277. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/_head_critic_judged.md.j2 +0 -0
  278. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/_macros.md.j2 +0 -0
  279. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/export.py +0 -0
  280. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/pr_body.md.j2 +0 -0
  281. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/promotion_pr.md.j2 +0 -0
  282. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/publication/record_pr.md.j2 +0 -0
  283. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/__init__.py +0 -0
  284. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/db.py +0 -0
  285. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/islands.py +0 -0
  286. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/migrate.py +0 -0
  287. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/researcher.py +0 -0
  288. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/submit.py +0 -0
  289. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/templates.py +0 -0
  290. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/users.py +0 -0
  291. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/record/validation.py +0 -0
  292. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/runs/__init__.py +0 -0
  293. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/runs/askpass.sh +0 -0
  294. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/runs/capacity.py +0 -0
  295. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/service/__init__.py +0 -0
  296. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/service/factory.py +0 -0
  297. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/skill/handoff.md +0 -0
  298. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/tools/__init__.py +0 -0
  299. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/tools/__main__.py +0 -0
  300. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/__init__.py +0 -0
  301. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/_plan.md.j2 +0 -0
  302. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/bounded.md.j2 +0 -0
  303. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/coordinator.md.j2 +0 -0
  304. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/direction.md.j2 +0 -0
  305. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/generator.md.j2 +0 -0
  306. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/librarian.md.j2 +0 -0
  307. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/memory.md.j2 +0 -0
  308. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/orchestrator.md.j2 +0 -0
  309. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/proximity.md.j2 +0 -0
  310. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/reporter.investigate.md.j2 +0 -0
  311. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/reporter.md.j2 +0 -0
  312. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/turns/roles/reporter.task.md.j2 +0 -0
  313. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/version.py +0 -0
  314. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/__init__.py +0 -0
  315. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/artifacts.py +0 -0
  316. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/auth.py +0 -0
  317. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/compute.py +0 -0
  318. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/events.py +0 -0
  319. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/openapi.py +0 -0
  320. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/web/static/assets/index-y1dUR_aG.css +0 -0
  321. {flyloft-0.2.2 → flyloft-0.3.0}/src/flyloft/workspace/__init__.py +0 -0
  322. {flyloft-0.2.2 → flyloft-0.3.0}/tests/__init__.py +0 -0
  323. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/__init__.py +0 -0
  324. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/fixtures/opencode-turn.jsonl +0 -0
  325. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_acting_as_owner.py +0 -0
  326. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_agent_interface.py +0 -0
  327. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_board1.py +0 -0
  328. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_board2.py +0 -0
  329. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_board3.py +0 -0
  330. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_board4.py +0 -0
  331. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_board5.py +0 -0
  332. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_compute.py +0 -0
  333. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_projects.py +0 -0
  334. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_replication.py +0 -0
  335. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_replication_board.py +0 -0
  336. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage1.py +0 -0
  337. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage2.py +0 -0
  338. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage3.py +0 -0
  339. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage3b.py +0 -0
  340. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage4a.py +0 -0
  341. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage4b.py +0 -0
  342. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage5a.py +0 -0
  343. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage5b.py +0 -0
  344. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage5c.py +0 -0
  345. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage5d.py +0 -0
  346. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage6a.py +0 -0
  347. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage6b.py +0 -0
  348. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_stage7.py +0 -0
  349. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_tasks.py +0 -0
  350. {flyloft-0.2.2 → flyloft-0.3.0}/tests/e2e/test_users.py +0 -0
  351. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fakes/__init__.py +0 -0
  352. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fixtures/slinky/capacity.txt +0 -0
  353. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fixtures/slinky/cluster.toml +0 -0
  354. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fixtures/toyproject/BRIEF.md +0 -0
  355. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fixtures/toyproject/config.json +0 -0
  356. {flyloft-0.2.2 → flyloft-0.3.0}/tests/fixtures/toyproject/train.py +0 -0
  357. {flyloft-0.2.2 → flyloft-0.3.0}/tests/host/__init__.py +0 -0
  358. {flyloft-0.2.2 → flyloft-0.3.0}/tests/host/test_host.py +0 -0
  359. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/__init__.py +0 -0
  360. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/builder.py +0 -0
  361. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/common.py +0 -0
  362. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/coordinator.py +0 -0
  363. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/cpu_investigator.py +0 -0
  364. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/cpu_tasker.py +0 -0
  365. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/crash.py +0 -0
  366. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/crew.py +0 -0
  367. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/failing_job.py +0 -0
  368. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/first_campaign.py +0 -0
  369. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/flaky_builder.py +0 -0
  370. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/generator.py +0 -0
  371. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/happy_path.py +0 -0
  372. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/integrator.py +0 -0
  373. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/integrator_spoil.py +0 -0
  374. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/investigator_quiet.py +0 -0
  375. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/lenient_critic.py +0 -0
  376. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/librarian.py +0 -0
  377. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/live_investigator.py +0 -0
  378. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/missing_results.py +0 -0
  379. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/near_far.py +0 -0
  380. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/orchestrator.py +0 -0
  381. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/plateau.py +0 -0
  382. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/post_then_crash.py +0 -0
  383. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/proximity.py +0 -0
  384. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/replication.py +0 -0
  385. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/reporter.py +0 -0
  386. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/retracting.py +0 -0
  387. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/silent_exit.py +0 -0
  388. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/strict_critic.py +0 -0
  389. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/task_critic.py +0 -0
  390. {flyloft-0.2.2 → flyloft-0.3.0}/tests/scripts/tasker.py +0 -0
  391. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/__init__.py +0 -0
  392. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/conftest.py +0 -0
  393. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/island_helpers.py +0 -0
  394. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/memory_helpers.py +0 -0
  395. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_budget.py +0 -0
  396. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_capacity.py +0 -0
  397. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_cli.py +0 -0
  398. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_client_main.py +0 -0
  399. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_compute_view.py +0 -0
  400. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_consistency.py +0 -0
  401. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_format_metrics.py +0 -0
  402. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_host_init.py +0 -0
  403. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_host_units.py +0 -0
  404. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_host_up.py +0 -0
  405. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_islands.py +0 -0
  406. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_knowledge.py +0 -0
  407. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_launch.py +0 -0
  408. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_measurement.py +0 -0
  409. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_opencode.py +0 -0
  410. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_population.py +0 -0
  411. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_projects.py +0 -0
  412. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_scripted.py +0 -0
  413. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_skill.py +0 -0
  414. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_store.py +0 -0
  415. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_submit.py +0 -0
  416. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_tasks.py +0 -0
  417. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_templates.py +0 -0
  418. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_tools.py +0 -0
  419. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_toyproject.py +0 -0
  420. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_transcript.py +0 -0
  421. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_users.py +0 -0
  422. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_wake.py +0 -0
  423. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_web_artifacts.py +0 -0
  424. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_web_compare.py +0 -0
  425. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_web_events.py +0 -0
  426. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/test_workspace.py +0 -0
  427. {flyloft-0.2.2 → flyloft-0.3.0}/tests/unit/web_helpers.py +0 -0
  428. {flyloft-0.2.2 → flyloft-0.3.0}/uv.lock +0 -0
  429. {flyloft-0.2.2 → flyloft-0.3.0}/web/components.json +0 -0
  430. {flyloft-0.2.2 → flyloft-0.3.0}/web/index.html +0 -0
  431. {flyloft-0.2.2 → flyloft-0.3.0}/web/package-lock.json +0 -0
  432. {flyloft-0.2.2 → flyloft-0.3.0}/web/package.json +0 -0
  433. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/App.tsx +0 -0
  434. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/api/client.test.ts +0 -0
  435. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/api/client.ts +0 -0
  436. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/api/live.ts +0 -0
  437. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Actions.tsx +0 -0
  438. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/AnswerValue.tsx +0 -0
  439. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Breadcrumbs.tsx +0 -0
  440. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/CampaignCard.tsx +0 -0
  441. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/CampaignFilter.tsx +0 -0
  442. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ChangeMarkers.tsx +0 -0
  443. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/DotBanner.tsx +0 -0
  444. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/EntityLink.tsx +0 -0
  445. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ErrorNote.tsx +0 -0
  446. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/FinishedRow.tsx +0 -0
  447. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Freshness.tsx +0 -0
  448. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/InvestigationCard.tsx +0 -0
  449. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/LogList.tsx +0 -0
  450. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ProgressChart.tsx +0 -0
  451. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/RoleChips.tsx +0 -0
  452. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/RoleNode.tsx +0 -0
  453. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Selectable.tsx +0 -0
  454. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Sidebar.tsx +0 -0
  455. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/StartFromAgent.tsx +0 -0
  456. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Step.tsx +0 -0
  457. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Story.tsx +0 -0
  458. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/SwarmMap.tsx +0 -0
  459. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Timeline.tsx +0 -0
  460. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Timestamp.tsx +0 -0
  461. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/Transcript.tsx +0 -0
  462. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ui/alert-dialog.tsx +0 -0
  463. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ui/button.tsx +0 -0
  464. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ui/popover.tsx +0 -0
  465. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ui/sheet.tsx +0 -0
  466. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ui/tabs.tsx +0 -0
  467. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/components/ui/tooltip.tsx +0 -0
  468. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/campaign.test.ts +0 -0
  469. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/campaign.ts +0 -0
  470. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/chart.test.ts +0 -0
  471. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/chart.ts +0 -0
  472. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/compute.test.ts +0 -0
  473. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/compute.ts +0 -0
  474. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/crew.test.ts +0 -0
  475. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/crew.ts +0 -0
  476. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/format.test.ts +0 -0
  477. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/format.ts +0 -0
  478. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/launch.test.ts +0 -0
  479. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/launch.ts +0 -0
  480. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/projects.ts +0 -0
  481. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/roles.test.ts +0 -0
  482. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/roles.ts +0 -0
  483. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/runs.test.ts +0 -0
  484. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/runs.ts +0 -0
  485. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/seen.test.ts +0 -0
  486. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/seen.ts +0 -0
  487. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/story.test.ts +0 -0
  488. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/story.ts +0 -0
  489. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/swarm.test.ts +0 -0
  490. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/swarm.ts +0 -0
  491. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/lib/utils.ts +0 -0
  492. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/main.tsx +0 -0
  493. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Admin.tsx +0 -0
  494. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Compare.tsx +0 -0
  495. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Compute.tsx +0 -0
  496. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Home.tsx +0 -0
  497. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Launch.tsx +0 -0
  498. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Login.tsx +0 -0
  499. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Projects.tsx +0 -0
  500. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/Turn.tsx +0 -0
  501. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/admin/Host.tsx +0 -0
  502. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/admin/Users.tsx +0 -0
  503. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/campaign/Activity.tsx +0 -0
  504. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/campaign/Hypotheses.tsx +0 -0
  505. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/campaign/context.ts +0 -0
  506. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/launch/steps.tsx +0 -0
  507. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/project/NewProject.tsx +0 -0
  508. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/pages/project/ProjectPage.tsx +0 -0
  509. {flyloft-0.2.2 → flyloft-0.3.0}/web/src/styles.css +0 -0
  510. {flyloft-0.2.2 → flyloft-0.3.0}/web/tsconfig.json +0 -0
  511. {flyloft-0.2.2 → flyloft-0.3.0}/web/vite.config.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flyloft
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Agent swarms for empirical research on Slurm clusters
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27
@@ -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)). 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)).
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 followed, in four parts, the first built (see [design/investigate.md](design/investigate.md)), the second parked on 2026-10-22 (§8: a bar fixed at launch), so the toolshed is its next part; 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)). On a cluster that names an object storage `work_dir_archive`, each ended run's directory and each ended campaign's folder move to the bucket (see [design/run-storage.md](design/run-storage.md)). The toolshed is built, and its tiers one and two passed on 2026-10-27 (see [design/toolshed.md](design/toolshed.md)); tier three follows.
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
 
@@ -75,9 +75,9 @@ An assessment must link to the run whose result it judges. It exists so an agent
75
75
 
76
76
  Stage two changes the last step: a finding entry leaves the hypothesis in testing, and a critique that accepts the finding moves it to supported. Stage 4a inserts screened and admitted between proposed and testing, and merged beside them for a duplicate; replication before review is a rule at the finding, not a state. States are text and transitions are code, so this growth is not shape.
77
77
 
78
- **Run lifecycle.** preparing, queued, running, finished, failed, canceled, unknown. The application state is derived from observed scheduler state. A job missing from the queue is unknown until confirmed, never assumed finished. A finished job without its expected result files yields an incomplete result, not a conclusion.
78
+ **Run lifecycle.** preparing, queued, running, finished, failed, canceled, interrupted, unknown. The application state is derived from observed scheduler state. A job missing from the queue is unknown until confirmed, never assumed finished. A finished job without its expected result files yields an incomplete result, not a conclusion. A preempted job is requeued by the cluster and waited on; it is interrupted when it is never requeued or its attempts have used twice its time limit. Interrupted is infrastructure, not a verdict, and is charged for the time it used ([design/preemption.md](design/preemption.md)).
79
79
 
80
- **Campaign lifecycle.** active, stopped, done. No campaign waits on a human (stage 5d): a failed turn is retried and, after three in a row, the agent is benched; a stall wakes the decision-maker and then finishes the campaign; a failed baseline stops it with its reason. Stopped means a human stopped new work, or the baseline failed; outstanding runs finish and are collected.
80
+ **Campaign lifecycle.** active, stopped, done. No campaign waits on a human (stage 5d): a failed turn is retried and, after three in a row, the agent is benched; a stall wakes the decision-maker and then finishes the campaign; a baseline that does not finish stops it with its reason, an interrupted one naming the preemption. Stopped means a human stopped new work, or the baseline failed; outstanding runs finish and are collected.
81
81
 
82
82
  **Agent state.** idle, active, benched. The active state is the claim that prevents a second concurrent turn for the same agent. It lives on the agent, not the campaign.
83
83
 
@@ -169,7 +169,7 @@ Two processes write to the record from the first turn, the service and the tool
169
169
  5. UUID identities.
170
170
  6. Agents act only through the tool surface, with tool lists per role.
171
171
  7. A substrate interface, justified by two implementations.
172
- 8. A worktree per agent.
172
+ 8. A worktree per agent, and in a campaign with a toolshed, a worktree of the shed beside it.
173
173
  9. PostgreSQL with numbered SQL migrations.
174
174
  10. What the researcher fixes at start is immutable: the question and, for an optimization, the baseline, metric, direction, and target, as campaign columns.
175
175
  11. Observed external state lives on entity rows with a last-observed timestamp.
@@ -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)); both are built and closed (2026-10-22). 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. The second part was parked on 2026-10-22 (§8): the researcher's own agent is the front door, so what follows a campaign is decided there, from the three campaign types and the client's commands, and a bar the researcher already knows goes into the brief, which the critic judges against today. flyloft builds primitives any workflow can be assembled from, not the workflows; the toolshed is the next primitive, and it is built ([design/toolshed.md](design/toolshed.md)): a lab-owned, public repository of tools, each a self-contained image with a prose skill; a campaign's run may name a tool and runs inside its image, and a campaign that needs a tool the shed lacks builds it through the shed's pull request, its own test run of the candidate image, and the critic's review, with no human in the loop. 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
 
@@ -275,7 +275,7 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
275
275
  | Replication policy | Done at stage 4a: a campaign's `replication` is the seeds a finding needs at one commit, enforced at `post_finding`; the job script exports `FLYLOFT_SEED` |
276
276
  | Replication beyond a seed count (a second dataset, a held-out split), and the coordinator ordering it | When a project's evaluation policy asks for more than seeds |
277
277
  | Replicating integration runs | With [design/replication.md](design/replication.md): a merged tree is measured at `replication` seeds |
278
- | A tool-builder role | With the toolshed (the vision's revision of 2026-10-13), where a tool-builder adds tools for every project |
278
+ | A tool-builder role | Done: the critic-judged implementer builds tools for the shed (see [design/toolshed.md](design/toolshed.md)) |
279
279
  | The swarm map's island row scrolling horizontally, its rule and stubs with it | When a campaign opens more islands than fit across the map (four at the board's width) |
280
280
  | A run slot reserved for consolidation (integration and root-island runs), so they never wait for a running test to end | When consolidation is seen waiting a whole run's length for a slot on a project whose runs take hours |
281
281
  | Backpressure on exploration: no new admissions while a campaign's queue of waiting runs is deep | When queued test runs are seen going stale (their island has moved on) before a slot reaches them |
@@ -305,6 +305,7 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
305
305
  | A per-project smoke time limit (a validation's is 30 minutes) | When a project's smoke run needs longer |
306
306
  | Cancelling a validation | When one is seen holding a GPU it should not |
307
307
  | A launch refused without a passed validation (the verdict is shown: see [design/projects.md](design/projects.md)) | When launches on unvalidated projects are seen failing |
308
+ | A run at a higher tier than the lab's lowest, for work that cannot be split under the preemption cap | When a campaign hits the cap on work that cannot be shortened or made to resume |
308
309
  | A run an outage kills mid-run (Slurm's NODE_FAIL) is a failed test run: it is charged, and its implementer may read a lost node as a refuted change; Slurm's `--requeue`, or flyloft resubmitting a NODE_FAIL run uncharged, are the likely fixes. The power outage under ESM-2 8427bd25 killed none: its jobs had finished, and the job script's `finished` marker recovered them | When an outage is seen killing runs |
309
310
  | An implementer whose request ran nothing because another line was measuring its tree is not woken if those runs fail: its hypothesis stays testing, unmeasured, with nothing in flight. The integrator in the same place re-requests the missing seeds at its verdict turn | When it is seen |
310
311
  | Two requests for one tree that arrive together both run: measure-once checks for a run in flight outside any lock, so concurrent requests each see none (budget billing's tier two: three seats requested one tree within 2 ms and all three ran, at three times the cost) | When the duplicate runs are seen costing a real campaign's budget |
@@ -337,7 +338,7 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
337
338
  | Artifact retention and garbage collection; W&B synchronization | When storage fills or a project asks |
338
339
  | Cost estimate on proposals | Done at stage 4a: the proposal's `cost` line, read by the coordinator |
339
340
  | Harness environment scrubbed of git and cluster credentials | Stage 3a, by allowlist |
340
- | Observation of pull request state, including merged or squash-merged promotion pull requests (stage 6b) | When something reads it |
341
+ | Observation of pull request state, including merged or squash-merged promotion pull requests (stage 6b) | Done for tool builds: the checks on a build's pushed and merge commits (see [design/toolshed.md](design/toolshed.md)); a campaign's own pull requests when something reads them |
341
342
  | A chart of cost and the metric over time on the board | When stage 6b's cost-per-promotion table stops being enough to read a long campaign |
342
343
  | An `.flyloft/` export at each promotion, not only at the stop | When a researcher needs turn transcripts before an indefinite campaign stops |
343
344
  | Timeout for runs and validations in unknown | When one is encountered |
@@ -377,10 +378,22 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
377
378
  | Searching or filtering the template list in New project | When a lab has more templates than fit on a screen |
378
379
  | Memory reading investigations: an earlier investigation's account and findings in a later campaign's assignment (today memory skips investigations both ways) | When an optimize campaign follows an investigation on the same project |
379
380
  | A size cap on an investigation run's collected `out/` | When a run is seen copying something large back to the host |
381
+ | Raising a running campaign's GPU-hour ceiling (the client, the host CLI, or the board; today it is set once at launch) | Done once by hand on the live database, 2026-10-23, for a task whose work outgrew its first estimate |
380
382
  | Computed completion for tasks: a checklist the ask is parsed into | When critics are seen accepting incomplete outputs |
381
383
  | Two campaigns writing a project's work directory at once | When two are seen colliding there |
382
384
  | 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
- | 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 |
385
+ | 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 | The toolshed done (see [design/toolshed.md](design/toolshed.md)); the data equivalent its own design |
386
+ | Two campaigns building the same tool at once | When a second campaign is seen submitting a name another's build holds |
387
+ | Abandoning a failed or blocked tool build to start another in the same campaign | When an implementer is seen stuck on a tool it should give up on |
388
+ | A tool whose test needs more than one GPU or 30 minutes | When one is built |
389
+ | A private tool (a license that forbids redistribution): a squashfs on the cluster's shared storage | When one arrives |
390
+ | A `toolshed` command for laptops that picks podman or docker and mounts the working directory | When the skill's `podman run` line proves insufficient |
391
+ | A tool build whose branch falls behind the shed's `main` under its strict protection: the merge is refused and retried each tick with a log line | When seen: update the branch through GitHub's API, then the checks again |
392
+ | Declared parameters per tool command | When agents are seen misusing commands from prose |
393
+ | A tool's image per GPU architecture (Arm) | When an Arm partition is wanted |
394
+ | `flyloft tools` and the catalogue in the researcher's skill | When a researcher's agent needs the shed's contents while writing a brief |
395
+ | A tool run on a cluster without pyxis | When a cluster without it is configured |
396
+ | A bar fixed at launch (what is measured, on what data, what counts as support) as a field of an investigation, and findings that answer it marked confirmed in place of exploratory; today the bar goes into the brief and the critic judges claims against it, without the label | When a researcher comes to a question knowing what settles it and wants the label |
384
397
  | 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
398
  | The flyloft skill for Codex and OpenCode | Done 2026-10-22: `flyloft install` serves every harness found |
386
399
  | `flyloft logout`, several hosts per laptop, refreshing a session before it expires | When one is missed |
@@ -392,6 +405,13 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
392
405
  | 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
406
  | 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
407
  | 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 |
408
+ | Watching archive jobs: a failed one leaves its directory and log on the cluster and tells no one ([design/run-storage.md](design/run-storage.md) §6) | When a failed archive goes unnoticed |
409
+ | An object storage key scoped to `flyloft/`, under a CoreWeave principal of flyloft's own, in place of Sarah's key ([design/run-storage.md](design/run-storage.md) §6) | When the lab wants the exposure closed |
410
+ | A cap on the disk that live runs and campaigns hold on an archiving cluster ([design/run-storage.md](design/run-storage.md) §6) | When a campaign fills `/mnt/data` |
411
+ | Archiving validations and the shared project work directory; object storage on-prem ([design/run-storage.md](design/run-storage.md) §6) | When either fills its volume |
412
+ | A lab's own job-script settings: first extra `#SBATCH` directives per cluster and GPU type (`--qos`, `--constraint`, `--reservation`), then a host-side prologue before the srun step (module loads), then a container runtime other than pyxis; a full template override only if those fall short, checked by doctor against what flyloft relies on (the run directory's output with append, the single srun step that ignores TERM, the `finished` marker, the `FLYLOFT_*` variables) | When a lab or a cluster needs a directive or runtime flyloft does not write |
413
+ | The archive's transfer per cluster (endpoint, tool, key names) in place of CoreWeave's s5cmd fork, `http://cwlota.com` and `COREWEAVE_S3_*` ([design/run-storage.md](design/run-storage.md) §2) | When a cluster outside CoreWeave needs a `work_dir_archive` |
414
+ | Archiving a run interrupted at the preemption cap only once squeue no longer lists its job: today it is archived in the tick that cancels it, so a file written during the kill grace can fail the check (the directory then stays, marked archived) ([design/run-storage.md](design/run-storage.md)) | When a cap-interrupted run's archive is seen to fail |
395
415
 
396
416
  ## References
397
417
 
@@ -23,22 +23,26 @@ campaign's metric. A project is one GitHub repository plus one registration on t
23
23
  | `environment_setup` | Shell lines run before the job enters the repository |
24
24
 
25
25
  Every run (a campaign's baseline, each test, a validation) is one Slurm job. Its script, as flyloft writes it (abridged:
26
- `--partition`, `--account`, `--cpus-per-task`, `--mem`, and `--container-mounts` appear when set):
26
+ `--partition`, `--account`, `--cpus-per-task`, `--mem`, and `--container-mounts` appear when set, and the lines that
27
+ pass a preemption's SIGTERM to the run command are left out):
27
28
 
28
29
  ```bash
29
30
  #!/bin/bash
30
31
  #SBATCH --job-name=flyloft
31
32
  #SBATCH --output=<run dir>/slurm-%j.out
33
+ #SBATCH --open-mode=append # a restarted run adds to the log
32
34
  #SBATCH --time=<the campaign's time limit; 00:30:00 for a validation>
33
35
  #SBATCH --gres=gpu:<count> # with the GPU type's gres name, as the cluster's file says
34
- #SBATCH --container-image=<image> # when the cluster runs jobs in a container
36
+ srun --ntasks=1 --container-image=<image> bash -c ' # one step, in the cluster's container
35
37
  set -euo pipefail
38
+ export FLYLOFT_PROJECT_WORK="<the project's directory on the cluster>"
36
39
  <environment_setup>
37
40
  export FLYLOFT_SEED=<seed> # each replicate's seed; 0 for a validation
38
41
  export FLYLOFT_SMOKE=1 # a validation only
39
42
  cd "<run dir>/code"
40
43
  <run_command>
41
44
  touch "<run dir>/finished"
45
+ '
42
46
  ```
43
47
 
44
48
  What follows from it:
@@ -53,6 +57,10 @@ What follows from it:
53
57
  - **`environment_setup` runs before the `cd`.** Its paths are absolute, or values the command resolves after the
54
58
  `cd`, such as `export DATA_DIR=data`.
55
59
  - **One job is the whole run.** Training, evaluation, and writing results all happen inside the time limit.
60
+ - **A run may restart.** Flyloft runs at the cluster's lowest tier, where a job running longer than 30 minutes can
61
+ be preempted and restarted from the top. The run command resumes from its own checkpoints, saved more often than
62
+ every 30 minutes, or starts over cleanly, writing outputs to a scratch path and moving them into place only when
63
+ complete. A preempted run gets SIGTERM five minutes before it is stopped, time enough to save a checkpoint.
56
64
 
57
65
  ## 2. The contract
58
66
 
@@ -186,6 +194,7 @@ Reading a validation's report:
186
194
  | passed | The job finished and wrote a flat object of numbers, which the report lists | The project runs: launch |
187
195
  | failed | The job exited with an error, ran out of time, or the cluster refused it; the report shows the log's last lines | A file nobody committed, a dependency the environment lacks, a wrong path, a crash in the evaluation |
188
196
  | incomplete | The job finished, but `results_path` is missing or is not a flat object of numbers | Results written somewhere else, a nested value, a metric written as a string |
197
+ | interrupted | The cluster preempted the job and never restarted it; this says nothing about the project | Validate again |
189
198
 
190
199
  A failed or incomplete validation means fix the repository, push, and validate again; the report is written to be
191
200
  pasted back to you. A passed validation shows that the code runs at all, not that a campaign will succeed. A smoke
@@ -7,7 +7,7 @@ An investigation is a campaign whose researcher fixes a question and nothing els
7
7
  The investigate mode is staged in four parts, each with its own design when it begins:
8
8
 
9
9
  1. **Investigate, solo and exploratory.** This document.
10
- 2. **A test fixed in advance.** The researcher fixes what will be measured, on what data, and what would count as support; findings answering it are confirmed, and critics judge claims against it.
10
+ 2. **A test fixed in advance.** The researcher fixes what will be measured, on what data, and what would count as support; findings answering it are confirmed, and critics judge claims against it. Parked on 2026-10-22: a bar the researcher knows goes into the brief, and the critic judges against the brief today; the *confirmed* label waits on the deferred list ([ARCHITECTURE.md](../ARCHITECTURE.md) §8).
11
11
  3. **The toolshed.** Tools shared across the lab, a tool-builder role, review before any agent calls a tool, agents looking there before they build.
12
12
  4. **Investigate at swarm size.** Generators, islands, and posture in an investigation, and per-hypothesis branches with an integrator, when a real campaign needs them.
13
13
 
@@ -164,7 +164,7 @@ tests/e2e/test_live.py tier two
164
164
 
165
165
  ## 9. Out of scope
166
166
 
167
- - A test fixed in advance and confirmed findings (part 2).
167
+ - A test fixed in advance and confirmed findings (part 2, parked; deferred).
168
168
  - The toolshed and the tool-builder (part 3).
169
169
  - Generators, islands, posture, several implementers, per-hypothesis branches, and an integrator in an investigation (part 4).
170
170
  - Memory reading investigations (deferred).
@@ -0,0 +1,218 @@
1
+ # Preemption and Interrupted Runs: Design
2
+
3
+ Status: presented 2026-10-20; revised and approved 2026-10-23 with the restart contract, the preemption cap, attempt-aware timing, and the appended log; built 2026-10-23, tiers one and two passed. Charging was decided in conversation on 2026-10-20: an interrupted run is charged its used GPU-time, as any ended run. On 2026-10-23 that was carried across attempts: the time-limit cap applies per attempt (§6).
4
+
5
+ Flyloft runs at each cluster's lowest scheduling tier, where every other tier preempts it: bcr's `low` QOS (slinky-config #177), on its GPU partitions and its `cpu` partition alike, and CoreWeave dev's `hpc-low`. Both clusters run `PreemptMode=REQUEUE`. On bcr, read live on 2026-10-23: `mid`, `service`, and `high` preempt `low`, and `low` is exempt for its first 30 minutes. `hpc-low` documents REQUEUE with a 300-second grace. Flyloft's job script does not opt out, so a preempted job restarts under the same job id and runs its script from the top.
6
+
7
+ Three things go wrong today:
8
+
9
+ - `runs/observe.py` maps `PREEMPTED` to `failed`, so a preemption is recorded as a failed experiment: negative evidence that never existed.
10
+ - Nothing tells a project that its run may restart. A run that cannot pick up where it stopped begins again from zero. A long run on a busy tier can begin again forever, the livelock the lab hit on `hpc-low` on 2026-07-29: six training runs died between 29 and 76 minutes in, five of them before their first checkpoint.
11
+ - The record measures a requeued run from its first start. The over-limit warning and an in-flight run's charge both count the time the job then waited in the queue.
12
+
13
+ This change does four things:
14
+
15
+ - It makes a requeued run something waited on, and a lost one a run with no verdict.
16
+ - It states what a run owes a restart.
17
+ - It bounds how much time a restarting run may use.
18
+ - It times and charges each attempt by what the cluster reports.
19
+
20
+ ## 1. Scenario
21
+
22
+ A campaign on a low tier submits a run whose job is preempted mid-flight.
23
+
24
+ - **Requeued and finishes.** Slurm requeues the job, and the run reads queued, then running, again. The run command resumes from its own checkpoint, or starts over cleanly. It finishes, and the campaign records its metrics as usual.
25
+ - **Preempted again and again.** Once the run's attempts together have used twice its time limit, flyloft cancels the job and the run ends `interrupted`, naming the time used and the preemption count. The agent that requested it wakes, and its assignment names the interruption as infrastructure. It re-requests the tree, splits the work into shorter steps, or makes the run resume from a checkpoint. The measure-once rule does not block a re-request, since nothing was measured.
26
+ - **The requeue never reappears.** The run ends `interrupted` the same way, without the count.
27
+ - **Charging.** Every attempt is charged the time the cluster reports it used, as any ended run (§6).
28
+ - **Visibility.** The board's run pages and `flyloft campaign show` show `interrupted`, the raw scheduler state, and the number of preemptions. The log tail keeps every attempt.
29
+
30
+ **Tier one** scripts all three outcomes in the fake cluster.
31
+
32
+ **Tier two** is a live drill on bcr. A small `low`-QOS job, whose command resumes from a step counter it writes, is preempted by a `mid`-QOS job and watched end to end. The drill also records four behaviors code cannot settle:
33
+
34
+ - what squeue shows inside the kill window;
35
+ - what `sacct --duplicates` writes for a requeued job's attempts;
36
+ - whether Slurm truncates the job's output file on restart;
37
+ - whether the command resumed from its counter.
38
+
39
+ **Tier three:** the researcher sees the interrupted run on the board.
40
+
41
+ ## 2. The map
42
+
43
+ `PREEMPTED` maps to `interrupted` instead of `failed`. Nothing else in the map changes. A job the queue still lists wins over accounting, as today, so a requeued job reads running (COMPLETING), then queued, then running again, and stays active throughout. Accounting's last word is PREEMPTED only when the queue no longer lists the job and no requeue record follows: the job is gone for good, and the run ends `interrupted` at once.
44
+
45
+ The drill (§14) showed bcr keeping a requeued job in the queue from the kill to its restart, and CoreWeave's accounting records the same requeue shape. A first design waited ten observations on a `preempted` state before giving up, in case a cluster dropped the job between the kill and the requeue. No cluster has been seen doing that, so the state and its wait were removed (decided 2026-10-23).
46
+
47
+ TIMEOUT, FAILED, and OUT_OF_MEMORY stay `failed`. A run past its own limit, or dying on its own, is evidence about the run, and `settled()` keeps rescuing a job the controller lost after the command exited 0. NODE_FAIL and BOOT_FAIL stay on the deferred list; their entries are the same one-line change when their trigger fires.
48
+
49
+ ## 3. What a run owes a restart
50
+
51
+ A requeued job runs `job.sh` again from the top, in the same run directory, with the same `$FLYLOFT_OUT` and `$FLYLOFT_PROJECT_WORK`. Slurm sets `SLURM_RESTART_COUNT` on a restart. Every run command must do one of two things:
52
+
53
+ - **Resume.** It checkpoints into a directory that survives the restart and, on start, continues from the latest complete checkpoint. A run's checkpoints go under the campaign's own directory in `$FLYLOFT_PROJECT_WORK` (`campaigns/<id8>/`), in a subdirectory named by `SLURM_JOB_ID`, which a requeue keeps. A run longer than the tier's exempt window checkpoints more often than that window, 30 minutes on both clusters, or it can lose every attempt to the next preemption.
54
+ - **Start over cleanly.** It writes to a scratch path and moves its outputs into place only when complete, so a restart never finds a half-written file and never appends to one.
55
+
56
+ **The warning.** A preemption on either cluster starts a five-minute grace (`low` and `hpc-low` both set `GraceTime` 300). At its start Slurm sends SIGTERM to the job's steps, not to its batch shell; at its end it sends SIGTERM to the batch shell, and SIGKILL after `KillWait`. So the job script runs the work as one `srun` step, which hears the first SIGTERM, and the batch shell ignores TERM (`trap '' TERM`) so it outlives the step. The container goes on the `srun` line, because pyxis removes a batch-level container when the batch shell exits, even with the work still running (§14). Slurm signals only the step's own process, the shell that wraps the run command, so that shell traps TERM and sends it to its whole process group (`kill -TERM 0`), which is the run and nothing outside the step. It then waits until every process of the run has exited, not only its direct child: the run holds a lock on an inherited file descriptor, and the shell waits for the lock (`flock`). Without that wait, a shell between the step's shell and the work dies on the TERM, the step ends, and pyxis removes the container under the work still saving. A run command that saves a checkpoint on SIGTERM has the five minutes. One that ignores it is killed at the grace's end.
57
+
58
+ Training runs resume. Data preparation, evaluation, and other runs that cannot checkpoint start over. A run under 30 minutes is exempt from preemption and owes nothing. No run declares which kind it is: the rule is the same for both, and the run command is where it is kept.
59
+
60
+ This is prose, not code:
61
+
62
+ - `NEW_PROJECT.md` gains a contract line beside "One job is the whole run". An optimization's `run_command` is the project's, so the project keeps the rule.
63
+ - The implementer's rules (`_cj_implementer_rules.md.j2`) gain one bullet, beside the two directories. An investigation's or task's run command is the implementer's, so the implementer keeps the rule.
64
+
65
+ ## 4. The preemption cap
66
+
67
+ A run whose attempts together have used twice its time limit is ended (decided 2026-10-23, in place of a count of preemptions). When a preempted run's summed elapsed time (§6) reaches twice its time limit, the service cancels the job, records the run `interrupted` itself rather than reading the cancellation back as `canceled`, collects it, and posts it as any terminal run. The factor is a constant, not configuration. A run that has never been preempted is bounded by Slurm's own limit and never reaches the cap.
68
+
69
+ The cap is on time rather than on a count of preemptions, because flyloft cannot see whether a run resumed. Time used tells the two kinds of run apart:
70
+
71
+ - **A run that resumes** loses at most its checkpoint interval and its startup per preemption, so its total stays near the work it needs. A 72-hour training run preempted eight times, losing about 35 minutes each, uses about 77 hours and finishes.
72
+ - **A run that starts over** repeats its work on every attempt, so its total climbs past its limit. A 10-hour clustering run preempted at 9 hours finishes at 19. One that keeps being preempted reaches 20 and is ended, and its agent splits the work.
73
+
74
+ The preemptions are counted too, for the record: §6's sacct call lists every attempt, and those that ended PREEMPTED are counted. The run row stores the count, and the board and the interruption's diagnostics show it.
75
+
76
+ A run is never moved to a higher QOS. Which tier flyloft may use is the lab's decision (slinky-config #177), and a higher tier preempts other people's jobs. The deferred list gains the row in §11.
77
+
78
+ ## 5. A requeue that never comes
79
+
80
+ A preempted job the queue no longer lists ends its run `interrupted` on the observation that sees it (§2), its reason `preempted and not requeued`. Nothing is waited on and nothing is counted, so nothing persists across a service restart.
81
+
82
+ ## 6. Timing and charging
83
+
84
+ **Each attempt is its own clock.** Slurm gives every attempt a fresh `--time`. The record follows:
85
+
86
+ - `started_at` is the current attempt's start: a run is stamped again when it reads running after reading queued, or when accounting shows a new preemption, which catches a requeue that restarts within one tick.
87
+ - The stored preemption count never falls: a tick on which accounting answers nothing keeps the count it had.
88
+ - The over-limit warning measures the current attempt.
89
+ - The board shows the preemption count beside the start, so the reset is visible.
90
+
91
+ **Elapsed is summed across attempts.** By default sacct reports only a requeued job's latest attempt, so the observation's sacct call gains `--duplicates`. `parse_observation` then sums `ElapsedRaw` over a job's records, and keeps the last record's state. A run that finishes after a requeue charges every attempt.
92
+
93
+ **An in-flight run is charged its observed elapsed time.** `charge()` uses the run's last observed `elapsed_seconds` while it is active, falling back to time since `started_at` when there is none. Time spent waiting in the queue after a requeue is never charged. The observation is at most one tick old.
94
+
95
+ **Charged as today otherwise.** An interrupted run bills the GPU time the cluster reports it used. The consequence is accepted and named here: a preemption-heavy low-tier campaign spends budget on attempts that taught nothing. Both the ledger's node-hours and the campaign's charge show that time.
96
+
97
+ **The cap is per attempt (decided 2026-10-23).** Each attempt may use up to the time limit, so `charge()` caps a run at the limit times its number of attempts. A run with a 10-hour limit that runs 8 hours, is preempted, and then runs 9 more is charged 17 hours, not 10. Every attempt is billed what it used, as the 2026-10-20 decision says. The preemption cap (§4) ends a run near twice its limit, so in practice no run is charged much more than that.
98
+
99
+ ## 7. The log
100
+
101
+ The job script gains `#SBATCH --open-mode=append`, so a restart adds to `slurm-<job>.out` instead of replacing it. The log tail then shows the preemption and what each attempt did. The drill confirms the truncation it guards against.
102
+
103
+ ## 8. The record and the agents
104
+
105
+ `interrupted` joins `canceled` as a terminal `RunStatus` that carries no verdict. The result body carries:
106
+
107
+ - status `interrupted`, with metrics null;
108
+ - the raw scheduler state and the preemption count, beside the log tail. `scheduler_state` already persists the raw word.
109
+
110
+ The agent that requested the run wakes on the result entry, as on any result. The roles that judge results, implementer and critic, gain one sentence: an interrupted run is infrastructure, not evidence about the hypothesis. A run interrupted by the cap is too long for the tier; the move is to re-request it shorter or resuming (§3).
111
+
112
+ The integrator's merged tree is measured by integration runs. When a seed's integration run is interrupted, the integrator requests the same merge commit again instead of reverting the finding, but only when every seed without the metric was interrupted, and each of those only once. A seed that failed or reported no metric is evidence about the merge, and a seed interrupted twice does not fit the tier; either way the integrator reverts, as today.
113
+
114
+ A baseline has no requester. One that does not finish stops the campaign, as today, but an interrupted one stops it with its own reason (decided 2026-10-23): "the baseline run was interrupted: preempted on the cluster's lowest tier; nothing is wrong with the project; start the campaign again". It is not resubmitted (§13).
115
+
116
+ The population and result renderings print the state beside the run, as they print `failed` today. No assessment rule changes: assessment is the agents' judgment, and nothing auto-refutes today.
117
+
118
+ ## 9. Validations
119
+
120
+ `_validate` shares the map and `RUN_ACTIVE`, so today a preempted smoke run reads `failed`: a project blamed for the cluster's tier. A smoke run finishes well inside 30 minutes, inside the exempt window, so preemption reaches it only in the kill window of a job that started before it. It gets the same treatment:
121
+
122
+ - A preempted smoke run that is requeued stays queued or running, as a run does.
123
+ - One that is never requeued ends `interrupted`, a new validation verdict beside passed, failed and incomplete, its log tail naming the preemption. `incomplete` keeps its one meaning: the job finished without readable results.
124
+ - Re-running is the existing path: `start_validation` from the CLI or the board.
125
+
126
+ The preemption cap does not apply to validations: a 30-minute job inside its own exempt window cannot be preempted often enough to use an hour.
127
+
128
+ ## 10. Failure behavior
129
+
130
+ | Condition | Behavior |
131
+ |---|---|
132
+ | A run's job is preempted and requeued | The run stays active and reads queued or running again. The command resumes or starts over (§3), and the run finishes or fails on its own merits |
133
+ | A preempted run's attempts have used twice its time limit | Flyloft cancels it, and the run ends `interrupted` with the time used and the count. No metrics; the requester is woken and can re-request shorter or resuming |
134
+ | A run that resumes is preempted many times | It keeps running: each preemption costs it little time, so it finishes under the cap |
135
+ | A preempted job is never requeued | The run ends `interrupted` at once; no metrics; the requester is woken and can re-request |
136
+ | An observation lands inside the kill window | The job reads COMPLETING, so the run stays running; no result is posted |
137
+ | Accounting answers nothing for a tick | The preemption count and charge keep what they had |
138
+ | An integration run is interrupted | The integrator measures the same merge again, once per seed |
139
+ | A requeued run waits in the queue | Not charged, not over its limit; its next attempt is timed from its own start |
140
+ | A run that starts over is restarted | It finds no half-written output, by the §3 rule |
141
+ | The cluster stops answering | The observation pass fails and counts toward nothing; the over-limit and unanswered warnings govern, as today |
142
+ | A run's job dies on its own (TIMEOUT, FAILED, OUT_OF_MEMORY) | `failed`, as today, with `settled()`'s rescue unchanged |
143
+ | A campaign's baseline is interrupted | The campaign stops, its reason naming the preemption, not the project |
144
+ | A validation's smoke run is preempted | Requeued, it runs on; never requeued, it ends `interrupted` with the reason in its log tail |
145
+ | A researcher cancels | `canceled`, unchanged |
146
+
147
+ ## 11. The docs
148
+
149
+ - **`ARCHITECTURE.md`, the run lifecycle paragraph.** The state list gains `interrupted`. The paragraph says a preempted job is requeued by the cluster and waited on, and is interrupted when it is never requeued or its attempts have used twice its time limit. Interrupted is infrastructure, not a verdict, and is charged for the time it used.
150
+ - **`ARCHITECTURE.md`, the deferred list.** It gains one row: "A run at a higher tier than the lab's lowest, for work that cannot be split under the preemption cap", triggered "when a campaign hits the cap on work that cannot be shortened or made to resume".
151
+ - **`NEW_PROJECT.md`, the contract.** It gains the §3 line: a run resumes from its own checkpoint or starts over cleanly.
152
+ - **`NEW_PROJECT.md`, the validation verdict table.** It gains an `interrupted` row: the cluster preempted the job and never restarted it, which says nothing about the project; validate again.
153
+
154
+ ## 12. Code layout
155
+
156
+ ```
157
+ src/flyloft/runs/observe.py the map entry: PREEMPTED → interrupted
158
+ src/flyloft/runs/ssh.py sacct --duplicates; elapsed summed, preemptions counted
159
+ src/flyloft/runs/staging.py --open-mode=append; the work in one srun step, the shell ignoring TERM (§3)
160
+ src/flyloft/record/models.py RunStatus, RUN_TERMINAL, RUN_ACTIVE
161
+ src/flyloft/record/actions.py charge(): observed elapsed in flight; the cap per attempt
162
+ src/flyloft/migrations/ the run's preemptions
163
+ src/flyloft/service/tick.py the _observe branch, started_at per attempt, the cap, _validate
164
+ src/flyloft/record/collect.py build_result_body's status literal and preemption count
165
+ src/flyloft/turns/roles/_cj_implementer_rules.md.j2 the restart rule (§3)
166
+ src/flyloft/turns/roles/implementer.md.j2, critic.md.j2 the interrupted-run sentence
167
+ src/flyloft/turns/assembly.py, integrator.md.j2 measuring an interrupted merge again
168
+ src/flyloft/web/ RunStatus and the preemption count where they render; types.ts regenerated
169
+ docs/ARCHITECTURE.md, docs/NEW_PROJECT.md §11
170
+ tests/fakes/cluster.py scripted preemptions, requeues, and attempt records
171
+ tests/ §1's scenarios, the cap, the parse sum, per-attempt timing, the integrator
172
+ ```
173
+
174
+ ## 13. Out of scope, and where it goes
175
+
176
+ - **Flyloft-side resubmission of interrupted runs.** This is already the NODE_FAIL row's idea on the deferred list. Slurm requeues on both clusters, and the requester re-requests, so nothing builds it until preemptions are seen eating a real campaign.
177
+ - **A higher tier for work that cannot be split.** This is the deferred row in §11.
178
+ - **An earlier warning (`--signal=B:USR1@<seconds>`).** The `srun` step's SIGTERM at grace start gives five minutes (§3). A longer warning would need `--signal` in the job script and, on bcr, `PreemptParameters=send_user_signal`, which runs on dev alone (slinky-config #204). Flyloft adds nothing until a run is seen needing more than five minutes.
179
+ - **A preemption-aware budget policy, charging interrupted runs nothing.** Decided against on 2026-10-20: node-hours and the campaign's charge both record the used time.
180
+
181
+ ## 14. Brought in line at close
182
+
183
+ **The drill (tier two), bcr, 2026-10-23.** Two `low` jobs on `cpu` (128 CPUs each, the counter in `scripts/preemption_drill.sh`), one with `--open-mode=append` and one without, were preempted 37 minutes in by one `mid --exclusive` job. It was submitted at 16:14 UTC and preempted them at 16:19:30.
184
+
185
+ - **The kill window.** squeue showed both jobs COMPLETING for about 10 seconds, then PENDING. The job never left the queue: a requeued job stays visible as running, then queued, then running. This is what removed the `preempted` state (§2).
186
+ - **Accounting.** `sacct -D` wrote the first attempt as `PREEMPTED` with its own ElapsedRaw (2217). At the requeue it added a new record, `PENDING|0`, which became `RUNNING` with its own growing ElapsedRaw. Summing the records gives the job's time across attempts. PREEMPTED is the word counted.
187
+ - **The restart.** The requeued jobs started again at 16:25:00, about five minutes after the kill, with `SLURM_RESTART_COUNT=1`. The counter resumed at step 174 of 174 saved.
188
+ - **The log.** Without `--open-mode=append`, the restart truncated the log: only the second attempt's lines remained. With it, the log kept the first attempt, Slurm's `CANCELLED ... DUE TO PREEMPTION` line, and the second attempt.
189
+ - **`MaxBatchRequeue = 10`** on bcr and on CoreWeave. slurm.conf documents it for requeues after a launch or prolog failure, not for preemption, so it does not bound a preempted run; the cap (§4) does. This has not been checked on a job preempted more than ten times.
190
+
191
+ **CoreWeave.** `sacct -D` on coreweave-b200 job 25439_98 (preempted on `hpc-low` on 2026-10-12) shows the same shape: three `PREEMPTED` records with their own ElapsedRaw (38381, 2397, 2422), then `COMPLETED`. The parse counts both clusters' records alike.
192
+
193
+ **Through flyloft, partlow, 2026-10-23.** This branch ran as a scratch host on partlow (a scratch database restored from a backup, its own config and data directory). An investigation's one CPU run on bcr `cpu` counted to 270 in ten-second steps, checkpointing a counter. A `mid --exclusive` job preempted it 37 minutes in.
194
+
195
+ - **The record.** The run read running (COMPLETING), then queued, then running. It ended `finished` with 2 preemptions and 3449 seconds elapsed, the sum of its three attempts' records (2252, 996, 201). `started_at` restarted with each attempt. No result was posted until it finished.
196
+ - **The work.** It resumed at step 198, and the log kept all three attempts.
197
+ - **An extra requeue after a clean exit.** The second attempt's batch step completed with exit 0 at 19:29:19, yet Slurm recorded the attempt `PREEMPTED` and requeued it. Nothing preempted it: the controller log has no preemption decision near then, only `requeue JobId=3098 per user/system request` after the exit. This is a bug in Slurm 26.05.4. A requeue after a preemption's grace leaves the job's `GRACE_PREEMPT` flag set, so the next attempt's normal exit is taken as a preemption and requeued once more. Slurm 26.05.5 fixes it (upstream `c960622101`). It was reproduced on bcr's dev cluster on 26.05.4 and is gone there on 26.05.5. bcr prod still runs 26.05.4. CoreWeave's 25.05.3 has the same code path; that is read in source, not reproduced. The third attempt resumed at 270 and finished in seconds. A run that starts over would have repeated its work. The exempt window is per attempt: a requeue resets the job's start time.
198
+ - **A preempted attempt outlived its container.** After Slurm's `CANCELLED ... DUE TO PREEMPTION` line, the first attempt's script ran on for seconds without the container's filesystem (`sleep`, `tee` and the checkpoint path were missing) and spun to its end without writing. The cause is how `low`'s grace signals a job. At grace start Slurm sends SIGTERM to the job's steps but not to the batch shell, and the job then ran the command in the batch shell with no steps, so the run command heard nothing. At grace end Slurm sends SIGTERM to the batch shell. `job.sh` did not trap it, so the shell died and pyxis removed the container while the run command, the shell's child, was still running. A dev probe whose shell trapped TERM kept its container until the SIGKILL 30 seconds later. Had the checkpoint path stayed writable, the orphan would have written a false last step. The job script now runs the work as an `srun` step in its own container, behind a batch shell that ignores TERM (§3). A run that resumes should still write a checkpoint only when it is complete (§3 says "latest complete checkpoint").
199
+
200
+ **The live tier, partlow, 2026-10-23.**
201
+
202
+ - **Passed:** a toy validation, and an investigation's CPU run and GPU run.
203
+ - **Failed on the scratch setup, not on this branch:**
204
+ - The four campaign tests needed projects and scripted harnesses the scratch config lacked.
205
+ - The two tests that create a repository were refused by partlow's GitHub App.
206
+ - **Untested live:** the four campaign tests were not rerun. The in-flight charge from accounting's elapsed time is therefore exercised live only by runs that are not budget-gated, because the fake cluster reports no elapsed time for a running job.
207
+
208
+ **Built differently from the plan:**
209
+
210
+ - **No `preempted` state** (§2, §5). The plan built it with a ten-observation wait; the drill and a review showed it guarded against a shape no cluster produces and carried three bugs of its own, so it was removed.
211
+ - **The integrator** (§8) was not in the plan. A review found an interrupted integration run would have reverted an accepted finding.
212
+ - **`record/views.py` is unchanged.** Stamping `started_at` per attempt makes the over-limit warning measure the current attempt.
213
+ - **The validation verdict.** An interrupted validation first ended `incomplete`; it got its own `interrupted` verdict so `incomplete` keeps one meaning (decided 2026-10-23).
214
+ - **The fake cluster** preempts as bcr does: COMPLETING in the queue while accounting records PREEMPTED, then PENDING. A job that is never requeued leaves the queue as PREEMPTED.
215
+ - **The job runs as an `srun` step** (§3). The plan left the job script's shape alone; the drill's orphaned attempt showed the run command heard no warning and outlived its container. Tried on bcr `cpu` on 2026-10-26:
216
+ - **A real preemption (job 3182)** of a step that only wrapped the run command in `bash -c`. The step was signaled one second after the `mid --exclusive` job's submission, and the attempt was recorded `PREEMPTED` and requeued. But only the wrapping shell got the TERM. It died, the step ended, and the run command, which never heard the signal, ran on without its container. This is why the step's shell forwards TERM and waits.
217
+ - **Signals sent by hand** (`scancel -b --signal=TERM`, then `scancel --signal=TERM`), standing in for the grace's end and start without waiting out the 30-minute exemption. With the shipped job script (job 3189), the batch shell ignored its TERM, the step's TERM reached a run command two shells down, and the run spent five seconds saving with its container intact before the job ended. A first try that waited on the shell's child alone (job 3186) ended the step while the run was still saving.
218
+ - **Known gap.** A template measurement's reference run time sums a preempted seed's attempts, so it includes time lost to preemption.
@@ -0,0 +1,158 @@
1
+ # Run Storage on Each Cluster: Design
2
+
3
+ Status: approved 2026-10-26. Tiers one and two passed on 2026-10-26: tier two archived a run and a campaign folder on coreweave-rtx6000 to brineylab-us-east.
4
+
5
+ A run's directory on the cluster holds its code checkout, its Slurm log, and whatever its command writes there: a few files for an analysis, hundreds of GB of checkpoints for an optimize campaign's training run. An investigation's or task's own products, its checkpoints included, go instead to the campaign's folder in the project work directory, `$FLYLOFT_PROJECT_WORK/campaigns/<id8>/`, where its later runs find them. flyloft copies back only the small part (`results.json`, `slurm.out`, an investigation's `out/`). Everything else stays where it was written, and nothing is ever deleted.
6
+
7
+ Two things are wrong with where that is today:
8
+
9
+ - **On CoreWeave it is the wrong volume.** Both clusters set `remote_work_dir = /mnt/home/flyloft-svc/flyloft`. `/mnt/home` is one 1T volume shared by every user, and on b200 it reached 80% on 2026-10-23 from one user's checkpoints. `/mnt/data` (4T) is bigger but not big enough either: b200 has under 900G free.
10
+ - **On CoreWeave researchers cannot read it.** `/mnt/home/flyloft-svc` is `0750 flyloft-svc`. Getting a checkpoint out of a campaign would take an admin.
11
+
12
+ The researcher's rule: **any lab member may read any campaign's outputs, and no admin hands data over.**
13
+
14
+ This change keeps on-prem on davyjones. On CoreWeave, runs move to `/mnt/data`, each ended run's directory moves to the region's object storage bucket, and each ended campaign's folder follows.
15
+
16
+ ## 1. Scenario
17
+
18
+ 1. **On-prem (bcr), runs stay on davyjones.** `/mnt/data/users/flyloft` is `brineylab_shared/flyloft`, with no quota. Every lab member's JupyterHub mounts `brineylab_shared` read-only at `~/shared`, so runs and project work directories are readable under `~/shared/flyloft/` while they run and after. Only the layout changes (below).
19
+ 2. **On CoreWeave, a campaign runs on b200.** Its runs write under `/mnt/data/flyloft/<campaign>/<run>/`, and an investigation's checkpoints under `/mnt/data/flyloft/projects/<project>/campaigns/<id8>/`.
20
+ 3. **A run ends** (finished, failed, cancelled, or timed out). flyloft collects the small files exactly as today and posts the result. The result's artifact is `s3://brineylab-eu/flyloft/runs/<campaign>/<run>/`, not the disk path.
21
+ 4. **flyloft submits the run's archive job.** It is a 0-GPU job on the cluster that copies the run directory to that prefix with s5cmd, checks that every file arrived, and only then deletes the directory from `/mnt/data`.
22
+ 5. **The campaign ends**, either `done` (goal met, concluded, stalled, or budget spent) or `stopped` by a researcher. Once its last run has ended, flyloft submits the campaign's archive job. That job moves `campaigns/<id8>/` to `s3://brineylab-eu/flyloft/campaigns/<id8>/` the same way. The shared parts of the project work directory (`envs/`, `data/`, `weights/`) stay for later campaigns.
23
+ 6. **A lab member reads the checkpoints** from the bucket with their own CoreWeave key. The `brineylab-users` access policy already gives every lab member read access to both buckets. The run page shows the run's prefix, and the campaign page shows the campaign's.
24
+ 7. **A follow-up campaign** on the same project that starts from the pilot's checkpoints pulls them from the pilot's campaign prefix in a setup run.
25
+ 8. **On rtx6000** the same happens with `s3://brineylab-us-east/flyloft/...`.
26
+
27
+ ## 2. What changes
28
+
29
+ **The cluster file.** `ClusterConfig` gains `work_dir_archive: str | None`, the bucket prefix (`s3://<bucket>/flyloft`) that ended runs and campaigns move to. A cluster without one keeps everything where it was written, as bcr does. The prefix must be in CoreWeave AI Object Storage: the archive job runs CoreWeave's s5cmd fork from the cluster's container against CoreWeave's in-cluster endpoint, `http://cwlota.com` (`ARCHIVE_ENDPOINT` in `runs/staging.py`), with the `COREWEAVE_S3_*` key. None of the three is configurable; a bucket elsewhere would fail at its first archive job. Config validation refuses a `work_dir_archive` on a cluster with no `partition`, because the archive job needs somewhere to run.
30
+
31
+ **One layout on every cluster.** `remote_work_dir` is flyloft's own top-level directory, and everything flyloft writes on a cluster sits directly under it:
32
+
33
+ ```
34
+ <remote_work_dir>/<campaign>/<run>/ a run
35
+ <remote_work_dir>/projects/<project>/ the project work directory
36
+ <remote_work_dir>/validations/<id>/ smoke runs
37
+ <remote_work_dir>/archive-logs/ archive job logs, on an archiving cluster
38
+ ```
39
+
40
+ The live files are in `brineylab/flyloft-config`, changed in a pull request there once this is deployed:
41
+
42
+ ```toml
43
+ # clusters/bcr.toml
44
+ remote_work_dir = "/mnt/data/users/flyloft"
45
+
46
+ # clusters/coreweave-b200.toml
47
+ remote_work_dir = "/mnt/data/flyloft"
48
+ container_mounts = "/mnt/data:/mnt/data"
49
+ work_dir_archive = "s3://brineylab-eu/flyloft"
50
+
51
+ # clusters/coreweave-rtx6000.toml
52
+ remote_work_dir = "/mnt/data/flyloft"
53
+ container_mounts = "/mnt/data:/mnt/data"
54
+ work_dir_archive = "s3://brineylab-us-east/flyloft"
55
+ ```
56
+
57
+ A run's prefix is `<work_dir_archive>/runs/<campaign>/<run>/`, mirroring `remote_run_dir`. A campaign's prefix is `<work_dir_archive>/campaigns/<id8>/`, mirroring its folder in the project work directory.
58
+
59
+ **The credential.** There is one object storage key. Sarah created it, and it is already in partlow's `secrets.env` as `COREWEAVE_S3_ACCESS_KEY_ID` and `COREWEAVE_S3_SECRET_ACCESS_KEY`. Both CoreWeave clusters use it, so the names are fixed rather than set per cluster. `flyloft doctor`'s secrets check requires both whenever any cluster has a `work_dir_archive`.
60
+
61
+ The ssh command that submits an archive job reads the key from stdin with `read` and exports it as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. It then runs `sbatch --export=ALL`. So the key is:
62
+ - never on a command line, where any user on the login pod could see it in the process list;
63
+ - never in the rendered script;
64
+ - never in a training job's environment, and never in `flyloft-svc`'s home directory.
65
+
66
+ Slurm does keep each job's environment in its state directory, where `flyloft-svc` and Slurm admins can read it back. Every job runs as `flyloft-svc`, so a training job set on finding the key could find it there. The key carries Sarah's full access to both buckets. That exposure is known and accepted (§6).
67
+
68
+ **The archive job.** One script serves both a run and a campaign: given a directory and a prefix, it does the following.
69
+ - **Resources:** 0 GPUs, 2 CPUs and 4G, a 12-hour limit, on the cluster's `partition` (`hpc-low`; neither CoreWeave cluster has a CPU-only partition). It runs in the cluster's container. `deeplearning.sqsh` installs CoreWeave's s5cmd fork, at `images/deeplearning/Dockerfile:131` in `devops/containers`. The job uses `--requeue`, so a preemption reruns it.
70
+ - **Copy:** `s5cmd sync` copies the directory to the prefix over CoreWeave's internal endpoint, which is a constant in the script.
71
+ - **Check:** the check lists every regular file on disk with `find` (hidden and empty files included) and compares paths and sizes against `s5cmd ls` of the prefix, independently of the copy, so a file the copy's walker skips cannot be deleted. Every file on disk must be in the bucket at the same size. The archive job's own log is left out of the check. Checking disk against bucket, not comparing totals, means a requeue that interrupts the delete cannot wedge the next attempt.
72
+ - **Delete:** the directory is deleted only when the check passes.
73
+ - **Log:** the job's log is written to `<remote_work_dir>/archive-logs/`, outside the directory it moves. If the job fails, the directory and its log stay on `/mnt/data`.
74
+
75
+ A sync is idempotent, so a requeued or resubmitted archive job finishes what an earlier one started.
76
+
77
+ **The service.** A migration adds `run.work_dir_archive` and `campaign.work_dir_archive`, each the prefix its files went to, set when its archive job is submitted. On each tick, on clusters with a `work_dir_archive`:
78
+ - **A run:** each ended run that had a Slurm job (`job_id` set) and has no archive job gets one, after it is collected and its result posted. A run cancelled before staging, or one whose `sbatch` was refused, had no job and is not archived.
79
+ - **A campaign:** each `done` or `stopped` campaign with no run still active and no archive job gets one for its `campaigns/<id8>/` folder. Only an investigation's or a task's campaign has its own folder; an optimize campaign's is not archived.
80
+ - **Retries:** a failed submission, by ssh or by `sbatch`, is logged and retried on the next tick, as staging is today.
81
+ - **Not done:** flyloft does not watch an archive job after submitting it (§6). Archive jobs do not count toward `max_outstanding_jobs` or run slots.
82
+
83
+ The cluster client (`runs/client.py`) gains one method that writes the archive script and runs `sbatch` in a single ssh call, with the key on stdin. The fake cluster in `tests/fakes/cluster.py` implements it too.
84
+
85
+ **The result.** On a cluster with a `work_dir_archive`, `build_result_body` names the run's prefix as its artifact instead of the disk path. On other clusters nothing changes.
86
+
87
+ **The implementer's assignment.** On an archiving cluster, the shared `_cj_implementer_rules.md.j2` partial adds one sentence after the campaign's folder: when the campaign ends, the folder moves to its campaign prefix. It also says that a later campaign that needs those products pulls them from there.
88
+
89
+ **What does not move.**
90
+ - *The shared project work directory* (`envs/`, `data/`, `weights/`) stays on `/mnt/data`, because later campaigns rely on it.
91
+ - *Validations* (`<remote_work_dir>/validations/<id>`) are 30-minute smoke runs and stay where they ran.
92
+
93
+ **Before deploying.** These are admin steps, not code:
94
+ - On each CoreWeave cluster, create `/mnt/data/flyloft` owned by `flyloft-svc`, group `sg-coreweave-slurm-users`, setgid and group-readable, so lab members can also read a live run on disk. `/mnt/data` is `0777`.
95
+ - On bcr, with no flyloft run active, move the contents of `/mnt/data/users/flyloft/runs/` up one level as `flyloft`, then deploy `bcr.toml`'s new `remote_work_dir`. It is one ZFS dataset, so each move is a rename. flyloft computes a run's directory from the cluster file, so a run active across the change would be collected from the wrong path.
96
+ - Check that the live database has no runs or campaigns on `coreweave-*`. Changing `remote_work_dir` would otherwise point archive jobs at paths that don't exist. No campaign has run on CoreWeave yet; if one has by then, mark its runs and campaigns as not to archive.
97
+
98
+ ## 3. The board
99
+
100
+ - **A run's page** shows its bucket prefix as where its files are, on an archiving cluster.
101
+ - **A campaign's page** shows its campaign prefix among the header's facts, once its archive job is submitted.
102
+ - `npm run types` regenerates `web/src/api/types.ts`.
103
+
104
+ **Where an agent finds the files.** The host API's cluster view (`ClusterView`, behind `flyloft options`) carries each cluster's `remote_work_dir` and `work_dir_archive`. `flyloft options` prints, under each cluster, where a run's files and a campaign's own folder are while they run and, on an archiving cluster, where they go after. The flyloft skill says to read that, with the campaign and run ids from `flyloft status <id> --json`, and that an `s3://` path is read with the researcher's own object storage key. The skill stays lab-neutral: it names no lab's paths.
105
+
106
+ ## 4. Tests
107
+
108
+ **Tier one.** The fake cluster gains a `work_dir_archive`, and a stub `s5cmd` on its `PATH` maps `s3://` to a local directory, so the real archive script runs. The checks are what a researcher would check:
109
+ - **An investigation on the archiving cluster** runs a setup run and a training run that writes fake checkpoints to its campaign folder and to its run directory, then concludes.
110
+ - Each run's result names its prefix.
111
+ - Each run's directory is under the stand-in bucket and gone from the work directory.
112
+ - After the campaign ends, `campaigns/<id8>/` is too, and the project's `envs/` is untouched.
113
+ - The board shows both prefixes.
114
+ - **A stopped campaign** with a run in flight is archived only after that run ends.
115
+ - **A cluster without a `work_dir_archive`** leaves runs and the campaign folder where they were written, as today.
116
+ - **A failed check:** the stub drops one file, and the directory is not deleted.
117
+
118
+ **Unit tests** cover the archive script and the submission:
119
+ - the key appears nowhere in the rendered script or in the ssh command line;
120
+ - deletion follows only a passing check.
121
+
122
+ **Tier two** (live, scripted, no model tokens) runs on prod `hpc-low` on b200, which Sarah allowed for this test. Dev has had no Slurm nodes since 2026-10-23.
123
+ - **Setup:** a reservation of a few hours for `flyloft-svc` is created at test time, on a node with a GPU free, after checking the shape of the jobs on it. `SBATCH_RESERVATION` goes in `flyloft-svc`'s `~/.bashrc` on the login pod for the window. Both are removed afterwards.
124
+ - **The test:** a run writes a few hundred MB of fake checkpoints to its run directory and its campaign folder, and the campaign concludes.
125
+ - **Checked:** `s5cmd ls` shows both prefixes holding every file at its size, and both directories are gone from `/mnt/data`.
126
+ - **Cleanup:** the test deletes its own prefixes.
127
+ - **Live config:** the test needs a live config naming `coreweave-b200`. `test_live.py` today takes the one cluster its config names. How it reaches a second cluster is the plan's call.
128
+ - **As run, 2026-10-26:** on `coreweave-rtx6000` and `brineylab-us-east` instead, with no reservation: b200's `hpc-low` was full and rtx6000 had idle nodes. The run asks for no GPU, so either cluster proves the same path. It ran from partlow with partlow's database, `flyloft-serve` stopped for the run, and passed in 2 min 20 s. The test project was `flyloft-trial`: Sarah's GitHub token cannot read `flyloft-dev`.
129
+
130
+ **Tier three** is the first real campaign on b200.
131
+
132
+ ## 5. Code layout
133
+
134
+ ```
135
+ src/flyloft/migrations/0030_archive.sql run.work_dir_archive, campaign.work_dir_archive
136
+ src/flyloft/config/models.py, config/load.py ClusterConfig.work_dir_archive; refused without a partition
137
+ src/flyloft/runs/staging.py run and campaign prefixes; render_archive_script()
138
+ src/flyloft/runs/client.py, runs/ssh.py archive(): write the script, sbatch, the key on stdin
139
+ src/flyloft/runs/collect.py the result's artifact is the prefix on an archiving cluster
140
+ src/flyloft/service/tick.py archive ended runs, then ended campaigns
141
+ src/flyloft/turns/roles/_cj_implementer_rules.md.j2 the campaign folder moves to its prefix at the end
142
+ src/flyloft/host/doctor.py the key is present when a cluster archives
143
+ src/flyloft/web/schemas.py, record/views.py the run's and the campaign's prefixes
144
+ web/src/... the prefixes on the run and campaign pages
145
+ tests/fakes/cluster.py archive(), and a stub s5cmd
146
+ examples/live-config/clusters/ a commented work_dir_archive line
147
+ ```
148
+
149
+ ## 6. Out of scope
150
+
151
+ Each goes on the deferred list in `docs/ARCHITECTURE.md`.
152
+
153
+ - **Watching archive jobs.** flyloft submits each one and moves on. If one fails, the directory stays on `/mnt/data` with its log, and nothing tells anyone. Added when a failed archive goes unnoticed.
154
+ - **A key scoped to `flyloft/`.** The key is Sarah's, with full access to both buckets. A CoreWeave principal for flyloft, with a policy on `brineylab-*/flyloft/*` only, removes the exposure. Asked for when the lab wants it closed.
155
+ - **A cap on live disk use.** While runs and campaigns are live, `/mnt/data` holds all of them. Added when a campaign fills it.
156
+ - **Archiving validations and the shared project work directory.**
157
+ - **Retention in the bucket.** Lab members can already delete under `flyloft/`, and nothing expires.
158
+ - **Object storage on-prem.**