flyloft 0.2.2__tar.gz → 0.4.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 (573) hide show
  1. flyloft-0.4.0/.containerignore +10 -0
  2. flyloft-0.4.0/.github/dependabot.yml +45 -0
  3. flyloft-0.4.0/.github/workflows/ci.yml +130 -0
  4. {flyloft-0.2.2 → flyloft-0.4.0}/.github/workflows/python-publish.yaml +3 -3
  5. {flyloft-0.2.2 → flyloft-0.4.0}/AGENTS.md +2 -1
  6. {flyloft-0.2.2 → flyloft-0.4.0}/PKG-INFO +1 -1
  7. {flyloft-0.2.2 → flyloft-0.4.0}/docs/ARCHITECTURE.md +64 -18
  8. {flyloft-0.2.2 → flyloft-0.4.0}/docs/NEW_PROJECT.md +28 -13
  9. flyloft-0.4.0/docs/design/agent-isolation.md +160 -0
  10. flyloft-0.4.0/docs/design/cluster-storage.md +193 -0
  11. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/compute.md +4 -0
  12. flyloft-0.4.0/docs/design/direct-jobs.md +149 -0
  13. flyloft-0.4.0/docs/design/harness-defaults.md +174 -0
  14. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/investigate.md +2 -2
  15. flyloft-0.4.0/docs/design/preemption.md +218 -0
  16. flyloft-0.4.0/docs/design/run-storage.md +158 -0
  17. flyloft-0.4.0/docs/design/toolshed.md +211 -0
  18. flyloft-0.4.0/docs/design/user-credentials.md +129 -0
  19. flyloft-0.4.0/docs/plans/agent-isolation.md +276 -0
  20. flyloft-0.4.0/docs/plans/cluster-storage.md +411 -0
  21. flyloft-0.4.0/docs/plans/direct-jobs.md +224 -0
  22. flyloft-0.4.0/docs/plans/harness-defaults-a.md +189 -0
  23. flyloft-0.4.0/docs/plans/harness-defaults-b.md +125 -0
  24. flyloft-0.4.0/docs/plans/preemption.md +325 -0
  25. flyloft-0.4.0/docs/plans/run-storage.md +498 -0
  26. flyloft-0.4.0/docs/plans/toolshed-b.md +982 -0
  27. flyloft-0.4.0/docs/plans/user-credentials.md +314 -0
  28. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/clusters/slinky.toml +4 -0
  29. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/claude.toml +1 -1
  30. {flyloft-0.2.2 → flyloft-0.4.0}/pyproject.toml +1 -1
  31. flyloft-0.4.0/scripts/preemption_drill.sh +14 -0
  32. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/_version.py +2 -2
  33. flyloft-0.4.0/src/flyloft/agent_image/Containerfile +26 -0
  34. flyloft-0.4.0/src/flyloft/agent_image/bridge.py +35 -0
  35. flyloft-0.4.0/src/flyloft/agent_image/prepare.py +37 -0
  36. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/cli/main.py +10 -3
  37. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/client/handoff.py +39 -1
  38. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/client/main.py +178 -2
  39. flyloft-0.4.0/src/flyloft/client/render.py +141 -0
  40. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/config/load.py +53 -5
  41. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/config/models.py +48 -5
  42. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/config/projects.py +2 -0
  43. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/github.py +56 -3
  44. flyloft-0.4.0/src/flyloft/host/catalogue/CLAUDE_CODE_OAUTH_TOKEN.toml +2 -0
  45. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/host/doctor.py +91 -23
  46. flyloft-0.4.0/src/flyloft/host/harnesses/claude.toml +4 -0
  47. flyloft-0.4.0/src/flyloft/host/harnesses/opencode.toml +4 -0
  48. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/host/init.py +8 -1
  49. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/host/up.py +15 -0
  50. flyloft-0.4.0/src/flyloft/migrations/0029_preemption.sql +3 -0
  51. flyloft-0.4.0/src/flyloft/migrations/0030_archive.sql +4 -0
  52. flyloft-0.4.0/src/flyloft/migrations/0031_toolshed.sql +27 -0
  53. flyloft-0.4.0/src/flyloft/migrations/0032_credentials.sql +9 -0
  54. flyloft-0.4.0/src/flyloft/migrations/0033_agent_branch.sql +4 -0
  55. flyloft-0.4.0/src/flyloft/migrations/0034_turn_error.sql +4 -0
  56. flyloft-0.4.0/src/flyloft/migrations/0035_cluster_storage.sql +15 -0
  57. flyloft-0.4.0/src/flyloft/migrations/0036_job.sql +22 -0
  58. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/body.py +10 -1
  59. flyloft-0.4.0/src/flyloft/publication/export.py +47 -0
  60. flyloft-0.4.0/src/flyloft/publication/gh.py +120 -0
  61. flyloft-0.4.0/src/flyloft/publication/publisher.py +29 -0
  62. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/record.md.j2 +14 -0
  63. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/actions.py +252 -28
  64. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/consistency.py +31 -0
  65. flyloft-0.4.0/src/flyloft/record/credentials.py +108 -0
  66. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/islands.py +29 -11
  67. flyloft-0.4.0/src/flyloft/record/jobs.py +102 -0
  68. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/launch.py +52 -12
  69. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/models.py +93 -5
  70. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/researcher.py +33 -0
  71. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/store.py +160 -15
  72. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/submit.py +23 -10
  73. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/users.py +3 -3
  74. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/validation.py +6 -0
  75. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/views.py +24 -2
  76. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/runs/capacity.py +33 -0
  77. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/runs/client.py +4 -2
  78. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/runs/collect.py +7 -3
  79. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/runs/observe.py +1 -1
  80. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/runs/ssh.py +43 -16
  81. flyloft-0.4.0/src/flyloft/runs/staging.py +209 -0
  82. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/service/tick.py +465 -32
  83. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/skill/SKILL.md +28 -11
  84. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/skill/contract.md +18 -22
  85. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/skill/handoff.md +3 -26
  86. flyloft-0.4.0/src/flyloft/tools/listen.py +41 -0
  87. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/tools/server.py +97 -19
  88. flyloft-0.4.0/src/flyloft/toolshed.py +114 -0
  89. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/assembly.py +122 -36
  90. flyloft-0.4.0/src/flyloft/turns/catalog.py +84 -0
  91. flyloft-0.4.0/src/flyloft/turns/container.py +173 -0
  92. flyloft-0.4.0/src/flyloft/turns/launch.py +333 -0
  93. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/__init__.py +2 -2
  94. flyloft-0.4.0/src/flyloft/turns/roles/_cj_critic_runs.md.j2 +22 -0
  95. flyloft-0.4.0/src/flyloft/turns/roles/_cj_implementer_rules.md.j2 +15 -0
  96. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/_cj_record.md.j2 +1 -1
  97. flyloft-0.4.0/src/flyloft/turns/roles/_toolshed.md.j2 +24 -0
  98. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/critic.investigate.md.j2 +1 -1
  99. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/critic.md.j2 +1 -1
  100. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/critic.task.md.j2 +1 -1
  101. flyloft-0.4.0/src/flyloft/turns/roles/critic.tool.md.j2 +52 -0
  102. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/implementer.investigate.md.j2 +2 -0
  103. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/implementer.md.j2 +2 -1
  104. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/implementer.task.md.j2 +3 -1
  105. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/integrator.md.j2 +4 -1
  106. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/reporter.task.md.j2 +11 -2
  107. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/substrates/__init__.py +15 -2
  108. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/substrates/claude_code.py +33 -7
  109. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/substrates/opencode.py +15 -9
  110. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/substrates/scripted.py +37 -18
  111. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/wake.py +15 -4
  112. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/app.py +99 -7
  113. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/compute.py +32 -17
  114. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/events.py +5 -2
  115. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/read.py +29 -16
  116. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/schemas.py +106 -4
  117. flyloft-0.4.0/src/flyloft/web/static/assets/index-GVgD4m4l.css +2 -0
  118. flyloft-0.4.0/src/flyloft/web/static/assets/index-iXMib-ny.js +70 -0
  119. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/static/index.html +2 -2
  120. flyloft-0.4.0/src/flyloft/workspace/git.py +321 -0
  121. {flyloft-0.2.2 → flyloft-0.4.0}/tests/conftest.py +119 -4
  122. flyloft-0.4.0/tests/durations.json +834 -0
  123. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_agent_interface.py +2 -1
  124. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_board2.py +3 -1
  125. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_board3.py +32 -10
  126. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_campaign_compute.py +3 -3
  127. flyloft-0.4.0/tests/e2e/test_cluster_storage.py +95 -0
  128. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_compute.py +62 -10
  129. flyloft-0.4.0/tests/e2e/test_credentials.py +255 -0
  130. flyloft-0.4.0/tests/e2e/test_harness_defaults.py +250 -0
  131. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_investigate.py +18 -8
  132. flyloft-0.4.0/tests/e2e/test_isolation.py +294 -0
  133. flyloft-0.4.0/tests/e2e/test_jobs.py +162 -0
  134. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_live.py +373 -6
  135. flyloft-0.4.0/tests/e2e/test_preemption.py +122 -0
  136. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_projects.py +11 -3
  137. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_replication.py +13 -8
  138. flyloft-0.4.0/tests/e2e/test_run_storage.py +188 -0
  139. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage1.py +41 -4
  140. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage5a.py +2 -3
  141. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage6a.py +2 -2
  142. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_tasks.py +27 -0
  143. flyloft-0.4.0/tests/e2e/test_toolshed.py +139 -0
  144. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_users.py +10 -3
  145. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_validation.py +13 -1
  146. flyloft-0.4.0/tests/fakes/bin/s5cmd +61 -0
  147. flyloft-0.4.0/tests/fakes/bin/srun +5 -0
  148. flyloft-0.4.0/tests/fakes/cluster.py +198 -0
  149. {flyloft-0.2.2 → flyloft-0.4.0}/tests/fakes/github.py +29 -0
  150. flyloft-0.4.0/tests/fakes/publisher.py +109 -0
  151. flyloft-0.4.0/tests/fixtures/agent-test-image/Containerfile +17 -0
  152. flyloft-0.4.0/tests/fixtures/toolshed/README.md +1 -0
  153. flyloft-0.4.0/tests/fixtures/toolshed/scripts/check.py +322 -0
  154. flyloft-0.4.0/tests/fixtures/toolshed/scripts/record +25 -0
  155. {flyloft-0.2.2 → flyloft-0.4.0}/tests/host/test_host.py +8 -3
  156. flyloft-0.4.0/tests/scripts/archive_investigator.py +18 -0
  157. flyloft-0.4.0/tests/scripts/attacker.py +147 -0
  158. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/common.py +7 -2
  159. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/investigator.py +6 -4
  160. flyloft-0.4.0/tests/scripts/keyed.py +15 -0
  161. flyloft-0.4.0/tests/scripts/live_archiver.py +23 -0
  162. flyloft-0.4.0/tests/scripts/live_contained.py +50 -0
  163. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/live_investigator.py +5 -4
  164. flyloft-0.4.0/tests/scripts/live_tool_critic.py +34 -0
  165. flyloft-0.4.0/tests/scripts/live_tool_tasker.py +120 -0
  166. flyloft-0.4.0/tests/scripts/max_turns.py +35 -0
  167. flyloft-0.4.0/tests/scripts/redirector.py +39 -0
  168. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/reporter.py +16 -1
  169. flyloft-0.4.0/tests/scripts/symlinker.py +20 -0
  170. flyloft-0.4.0/tests/scripts/tool_critic.py +58 -0
  171. flyloft-0.4.0/tests/scripts/tool_outsider.py +61 -0
  172. flyloft-0.4.0/tests/scripts/tool_tasker.py +116 -0
  173. flyloft-0.4.0/tests/unit/__init__.py +0 -0
  174. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/island_helpers.py +10 -3
  175. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_actions.py +19 -0
  176. flyloft-0.4.0/tests/unit/test_archive_script.py +180 -0
  177. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_assembly.py +302 -9
  178. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_capacity.py +21 -0
  179. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_claude_code.py +31 -13
  180. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_cli.py +4 -2
  181. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_client.py +67 -3
  182. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_config.py +110 -8
  183. flyloft-0.4.0/tests/unit/test_container.py +155 -0
  184. flyloft-0.4.0/tests/unit/test_doctor.py +18 -0
  185. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_gh.py +28 -0
  186. flyloft-0.4.0/tests/unit/test_github.py +49 -0
  187. flyloft-0.4.0/tests/unit/test_host_doctor.py +233 -0
  188. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_host_init.py +8 -1
  189. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_host_up.py +24 -1
  190. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_investigation.py +94 -5
  191. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_islands.py +19 -4
  192. flyloft-0.4.0/tests/unit/test_jobs.py +78 -0
  193. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_launch.py +126 -38
  194. flyloft-0.4.0/tests/unit/test_listen.py +109 -0
  195. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_measurement.py +1 -0
  196. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_migrate.py +10 -0
  197. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_opencode.py +33 -17
  198. flyloft-0.4.0/tests/unit/test_prepare.py +62 -0
  199. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_publication.py +41 -1
  200. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_runs.py +138 -43
  201. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_scripted.py +28 -14
  202. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_service.py +203 -18
  203. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_ssh.py +56 -5
  204. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_tools.py +40 -16
  205. flyloft-0.4.0/tests/unit/test_toolshed.py +772 -0
  206. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_web_app.py +45 -4
  207. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_web_events.py +22 -0
  208. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_web_read.py +42 -0
  209. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_workspace.py +1 -3
  210. flyloft-0.4.0/tests/unit/test_workspace_git.py +121 -0
  211. flyloft-0.4.0/uv.lock +1486 -0
  212. {flyloft-0.2.2 → flyloft-0.4.0}/web/package-lock.json +1149 -2094
  213. {flyloft-0.2.2 → flyloft-0.4.0}/web/package.json +8 -8
  214. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/App.tsx +2 -0
  215. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/api/queries.ts +6 -1
  216. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/api/types.ts +621 -7
  217. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Actions.tsx +34 -0
  218. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Sidebar.tsx +3 -1
  219. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/crew.test.ts +15 -10
  220. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/crew.ts +18 -9
  221. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/launch.test.ts +2 -2
  222. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/roles.test.ts +1 -1
  223. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Compute.tsx +161 -61
  224. flyloft-0.4.0/web/src/pages/Credentials.tsx +86 -0
  225. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Launch.tsx +9 -16
  226. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Run.tsx +13 -1
  227. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Turn.tsx +1 -0
  228. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/campaign/Activity.tsx +1 -1
  229. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/campaign/Frame.tsx +25 -6
  230. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/campaign/Overview.tsx +31 -0
  231. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/campaign/Runs.tsx +1 -0
  232. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/launch/steps.tsx +36 -19
  233. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/project/ValidationPanel.tsx +6 -2
  234. flyloft-0.2.2/.github/workflows/ci.yml +0 -89
  235. flyloft-0.2.2/src/flyloft/client/render.py +0 -84
  236. flyloft-0.2.2/src/flyloft/host/harnesses/claude.toml +0 -3
  237. flyloft-0.2.2/src/flyloft/host/harnesses/opencode.toml +0 -3
  238. flyloft-0.2.2/src/flyloft/publication/export.py +0 -49
  239. flyloft-0.2.2/src/flyloft/publication/gh.py +0 -64
  240. flyloft-0.2.2/src/flyloft/publication/publisher.py +0 -15
  241. flyloft-0.2.2/src/flyloft/runs/staging.py +0 -94
  242. flyloft-0.2.2/src/flyloft/turns/launch.py +0 -225
  243. flyloft-0.2.2/src/flyloft/turns/roles/_cj_critic_runs.md.j2 +0 -22
  244. flyloft-0.2.2/src/flyloft/turns/roles/_cj_implementer_rules.md.j2 +0 -11
  245. flyloft-0.2.2/src/flyloft/web/static/assets/index-CEib_UXZ.js +0 -244
  246. flyloft-0.2.2/src/flyloft/web/static/assets/index-y1dUR_aG.css +0 -1
  247. flyloft-0.2.2/src/flyloft/workspace/git.py +0 -175
  248. flyloft-0.2.2/tests/fakes/cluster.py +0 -111
  249. flyloft-0.2.2/tests/fakes/publisher.py +0 -41
  250. flyloft-0.2.2/tests/unit/test_github.py +0 -26
  251. flyloft-0.2.2/tests/unit/test_host_doctor.py +0 -120
  252. flyloft-0.2.2/uv.lock +0 -1538
  253. {flyloft-0.2.2 → flyloft-0.4.0}/.gitignore +0 -0
  254. {flyloft-0.2.2 → flyloft-0.4.0}/.python-version +0 -0
  255. {flyloft-0.2.2 → flyloft-0.4.0}/CLAUDE.md +0 -0
  256. {flyloft-0.2.2 → flyloft-0.4.0}/README.md +0 -0
  257. {flyloft-0.2.2 → flyloft-0.4.0}/docs/NEW_TEMPLATE.md +0 -0
  258. {flyloft-0.2.2 → flyloft-0.4.0}/docs/VISION.md +0 -0
  259. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/agent-interface.md +0 -0
  260. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-1.md +0 -0
  261. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-2.md +0 -0
  262. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-3.md +0 -0
  263. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-4.md +0 -0
  264. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-5/campaign.html +0 -0
  265. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-5/campaigns.html +0 -0
  266. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-5/launch.html +0 -0
  267. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-5/pages.html +0 -0
  268. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/board-5.md +0 -0
  269. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/budget-billing.md +0 -0
  270. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/campaign-compute.md +0 -0
  271. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/compute/compute.html +0 -0
  272. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/cpu-partition.md +0 -0
  273. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/criterion-removal.md +0 -0
  274. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/flyloft-up.md +0 -0
  275. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/multi-user.md +0 -0
  276. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/new-project.md +0 -0
  277. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/projects.md +0 -0
  278. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/replication.md +0 -0
  279. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-1.md +0 -0
  280. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-2.md +0 -0
  281. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-3a.md +0 -0
  282. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-3b.md +0 -0
  283. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-4a.md +0 -0
  284. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-4b.md +0 -0
  285. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-5a.md +0 -0
  286. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-5b.md +0 -0
  287. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-5c.md +0 -0
  288. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-5d.md +0 -0
  289. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-6a.md +0 -0
  290. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-6b.md +0 -0
  291. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/stage-7.md +0 -0
  292. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/tasks.md +0 -0
  293. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/tier-one-lockstep.md +0 -0
  294. {flyloft-0.2.2 → flyloft-0.4.0}/docs/design/validation.md +0 -0
  295. {flyloft-0.2.2 → flyloft-0.4.0}/docs/environments/deeplearning_v2026-09-15.txt +0 -0
  296. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/agent-interface.md +0 -0
  297. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/board-1.md +0 -0
  298. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/board-2.md +0 -0
  299. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/board-3.md +0 -0
  300. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/board-4.md +0 -0
  301. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/board-5.md +0 -0
  302. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/budget-billing.md +0 -0
  303. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/campaign-compute.md +0 -0
  304. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/compute.md +0 -0
  305. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/cpu-partition.md +0 -0
  306. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/criterion-removal.md +0 -0
  307. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/flyloft-up.md +0 -0
  308. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/investigate.md +0 -0
  309. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/multi-user-a.md +0 -0
  310. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/multi-user-b.md +0 -0
  311. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/new-project.md +0 -0
  312. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/projects.md +0 -0
  313. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/replication-a.md +0 -0
  314. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/replication-b.md +0 -0
  315. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/replication-c.md +0 -0
  316. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-1.md +0 -0
  317. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-2.md +0 -0
  318. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-3a.md +0 -0
  319. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-3b.md +0 -0
  320. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-4a.md +0 -0
  321. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-4b.md +0 -0
  322. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-5a.md +0 -0
  323. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-5b.md +0 -0
  324. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-5c.md +0 -0
  325. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-5d.md +0 -0
  326. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-6a.md +0 -0
  327. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-6b.md +0 -0
  328. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/stage-7.md +0 -0
  329. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/tasks.md +0 -0
  330. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/tier-one-lockstep.md +0 -0
  331. {flyloft-0.2.2 → flyloft-0.4.0}/docs/plans/validation.md +0 -0
  332. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/flyloft.toml +0 -0
  333. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/builder.toml +0 -0
  334. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/coordinator.toml +0 -0
  335. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/first_campaign.toml +0 -0
  336. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/generator.toml +0 -0
  337. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/happy_path.toml +0 -0
  338. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/integrator.toml +0 -0
  339. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/lenient_critic.toml +0 -0
  340. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/librarian.toml +0 -0
  341. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/opencode.toml +0 -0
  342. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/orchestrator.toml +0 -0
  343. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/proximity.toml +0 -0
  344. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/reporter.toml +0 -0
  345. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/harnesses/strict_critic.toml +0 -0
  346. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/projects/toy-live-4b.toml +0 -0
  347. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/projects/toy-live-5a.toml +0 -0
  348. {flyloft-0.2.2 → flyloft-0.4.0}/examples/live-config/projects/toy-live.toml +0 -0
  349. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/__init__.py +0 -0
  350. {flyloft-0.2.2/src/flyloft/budget → flyloft-0.4.0/src/flyloft/agent_image}/__init__.py +0 -0
  351. {flyloft-0.2.2/src/flyloft/cli → flyloft-0.4.0/src/flyloft/budget}/__init__.py +0 -0
  352. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/budget/compare.py +0 -0
  353. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/budget/ledger.py +0 -0
  354. {flyloft-0.2.2/src/flyloft/config → flyloft-0.4.0/src/flyloft/cli}/__init__.py +0 -0
  355. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/client/__init__.py +0 -0
  356. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/client/api.py +0 -0
  357. {flyloft-0.2.2/src/flyloft/publication → flyloft-0.4.0/src/flyloft/config}/__init__.py +0 -0
  358. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/config/toml.py +0 -0
  359. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/host/__init__.py +0 -0
  360. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/host/units.py +0 -0
  361. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/knowledge/__init__.py +0 -0
  362. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/knowledge/memory.py +0 -0
  363. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0001_initial.sql +0 -0
  364. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0002_finding_state.sql +0 -0
  365. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0003_several_implementers.sql +0 -0
  366. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0004_entry_order_within_a_transaction.sql +0 -0
  367. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0005_population.sql +0 -0
  368. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0006_memory.sql +0 -0
  369. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0007_islands.sql +0 -0
  370. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0008_blocks.sql +0 -0
  371. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0009_export.sql +0 -0
  372. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0010_failures.sql +0 -0
  373. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0011_unblock.sql +0 -0
  374. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0012_indefinite.sql +0 -0
  375. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0013_pull_requests.sql +0 -0
  376. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0014_run_elapsed.sql +0 -0
  377. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0015_campaign_types.sql +0 -0
  378. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0016_island_aim.sql +0 -0
  379. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0017_campaign_project_config.sql +0 -0
  380. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0018_fresh_record.sql +0 -0
  381. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0019_run_tree.sql +0 -0
  382. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0020_users.sql +0 -0
  383. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0021_token_refused.sql +0 -0
  384. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0022_drop_criterion.sql +0 -0
  385. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0023_campaign_compute.sql +0 -0
  386. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0024_optional_cpus_mem.sql +0 -0
  387. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0025_validation.sql +0 -0
  388. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0026_measurement.sql +0 -0
  389. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0027_investigation.sql +0 -0
  390. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/migrations/0028_cpu_runs.sql +0 -0
  391. {flyloft-0.2.2/src/flyloft/record → flyloft-0.4.0/src/flyloft/publication}/__init__.py +0 -0
  392. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/_findings.md.j2 +0 -0
  393. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/_findings_critic_judged.md.j2 +0 -0
  394. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/_head.md.j2 +0 -0
  395. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/_head_critic_judged.md.j2 +0 -0
  396. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/_macros.md.j2 +0 -0
  397. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/pr_body.md.j2 +0 -0
  398. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/promotion_pr.md.j2 +0 -0
  399. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/publication/record_pr.md.j2 +0 -0
  400. {flyloft-0.2.2/src/flyloft/runs → flyloft-0.4.0/src/flyloft/record}/__init__.py +0 -0
  401. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/db.py +0 -0
  402. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/migrate.py +0 -0
  403. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/record/templates.py +0 -0
  404. {flyloft-0.2.2/src/flyloft/service → flyloft-0.4.0/src/flyloft/runs}/__init__.py +0 -0
  405. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/runs/askpass.sh +0 -0
  406. {flyloft-0.2.2/src/flyloft/tools → flyloft-0.4.0/src/flyloft/service}/__init__.py +0 -0
  407. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/service/factory.py +0 -0
  408. {flyloft-0.2.2/src/flyloft/turns → flyloft-0.4.0/src/flyloft/tools}/__init__.py +0 -0
  409. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/tools/__main__.py +0 -0
  410. {flyloft-0.2.2/src/flyloft/web → flyloft-0.4.0/src/flyloft/turns}/__init__.py +0 -0
  411. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/_plan.md.j2 +0 -0
  412. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/bounded.md.j2 +0 -0
  413. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/coordinator.md.j2 +0 -0
  414. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/direction.md.j2 +0 -0
  415. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/generator.md.j2 +0 -0
  416. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/librarian.md.j2 +0 -0
  417. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/memory.md.j2 +0 -0
  418. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/orchestrator.md.j2 +0 -0
  419. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/proximity.md.j2 +0 -0
  420. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/reporter.investigate.md.j2 +0 -0
  421. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/turns/roles/reporter.md.j2 +0 -0
  422. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/version.py +0 -0
  423. {flyloft-0.2.2/src/flyloft/workspace → flyloft-0.4.0/src/flyloft/web}/__init__.py +0 -0
  424. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/artifacts.py +0 -0
  425. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/auth.py +0 -0
  426. {flyloft-0.2.2 → flyloft-0.4.0}/src/flyloft/web/openapi.py +0 -0
  427. {flyloft-0.2.2/tests → flyloft-0.4.0/src/flyloft/workspace}/__init__.py +0 -0
  428. {flyloft-0.2.2/tests/e2e → flyloft-0.4.0/tests}/__init__.py +0 -0
  429. {flyloft-0.2.2/tests/fakes → flyloft-0.4.0/tests/e2e}/__init__.py +0 -0
  430. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/fixtures/opencode-turn.jsonl +0 -0
  431. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_acting_as_owner.py +0 -0
  432. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_board1.py +0 -0
  433. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_board4.py +0 -0
  434. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_board5.py +0 -0
  435. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_replication_board.py +0 -0
  436. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage2.py +0 -0
  437. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage3.py +0 -0
  438. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage3b.py +0 -0
  439. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage4a.py +0 -0
  440. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage4b.py +0 -0
  441. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage5b.py +0 -0
  442. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage5c.py +0 -0
  443. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage5d.py +0 -0
  444. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage6b.py +0 -0
  445. {flyloft-0.2.2 → flyloft-0.4.0}/tests/e2e/test_stage7.py +0 -0
  446. {flyloft-0.2.2/tests/host → flyloft-0.4.0/tests/fakes}/__init__.py +0 -0
  447. {flyloft-0.2.2 → flyloft-0.4.0}/tests/fixtures/slinky/capacity.txt +0 -0
  448. {flyloft-0.2.2 → flyloft-0.4.0}/tests/fixtures/slinky/cluster.toml +0 -0
  449. {flyloft-0.2.2 → flyloft-0.4.0}/tests/fixtures/toyproject/BRIEF.md +0 -0
  450. {flyloft-0.2.2 → flyloft-0.4.0}/tests/fixtures/toyproject/config.json +0 -0
  451. {flyloft-0.2.2 → flyloft-0.4.0}/tests/fixtures/toyproject/train.py +0 -0
  452. {flyloft-0.2.2/tests/scripts → flyloft-0.4.0/tests/host}/__init__.py +0 -0
  453. {flyloft-0.2.2/tests/unit → flyloft-0.4.0/tests/scripts}/__init__.py +0 -0
  454. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/builder.py +0 -0
  455. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/coordinator.py +0 -0
  456. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/cpu_investigator.py +0 -0
  457. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/cpu_tasker.py +0 -0
  458. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/crash.py +0 -0
  459. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/crew.py +0 -0
  460. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/failing_job.py +0 -0
  461. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/first_campaign.py +0 -0
  462. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/flaky_builder.py +0 -0
  463. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/generator.py +0 -0
  464. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/happy_path.py +0 -0
  465. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/integrator.py +0 -0
  466. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/integrator_spoil.py +0 -0
  467. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/investigator_quiet.py +0 -0
  468. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/lenient_critic.py +0 -0
  469. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/librarian.py +0 -0
  470. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/missing_results.py +0 -0
  471. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/near_far.py +0 -0
  472. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/orchestrator.py +0 -0
  473. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/plateau.py +0 -0
  474. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/post_then_crash.py +0 -0
  475. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/proximity.py +0 -0
  476. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/replication.py +0 -0
  477. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/retracting.py +0 -0
  478. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/silent_exit.py +0 -0
  479. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/strict_critic.py +0 -0
  480. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/task_critic.py +0 -0
  481. {flyloft-0.2.2 → flyloft-0.4.0}/tests/scripts/tasker.py +0 -0
  482. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/conftest.py +0 -0
  483. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/memory_helpers.py +0 -0
  484. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_budget.py +0 -0
  485. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_client_main.py +0 -0
  486. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_compute_view.py +0 -0
  487. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_consistency.py +0 -0
  488. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_format_metrics.py +0 -0
  489. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_host_units.py +0 -0
  490. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_knowledge.py +0 -0
  491. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_population.py +0 -0
  492. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_projects.py +0 -0
  493. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_skill.py +0 -0
  494. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_store.py +0 -0
  495. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_submit.py +0 -0
  496. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_tasks.py +0 -0
  497. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_templates.py +0 -0
  498. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_toyproject.py +0 -0
  499. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_transcript.py +0 -0
  500. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_users.py +0 -0
  501. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_wake.py +0 -0
  502. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_web_artifacts.py +0 -0
  503. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/test_web_compare.py +0 -0
  504. {flyloft-0.2.2 → flyloft-0.4.0}/tests/unit/web_helpers.py +0 -0
  505. {flyloft-0.2.2 → flyloft-0.4.0}/web/components.json +0 -0
  506. {flyloft-0.2.2 → flyloft-0.4.0}/web/index.html +0 -0
  507. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/api/client.test.ts +0 -0
  508. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/api/client.ts +0 -0
  509. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/api/live.ts +0 -0
  510. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/AnswerValue.tsx +0 -0
  511. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Breadcrumbs.tsx +0 -0
  512. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/CampaignCard.tsx +0 -0
  513. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/CampaignFilter.tsx +0 -0
  514. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ChangeMarkers.tsx +0 -0
  515. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/DotBanner.tsx +0 -0
  516. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/EntityLink.tsx +0 -0
  517. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ErrorNote.tsx +0 -0
  518. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/FinishedRow.tsx +0 -0
  519. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Freshness.tsx +0 -0
  520. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/InvestigationCard.tsx +0 -0
  521. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/LogList.tsx +0 -0
  522. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ProgressChart.tsx +0 -0
  523. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/RoleChips.tsx +0 -0
  524. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/RoleNode.tsx +0 -0
  525. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Selectable.tsx +0 -0
  526. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/StartFromAgent.tsx +0 -0
  527. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Step.tsx +0 -0
  528. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Story.tsx +0 -0
  529. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/SwarmMap.tsx +0 -0
  530. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Timeline.tsx +0 -0
  531. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Timestamp.tsx +0 -0
  532. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/Transcript.tsx +0 -0
  533. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ui/alert-dialog.tsx +0 -0
  534. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ui/button.tsx +0 -0
  535. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ui/popover.tsx +0 -0
  536. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ui/sheet.tsx +0 -0
  537. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ui/tabs.tsx +0 -0
  538. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/components/ui/tooltip.tsx +0 -0
  539. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/campaign.test.ts +0 -0
  540. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/campaign.ts +0 -0
  541. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/chart.test.ts +0 -0
  542. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/chart.ts +0 -0
  543. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/compute.test.ts +0 -0
  544. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/compute.ts +0 -0
  545. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/format.test.ts +0 -0
  546. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/format.ts +0 -0
  547. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/launch.ts +0 -0
  548. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/projects.ts +0 -0
  549. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/roles.ts +0 -0
  550. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/runs.test.ts +0 -0
  551. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/runs.ts +0 -0
  552. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/seen.test.ts +0 -0
  553. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/seen.ts +0 -0
  554. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/story.test.ts +0 -0
  555. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/story.ts +0 -0
  556. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/swarm.test.ts +0 -0
  557. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/swarm.ts +0 -0
  558. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/lib/utils.ts +0 -0
  559. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/main.tsx +0 -0
  560. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Admin.tsx +0 -0
  561. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Compare.tsx +0 -0
  562. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Home.tsx +0 -0
  563. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Login.tsx +0 -0
  564. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/Projects.tsx +0 -0
  565. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/admin/Host.tsx +0 -0
  566. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/admin/Users.tsx +0 -0
  567. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/campaign/Hypotheses.tsx +0 -0
  568. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/campaign/context.ts +0 -0
  569. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/project/NewProject.tsx +0 -0
  570. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/pages/project/ProjectPage.tsx +0 -0
  571. {flyloft-0.2.2 → flyloft-0.4.0}/web/src/styles.css +0 -0
  572. {flyloft-0.2.2 → flyloft-0.4.0}/web/tsconfig.json +0 -0
  573. {flyloft-0.2.2 → flyloft-0.4.0}/web/vite.config.ts +0 -0
@@ -0,0 +1,10 @@
1
+ # The test agent image builds from the repository root (tests/fixtures/agent-test-image). Anything a test run writes
2
+ # here would change the build context between workers and rebuild the image in each one.
3
+ .git
4
+ .venv
5
+ **/__pycache__
6
+ **/*.pyc
7
+ .pytest_cache
8
+ .ruff_cache
9
+ test-results
10
+ web/node_modules
@@ -0,0 +1,45 @@
1
+ # Dependabot checks on the first of each month and proposes each ecosystem's minor and patch updates as one pull
2
+ # request. A version must have been published for two weeks before it is proposed, so a release that is pulled in its
3
+ # first days never reaches a pull request. Major versions are ignored: each is taken by hand, deliberately (this also
4
+ # keeps a security fix that only ships in a new major from opening a pull request; its alert still shows).
5
+ # Security updates are a repository setting and are not delayed by the cooldown.
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: "uv"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "monthly"
12
+ cooldown:
13
+ default-days: 14
14
+ groups:
15
+ minor-and-patch:
16
+ update-types: ["minor", "patch"]
17
+ ignore:
18
+ - dependency-name: "*"
19
+ update-types: ["version-update:semver-major"]
20
+
21
+ - package-ecosystem: "npm"
22
+ directory: "/web"
23
+ schedule:
24
+ interval: "monthly"
25
+ cooldown:
26
+ default-days: 14
27
+ groups:
28
+ minor-and-patch:
29
+ update-types: ["minor", "patch"]
30
+ ignore:
31
+ - dependency-name: "*"
32
+ update-types: ["version-update:semver-major"]
33
+
34
+ - package-ecosystem: "github-actions"
35
+ directory: "/"
36
+ schedule:
37
+ interval: "monthly"
38
+ cooldown:
39
+ default-days: 14
40
+ groups:
41
+ minor-and-patch:
42
+ update-types: ["minor", "patch"]
43
+ ignore:
44
+ - dependency-name: "*"
45
+ update-types: ["version-update:semver-major"]
@@ -0,0 +1,130 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # One suite per pull request, on its latest commit. A push to main has no head_ref, so it falls back to its own run
9
+ # id: every merge builds, and no merge cancels the one before it.
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
12
+ cancel-in-progress: true
13
+
14
+ env:
15
+ FLYLOFT_DURATIONS: tests/durations.json # pytest starts the long tests first (tests/conftest.py)
16
+
17
+ jobs:
18
+ lint:
19
+ runs-on: ${{ vars.CI_RUNS_ON && fromJSON(vars.CI_RUNS_ON) || 'ubuntu-latest' }}
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ with:
23
+ fetch-depth: 0 # hatch-vcs reads the version from git tags
24
+ - uses: astral-sh/setup-uv@v10.3.3
25
+ - run: uv sync --locked --all-extras
26
+ - run: uv run ruff format --check .
27
+ - run: uv run ruff check .
28
+
29
+ types:
30
+ runs-on: ${{ vars.CI_RUNS_ON && fromJSON(vars.CI_RUNS_ON) || 'ubuntu-latest' }}
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ with:
34
+ fetch-depth: 0
35
+ - uses: astral-sh/setup-uv@v10.3.3
36
+ - run: uv sync --locked --all-extras
37
+ - run: uv run ty check src
38
+
39
+ web:
40
+ runs-on: ${{ vars.CI_RUNS_ON && fromJSON(vars.CI_RUNS_ON) || 'ubuntu-latest' }}
41
+ steps:
42
+ - uses: actions/checkout@v7
43
+ with:
44
+ fetch-depth: 0
45
+ - uses: astral-sh/setup-uv@v10.3.3
46
+ - uses: actions/setup-node@v7
47
+ with:
48
+ node-version: 22
49
+ cache: npm
50
+ cache-dependency-path: web/package-lock.json
51
+ - run: uv sync --locked --all-extras
52
+ - run: npm ci
53
+ working-directory: web
54
+ - run: npm run typecheck
55
+ working-directory: web
56
+ - run: npm test
57
+ working-directory: web
58
+ - run: npm run types && git diff --exit-code src/api/types.ts
59
+ working-directory: web
60
+
61
+ tests:
62
+ runs-on: ${{ vars.CI_RUNS_ON && fromJSON(vars.CI_RUNS_ON) || 'ubuntu-latest' }}
63
+ strategy:
64
+ fail-fast: false
65
+ matrix:
66
+ # GitHub's runners: each job runs every fourth test, as a scenario's turns are CPU-bound on 2 cores. Self-hosted:
67
+ # one job (shard 0), so one worker pool balances the whole suite and setup runs once.
68
+ shard: ${{ fromJSON(contains(vars.CI_RUNS_ON || '', 'self-hosted') && '[0]' || '[1, 2, 3, 4]') }}
69
+ name: ${{ matrix.shard == 0 && 'tests' || format('tests ({0}/4)', matrix.shard) }}
70
+ services:
71
+ postgres:
72
+ # None on self-hosted runners, where the suite starts its own per worker on a port the host hands out: jobs share
73
+ # the host, and rootless Docker picks a published port without seeing the host's ports in use.
74
+ image: ${{ !contains(vars.CI_RUNS_ON || '', 'self-hosted') && 'postgres:18' || '' }}
75
+ env:
76
+ POSTGRES_PASSWORD: flyloft
77
+ POSTGRES_DB: flyloft_test
78
+ ports:
79
+ - 127.0.0.1:5432:5432 # loopback only, as on a host: a contained turn must not reach it
80
+ options: >-
81
+ --health-cmd "pg_isready -U postgres"
82
+ --health-interval 5s
83
+ --health-timeout 5s
84
+ --health-retries 10
85
+ env:
86
+ FLYLOFT_SHARD: ${{ matrix.shard != 0 && format('{0}/4', matrix.shard) || '' }}
87
+ steps:
88
+ - uses: actions/checkout@v7
89
+ with:
90
+ fetch-depth: 0
91
+ - uses: astral-sh/setup-uv@v10.3.3
92
+ - uses: actions/setup-node@v7
93
+ with:
94
+ node-version: 22
95
+ cache: npm
96
+ cache-dependency-path: web/package-lock.json
97
+ - run: uv sync --locked --all-extras
98
+ - run: npm ci
99
+ working-directory: web
100
+ - run: npm run build # the browser scenario serves it
101
+ working-directory: web
102
+ - run: uv run playwright install ${{ runner.environment == 'github-hosted' && '--with-deps' || '' }} chromium # self-hosted has the deps and no sudo
103
+ - run: uv run pytest -q -n ${{ runner.environment == 'github-hosted' && 'auto' || '24' }} --dist worksteal --tracing retain-on-failure --output test-results # auto would be every core of a shared host; worksteal lets an idle worker take tests queued behind a long one
104
+ env:
105
+ FLYLOFT_TEST_DATABASE_URL: ${{ runner.environment == 'github-hosted' && 'postgresql://postgres:flyloft@127.0.0.1:5432/flyloft_test' || '' }}
106
+ - uses: actions/upload-artifact@v7 # a failed browser test's trace: npx playwright show-trace trace.zip
107
+ if: failure()
108
+ with:
109
+ name: playwright-traces-${{ matrix.shard }}
110
+ path: test-results/
111
+ if-no-files-found: ignore
112
+ retention-days: 14
113
+
114
+ client:
115
+ runs-on: ${{ vars.CI_RUNS_ON && fromJSON(vars.CI_RUNS_ON) || 'ubuntu-latest' }}
116
+ strategy:
117
+ matrix:
118
+ python: ["3.11", "3.12"]
119
+ steps:
120
+ - uses: actions/checkout@v7
121
+ with:
122
+ fetch-depth: 0
123
+ - uses: astral-sh/setup-uv@v10.3.3
124
+ with:
125
+ cache-python: true # the matrix installs a managed 3.11 and 3.12 on every run
126
+ - run: uv venv client-venv --python ${{ matrix.python }} && uv pip install --python client-venv/bin/python .
127
+ - run: client-venv/bin/flyloft --help
128
+ - run: client-venv/bin/flyloft install --harness claude
129
+ env:
130
+ HOME: ${{ runner.temp }}/home # a self-hosted runner's home is shared by every job
@@ -11,11 +11,11 @@ jobs:
11
11
  id-token: write # trusted publishing: PyPI accepts this workflow's OIDC token, no API token
12
12
  contents: read
13
13
  steps:
14
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@v7
15
15
  with:
16
16
  fetch-depth: 0 # hatch-vcs reads the version from the release tag
17
- - uses: astral-sh/setup-uv@v6
18
- - uses: actions/setup-node@v4
17
+ - uses: astral-sh/setup-uv@v10.3.3
18
+ - uses: actions/setup-node@v7
19
19
  with:
20
20
  node-version: 22
21
21
  cache: npm
@@ -47,7 +47,8 @@ This branch is a fresh design. Documents stand alone and do not describe the sys
47
47
  ## Setup and tests
48
48
 
49
49
  - `uv sync` installs everything. Run commands as `uv run ...`.
50
- - `uv run pytest -n 4` runs tiers one and unit tests on four workers; each worker gets its own database. Scripted turns are CPU-bound, so use no more workers than cores (CI uses `-n auto`: two on its runner). It starts a PostgreSQL container with rootless Podman unless `FLYLOFT_TEST_DATABASE_URL` is set.
50
+ - `uv run pytest -n 4` runs tiers one and unit tests on four workers; each worker gets its own database. Scripted turns are CPU-bound, so use no more workers than cores (CI on GitHub's runners uses `-n auto`, two per runner, in each of four jobs, and `FLYLOFT_SHARD=i/4` runs every fourth test; on the lab's self-hosted runners it is one job with `-n 24 --dist worksteal`. The repo variable `CI_RUNS_ON`, a JSON `runs-on` value, picks the runners; unset means `ubuntu-latest`). It starts a PostgreSQL container with rootless Podman unless `FLYLOFT_TEST_DATABASE_URL` is set.
51
+ - Added or reshaped a scenario test? Make your full run before committing `FLYLOFT_RECORD_DURATIONS=1 uv run pytest -n 4`, and commit the `tests/durations.json` it updates; CI starts the long tests first by it, and a test the file lacks counts as 1 s.
51
52
  - `FLYLOFT_LIVE=1 uv run pytest -m live` runs tier two against the configured cluster and test repository.
52
53
  - `uv run flyloft --help` lists the client's commands; `uv run flyloft host --help` the host's.
53
54
  - The board: `npm ci` and `npm run build` in `web/` (Node 22+), then `uv run flyloft host web` serves it on `web_bind`. `npm run types` regenerates `web/src/api/types.ts` from the API; CI fails if it drifts.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flyloft
3
- Version: 0.2.2
3
+ Version: 0.4.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 closed: tiers one, two, and three passed on 2026-10-27 (see [design/toolshed.md](design/toolshed.md)). A researcher's own agent runs one job on a cluster without a project or campaign, through `flyloft job` (see [design/direct-jobs.md](design/direct-jobs.md)).
4
4
 
5
5
  This document settles the shape of flyloft: the record that holds its state, the lifecycle and authority rules, the agent turn, the subsystem boundaries, and the stages by which the system grows. It fixes what is expensive to change later and leaves everything else to the design document of the stage that needs it. [VISION.md](VISION.md) says what flyloft is for; [AGENTS.md](../AGENTS.md) says how to work on it. This document is the bridge between them.
6
6
 
@@ -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 the campaign, or the baseline failed; its outstanding runs are canceled, since the hypotheses they serve are abandoned (2026-11-02; before, they finished and were 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
 
@@ -103,7 +103,7 @@ A turn is one disposable process. The system assembles context, launches the age
103
103
 
104
104
  **Assembly** is a function of role, agent, and campaign that produces one rendered assignment, written to disk and referenced from the turn row. At stage one it renders the campaign's question, criterion, baseline, and constraints; the project's research brief from its repository; the submissions remaining; and the whole record so far: hypotheses with states, runs with results, findings, assessments, and steering. A single campaign's record fits in a turn's context, so there is no read tool. Run directories are on the same host, so the assignment names them and the agent reads logs from disk.
105
105
 
106
- **Tools.** The implementer's MCP server exposes five tools, one per entry kind it may post: propose, request_run, assess, post_finding, ask. Each validates its body against the kind's schema and returns errors in the turn, so the agent corrects mistakes before it exits. Two rules live in request_run: the worktree must be clean and HEAD is recorded as the tested commit; and one run may be outstanding per campaign. The tool records intent and returns a run identity. Staging and submission happen after the turn, and the agent learns the outcome from the result entry in its next turn. SSH latency and submission failures never enter the agent's process.
106
+ **Tools.** The implementer's MCP server exposes five tools, one per entry kind it may post: propose, request_run, assess, post_finding, ask. Each validates its body against the kind's schema and returns errors in the turn, so the agent corrects mistakes before it exits. Two rules live in request_run: the agent's branch tip, fetched into the host's repository, is the tested commit (docs/design/agent-isolation.md §4a); and one run may be outstanding per campaign. The tool records intent and returns a run identity. Staging and submission happen after the turn, and the agent learns the outcome from the result entry in its next turn. SSH latency and submission failures never enter the agent's process.
107
107
 
108
108
  **Substrates.** A substrate executes a turn: run this assignment with these tools and report exit status. Two exist at stage one, which is what justifies the interface. The coding-harness substrate launches a harness headless in the worktree with the assignment as its prompt and the tool server configured. The scripted substrate, used in tests, connects to the same tool server and issues tool calls from a script. Every role runs in a coding harness or the scripted substrate; a role that does not edit code runs under a read-only tool policy. A native tool-calling loop is deferred until a role's harness overhead or a subscription's limits are observed to be a problem.
109
109
 
@@ -143,7 +143,7 @@ flowchart LR
143
143
  CLI --> PG[(PostgreSQL)]
144
144
  S[Service] <--> PG
145
145
  S -->|launch turn| H[Harness]
146
- H -->|stdio| T[Tool server]
146
+ H -->|socket| T[Tool server]
147
147
  T --> PG
148
148
  H --> M[Model endpoint]
149
149
  S -->|ssh| C[Slurm clusters]
@@ -151,7 +151,7 @@ flowchart LR
151
151
  ```
152
152
 
153
153
  - **The service.** One long-running process whose tick wakes agents, launches turns, stages and submits runs, observes jobs, and publishes. A run waits in the record until its campaign has a free run slot; a free slot goes to integration runs first, then the root island's runs, then island test runs, first come first within each, so consolidation never queues behind exploration.
154
- - **Per-turn processes.** The harness, and the tool server it launches as a stdio subprocess with the agent and turn identity in its environment. The server writes entries through record.
154
+ - **Per-turn processes.** The harness, in a container, and the tool server the service starts on the host behind a socket for that turn alone; the socket is the agent and turn identity. The server writes entries through record.
155
155
  - **The CLI.** Short-lived. It reads the record for status and posts steering entries.
156
156
 
157
157
  Two processes write to the record from the first turn, the service and the tool server, so PostgreSQL is a stage-one necessity rather than preparation.
@@ -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.
@@ -181,7 +181,7 @@ Two processes write to the record from the first turn, the service and the tool
181
181
 
182
182
  **Stage 2: a critic.** A finding is a candidate until a critic, running in a harness under a read-only tool policy, accepts it or blocks it with its reason. Only accepted findings reach the pull request. Adds the critique kind, finding state, per-role harness and model selection, and role wake filters. First communication between roles through the record.
183
183
 
184
- **Stage 3: several implementers, in two parts.** 3a: hypotheses in flight in parallel, each on its own branch off the campaign branch, admitted up to a per-campaign slot count. An integrator merges accepted implementations and keeps the campaign branch runnable. The same question runs with one and several implementers, and a compare report gives verified improvement against cost: the first control arm. Adds the integration kind, run ownership, wake predicates with relevance, the cost ledger, a done state for exhausted campaigns, and an allowlisted harness environment. 3b: a read-only web view of the board. The HTTP tool server with per-turn tokens is deferred; the reason is in [design/stage-3a.md](design/stage-3a.md) §7.
184
+ **Stage 3: several implementers, in two parts.** 3a: hypotheses in flight in parallel, each on its own branch off the campaign branch, admitted up to a per-campaign slot count. An integrator merges accepted implementations and keeps the campaign branch runnable. The same question runs with one and several implementers, and a compare report gives verified improvement against cost: the first control arm. Adds the integration kind, run ownership, wake predicates with relevance, the cost ledger, a done state for exhausted campaigns, and an allowlisted harness environment. 3b: a read-only web view of the board. The tool server's per-turn socket belongs to [agent isolation](design/agent-isolation.md).
185
185
 
186
186
  **Stage 4: a population, in two parts.** 4a: generators with distinct framings propose; proximity merges duplicates within the campaign; a coordinator admits under a posture set by the researchers; a finding needs the seeds the campaign demands. Adds the decision kind, the screened, admitted, and merged states, replication as a rule, and the control arm extended to the population. 4b: memory. A new campaign reads what the project's earlier campaigns settled, rendered from the record; proximity flags repeats of earlier work; revivals carry lineage as links; `memory = false` starts fresh. Adds the knowledge module. Read and search tools keep their trigger.
187
187
 
@@ -197,11 +197,11 @@ 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
 
204
- **Known stage-one limitation.** The tool server is a stdio subprocess launched by the harness, so database credentials are in the harness's environment and a misbehaving agent could bypass the tool surface. With one trusted agent on a lab host this is accepted. Stage 3a narrows it by allowlisting the harness environment, so cluster and GitHub credentials no longer reach agent processes; the database credential goes when the tool server moves to HTTP, deferred until a turn runs where that credential cannot be given to the process.
204
+ **Agent isolation.** No agent process holds the database credential, and identity follows the turn's socket rather than a claim in its environment: a turn's tool server runs on the host behind a socket that exists for that turn alone, and the harness runs in a container that holds only its own campaign's repository and the model endpoint. See [design/agent-isolation.md](design/agent-isolation.md).
205
205
 
206
206
  ## 7. Testing
207
207
 
@@ -232,13 +232,20 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
232
232
  | Several critics with different framings per candidate finding | When one critic's acceptance proves a rubber stamp, expected by stage 4 |
233
233
  | Integrator role; run admission with per-campaign slots | Stage 3a |
234
234
  | Budget ledger for tokens, GPU-hours, and cost | Stage 3a; at 4a the coordinator reads the ledger in its assignment, so there is no query tool |
235
- | HTTP tool server with per-turn tokens | When a turn runs where the database credential cannot be given to the process: a remote executor, an external agent, a provider's hosted runtime |
236
235
  | Read-only web API and dashboard | Done at stage 3b: a JSON read API, one change stream per campaign, and a single-page application served by `flyloft host web` |
237
236
  | Steer, approve, and stop from the board | Done at stage 5b: `POST` routes on the board's one router calling `record/researcher.py`, which the CLI calls too; approve deleted at stage 5d, when decompositions open at once |
238
237
  | Starting or configuring a campaign from the board | Board part 3: registering projects and launching campaigns; see [design/board-3.md](design/board-3.md) |
239
238
  | Authentication for the board: GitHub sign-in through a GitHub App (or an OAuth App) whose token also lets flyloft push and open pull requests as the signed-in user; owners on campaigns; an allowlist with admins; an admin page for users and host health | Next: see [design/multi-user.md](design/multi-user.md) |
240
239
  | Refreshing GitHub App user tokens (eight hours, with a six-month refresh token), so a host can leave the app's token expiration on; today it must opt out | When a lab will not register an app with expiration off |
241
- | Agents run as a separate OS user who cannot read the host's configuration, `secrets.env`, or the database, so a prompt-injected agent cannot read stored GitHub tokens | When flyloft runs an untrusted model or harness, or a lab will not accept agents sharing the operator's account |
240
+ | Egress filtering: a contained agent can still reach the internet and leak what it can read, its own campaign's code and the record entries in its assignment; the route is an allowlisting proxy through `HTTPS_PROXY` ([design/agent-isolation.md](design/agent-isolation.md) §11) | When a leak happens or a project holds data that must not leave |
241
+ | A flyloft-scoped object storage key, kept out of runs' reach: archive jobs get Sarah's full-access key in their environment, and a run on the same node as the same user can read it | With runs isolated per campaign; already deferred by run storage |
242
+ | A forced command on flyloft's cluster key, to limit what the key does if the service user itself is compromised | When the service user's compromise is a threat worth the command's upkeep; agents do not reach the key |
243
+ | A per-turn model key, or a proxy that adds the key outside the container | If the shared key leaks or LiteLLM spend needs attributing per campaign |
244
+ | Run credentials and per-user secrets (W&B and the like); anything a run can read, its agent can read, so the rule is lab-scoped keys in runs and personal keys used only on the host | When a project needs a key a run cannot hold |
245
+ | Turn containers under a separate, agent-only OS user, so a container escape does not land as the service's user; not the shared `brineylab` account | When flyloft runs an untrusted model, or a Podman or crun escape relevant to the host is published |
246
+ | Agents interfering within their own campaign: implementers of one campaign share its repository and can move one another's branches | When it is seen |
247
+ | A campaign repository borrows objects from the project clone through alternates; pruning the project clone after a force-push to its base could break it | When seen |
248
+ | A turn holds no database credential, but on podman 5 with pasta `host.containers.internal` reaches the host, so a service bound beyond loopback answers a turn | Tier two on partlow checks that the database and the board refuse a contained turn at `host.containers.internal` |
242
249
  | A better estimate of a run's cost than its time limit: a budgeted campaign reserves each next config at `time limit × GPUs`, so it ends far short of its cap when runs finish well inside the limit (toy-live c812fc9f: "budget spent" at 0.2 of 0.5 GPU-h, runs ~200 s against a 1200 s limit); reserving by the baseline's measured duration is the likely fix, and the ESM-2 project's 24-hour data-regime limit is the case that will show it | Done: see [design/budget-billing.md](design/budget-billing.md). A run is charged the GPU time it uses, new work starts while any budget is left, and planning reads the campaign's measured mean run |
243
250
  | Per-seed dots on the chart beside each config's mean | When a researcher misses them |
244
251
  | A margin or significance test before a config's mean beats its reference, the integrator's best of head, finding alone, and merged included | When promotions are seen on noise. Seen on ESM-2 f6b4591b at replication 1: the integrator replaced a head holding the accepted cosine schedule with a tree lacking it on 2.561 against 2.564, a 0.003-nat margin, and accepted steps of 0.004 to 0.009 rest on one seed each |
@@ -246,11 +253,16 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
246
253
  | The integrator's assignment names the verdict from its first integration run while the record judges its latest request; they diverge only if an integrator re-merges to a different tree and requests again | When a live integrator is refused that way |
247
254
  | The board's read layer computes each hypothesis's comparison per page more than once (O(hypotheses × runs) per request, re-run on every change event) | When a long campaign's page is seen slow with the board open |
248
255
  | The CLI as a client of the board's API, so anyone can use it from their own machine without the host's secrets; today the host's CLI acts as the operator who ran `flyloft host login` | Done: see [design/agent-interface.md](design/agent-interface.md) |
256
+ | A launch field naming the campaign a follow-up builds on, with its location passed to jobs | When a follow-up needs it without an agent to find it, for example an optimization whose code must read an earlier campaign's data by path |
257
+ | Runs isolated per campaign: every campaign's directory, its runs inside it, sits under `<owner>/<project>/` in one directory owned by the cluster's flyloft account, so a run's code can write any other campaign's; ownership enforced by per-campaign mounts from the cluster's prolog | When a campaign damages another's work |
258
+ | Campaigns that write shared data | When a campaign needs to build a dataset in place |
259
+ | Moving files from before the storage cutover into the new layout | When someone needs an old campaign's files |
260
+ | Per-owner disk quotas | When one owner's files crowd out the lab's |
249
261
  | Per-user or locked project scope, or the compare page flagging that two campaigns' stored project configurations differ; today any user may edit a shared project | When a colleague's edit invalidates a comparison |
250
262
  | Transferring a campaign's ownership | When a user leaves mid-campaign and someone must publish for them |
251
263
  | Usage accounting on the admin page: GPU-hours and cost per user and per project | When a PI asks who spent what |
252
264
  | Editing clusters and harnesses on the board (they carry secrets) | When someone other than the operator needs to add a cluster |
253
- | Notifications: a Slack incoming webhook that posts each decision-maker's note | When campaigns run unattended long enough that checking the board misses notes; at stage 6b a promotion's new draft pull request already notifies the repository's watchers |
265
+ | Notifications: a Slack incoming webhook that posts each decision-maker's note, and a role waiting on its model | When campaigns run unattended long enough that checking the board misses notes; at stage 6b a promotion's new draft pull request already notifies the repository's watchers |
254
266
  | Replication as the unit of judgment: a config is a tree measured at `replication` seeds in parallel; code compares paired means; nothing is measured twice; the integrator keeps the best of head, finding alone, and merged; one chart dot per config | Done in three plans, closed 2026-10-07: see [design/replication.md](design/replication.md) |
255
267
  | A Claude Code transcript on the turn page (`--output-format stream-json` in place of `json`, which writes only the final result) | When Claude Code runs live turns again; five of 2,663 live turns used it at board part 4 |
256
268
  | AG-UI as a second stream beside the board's change stream; the turn page reads a finished or open turn's transcript since board part 4 | When a live turn transcript is streamed to the board (the stage-5 campaign feed), or when a researcher needs to read a harness turn rather than its record entries |
@@ -275,7 +287,7 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
275
287
  | 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
288
  | 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
289
  | 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 |
290
+ | A tool-builder role | Done: the critic-judged implementer builds tools for the shed (see [design/toolshed.md](design/toolshed.md)) |
279
291
  | 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
292
  | 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
293
  | 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 |
@@ -284,10 +296,8 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
284
296
  | Template changes reaching projects created from it, by merge or by a diff against the recorded template commit | When a template is changed that its projects need |
285
297
  | A list of the user's GitHub organizations for a new repository's owner (needs the `read:org` scope) | When a mistyped owner is seen |
286
298
  | A setup agent on the host that prepares a project's repository and brief from a researcher's description | When researchers' own agents, following the lab's guidance, prove too slow or too uneven; the vision leaves the brief to the researcher |
287
- | Different harnesses for different roles in a campaign launched from the board | When a campaign needs one; a campaign file already allows it |
288
299
  | Fixed island charters and raw role tables on the board's launch page | When a researcher needs them; a campaign file covers both |
289
300
  | The board laid out for phones: the sidebar and the campaign page's side column at a narrow width | When a researcher needs to read or steer a campaign from a phone; board part 5's tier three found it works on desktop and not on mobile |
290
- | Each harness's models listed by asking the harness or its provider, in place of the `models` list in its configuration file | When updating a harness's `models` list for a new model becomes a chore; not every harness can list its models |
291
301
  | A Compute page on the board: each cluster asked what it has, per GPU type its file lists, with flyloft's jobs on each, and free counts beside the launch page's GPU types | Done: see [design/compute.md](design/compute.md) |
292
302
  | Cluster health and usage by cluster user on the admin page (nodes down or drained, unreachable clusters, who holds which GPUs), toward user quotas | Next after the Compute page |
293
303
  | GPU types discovered from `sinfo` in place of the list in each cluster's file | When keeping the lists becomes a chore |
@@ -305,6 +315,7 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
305
315
  | A per-project smoke time limit (a validation's is 30 minutes) | When a project's smoke run needs longer |
306
316
  | Cancelling a validation | When one is seen holding a GPU it should not |
307
317
  | 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 |
318
+ | 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
319
  | 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
320
  | 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
321
  | 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 +348,7 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
337
348
  | Artifact retention and garbage collection; W&B synchronization | When storage fills or a project asks |
338
349
  | Cost estimate on proposals | Done at stage 4a: the proposal's `cost` line, read by the coordinator |
339
350
  | 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 |
351
+ | 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
352
  | 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
353
  | An `.flyloft/` export at each promotion, not only at the stop | When a researcher needs turn transcripts before an indefinite campaign stops |
343
354
  | Timeout for runs and validations in unknown | When one is encountered |
@@ -377,10 +388,29 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
377
388
  | Searching or filtering the template list in New project | When a lab has more templates than fit on a screen |
378
389
  | 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
390
  | A size cap on an investigation run's collected `out/` | When a run is seen copying something large back to the host |
391
+ | 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
392
  | Computed completion for tasks: a checklist the ask is parsed into | When critics are seen accepting incomplete outputs |
381
393
  | Two campaigns writing a project's work directory at once | When two are seen colliding there |
382
394
  | 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 |
395
+ | 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 |
396
+ | Two campaigns building the same tool at once | When a second campaign is seen submitting a name another's build holds |
397
+ | 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 |
398
+ | A tool whose test needs more than one GPU or 30 minutes | When one is built |
399
+ | A private tool (a license that forbids redistribution): a squashfs on the cluster's shared storage | When one arrives |
400
+ | A `toolshed` command for laptops that picks podman or docker and mounts the working directory | When the skill's `podman run` line proves insufficient |
401
+ | 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 |
402
+ | User-specific credentials beyond the GitHub sign-in. The lab has no shared Claude or Codex account, only individual ones with their own budgets; runs should log to their owner's wandb; the local endpoints issue per-user tokens for usage statistics; and today `harnesses/claude.toml` names no `api_key_env`, so Claude Code finds the operator's login through `HOME` and every member's campaign spends the operator's subscription. An admin-owned catalogue in the configuration directory, `credentials/<NAME>.toml`, names each credential the lab uses: a label, how to get it, its kind (`token`, `login`, or `ssh_key`; the board asks for and accepts only that kind), and its scope, `host` (the default) or `runs`. Each user enters theirs on a Credentials page of the board, never through an agent, theirs or flyloft's: set, when, Replace, Remove, a value never shown again. Stored Fernet-encrypted like the GitHub token. A campaign uses its owner's credentials, the host's `secrets.env` filling a name the owner has not set: `host` ones reach turns' harness environment and tool builds, `runs` ones also reach runs on stdin at submission, as the archive key does; harness keys stay `host`. A harness's `api_key_env` names its key (`claude.toml` gets `CLAUDE_CODE_OAUTH_TOKEN`); launch is refused without it, and a turn never falls back to the operator's login. Only `token` is built first. Open: a CLI path, expiry warnings, how a `login` reaches a process | Designed: see [design/user-credentials.md](design/user-credentials.md) |
403
+ | Reserved catalogue names: a name matching a secret the host holds (`GITHUB_CLIENT_SECRET`, `COREWEAVE_S3_*`, a cluster's `password_env`) is filled from the host's environment for every owner, exporting that secret to turns (and to runs when its scope is `runs`); the catalogue check would refuse them | When an admin is seen to do it |
404
+ | Tool builds receiving the owner's credentials, such as a gated Hugging Face model | When the first build needs one |
405
+ | A CLI setter for credentials, with a hidden prompt | When someone asks for it, perhaps as part of an onboarding flow |
406
+ | Credential expiry: a lifetime in the catalogue, a warning on the Credentials page, and a credential marked refused when its service refuses it | When a credential expires on someone mid-campaign |
407
+ | Per-user cluster identity: every cluster operation of a campaign (staging, sbatch, observation, scancel, archive jobs) runs as its owner, with the owner's own cluster login (`ssh_key` or `login` from the credentials catalogue). Required on CoreWeave SUNK, which allows no account not tied to a person; on slinky it puts campaigns under Slurm's existing per-user quotas and accounting. Each campaign already has its own directory under its owner; per-user logins were discussed for cluster storage and found unneeded for now. Open: whose identity observes and archives | After user-specific credentials, whose catalogue it uses; needs its own design |
408
+ | Long-running services a campaign can call (a ColabFold MSA server with its terabyte of databases on shared storage, used by Boltz, Chai, and Protenix): first as lab infrastructure named in the cluster's config and exported to runs and assignments as a URL, the server itself a shed tool kept running; flyloft managing its lifecycle (start on demand, a lease, restart, stop when idle) only if keeping it up proves to be the pain | Brainstormed 2026-10-27; when the first campaign needs an MSA server |
409
+ | Declared parameters per tool command | When agents are seen misusing commands from prose |
410
+ | A tool's image per GPU architecture (Arm) | When an Arm partition is wanted |
411
+ | `flyloft tools` and the catalogue in the researcher's skill | When a researcher's agent needs the shed's contents while writing a brief |
412
+ | A tool run on a cluster without pyxis | When a cluster without it is configured |
413
+ | 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
414
  | 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
415
  | The flyloft skill for Codex and OpenCode | Done 2026-10-22: `flyloft install` serves every harness found |
386
416
  | `flyloft logout`, several hosts per laptop, refreshing a session before it expires | When one is missed |
@@ -392,6 +422,22 @@ Ideas that have arrived before their stage. Adding a line here is how an impulse
392
422
  | 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
423
  | 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
424
  | 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 |
425
+ | 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 |
426
+ | 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 |
427
+ | 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` |
428
+ | Archiving validations, whose directories stay on the cluster; object storage on-prem ([design/run-storage.md](design/run-storage.md) §6) | When either fills its volume |
429
+ | 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 |
430
+ | 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` |
431
+ | 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 |
432
+ | GPT models through Codex on each user's own ChatGPT subscription ([design/harness-defaults.md](design/harness-defaults.md) §6), benched 2026-10-28: a contained Codex turn would need the agent image, the bridge, and the owner's Codex home mounted; the built-but-unmerged substrate is on branch `harness-defaults` (PR #82) | Revisit through OpenCode first: OpenAI models authenticated there may prove simpler |
433
+ | TypeScript 7 for the board: `npm install -D typescript@7` and drop `baseUrl` from `web/tsconfig.json` (TS 7 removed it; `paths` is already relative); the board then typechecks clean | When openapi-typescript (`npm run types`) works with it: it builds `types.ts` through TypeScript's JavaScript compiler API, which TS 7 does not ship |
434
+ | A wall-clock cap on OpenCode turns (`max_turns` caps only Claude Code); a GLM implementer kept a task turn open for hours with `sleep` polls after `pass_turn` (#91) | When a model keeps a turn open despite #91's wording and `pass_turn` reply |
435
+ | `sleep` in the OpenCode bash deny list (`python -c` gets around it) | Only if a turn cap is not enough |
436
+ | A per-user cap on direct cluster jobs (`flyloft job`, [design/direct-jobs.md](design/direct-jobs.md)): concurrent jobs or GPUs, refused at submit; at launch only a 24-hour time limit per job (#133) | When one user's direct jobs fill a queue that campaigns or other people are waiting on |
437
+ | Direct jobs on the board: a researcher's jobs, their state, GPUs, and log tail, likely on the Compute page (#134) | When someone wants to see direct jobs outside their agent session |
438
+ | Archiving ended direct-job directories to object storage, as runs are | When job directories are seen piling up on a cluster |
439
+ | A direct job in a toolshed tool's image (`flyloft job submit --tool`), with the shed's tools listed in `flyloft options` (#135) | When a researcher's agent wants a direct job inside a tool |
440
+ | A cancel that lands while the tick is staging the same direct job is overwritten to queued, and the job runs | When a canceled direct job is seen running |
395
441
 
396
442
  ## References
397
443
 
@@ -20,25 +20,32 @@ campaign's metric. A project is one GitHub repository plus one registration on t
20
20
  | `brief_path` | The brief, `BRIEF.md` by convention |
21
21
  | `run_command` | The command one run executes, from the repository root |
22
22
  | `results_path` | The JSON file that command writes, `results.json` by convention |
23
- | `environment_setup` | Shell lines run before the job enters the repository |
23
+ | `environment_setup` | Shell lines run in the repository before the run command |
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_CAMPAIGN="<the campaign's directory on the cluster>"
39
+ export FLYLOFT_DATA="<the cluster's shared data directory>" # only when the cluster sets one
40
+ export FLYLOFT_SHARED="<the cluster's shared drive>" # only when the cluster sets one
41
+ cd "<run dir>/code"
36
42
  <environment_setup>
37
43
  export FLYLOFT_SEED=<seed> # each replicate's seed; 0 for a validation
38
44
  export FLYLOFT_SMOKE=1 # a validation only
39
- cd "<run dir>/code"
45
+ export FLYLOFT_OUT="<run dir>/out" # an investigation's or a task's own runs only
40
46
  <run_command>
41
47
  touch "<run dir>/finished"
48
+ '
42
49
  ```
43
50
 
44
51
  What follows from it:
@@ -50,9 +57,13 @@ What follows from it:
50
57
  the packages the project uses in flyloft's `docs/environments/`, or ask the researcher; write against those, and list
51
58
  them in the README. A package the image lacks is vendored into the repository or asked of the researcher, never
52
59
  installed by the job.
53
- - **`environment_setup` runs before the `cd`.** Its paths are absolute, or values the command resolves after the
54
- `cd`, such as `export DATA_DIR=data`.
60
+ - **`environment_setup` runs in `<run dir>/code`.** It can run the repository's committed scripts, such as
61
+ `bash setup_env.sh "$FLYLOFT_CAMPAIGN/envs/main"`.
55
62
  - **One job is the whole run.** Training, evaluation, and writing results all happen inside the time limit.
63
+ - **A run may restart.** Flyloft runs at the cluster's lowest tier, where a job running longer than 30 minutes can
64
+ be preempted and restarted from the top. The run command resumes from its own checkpoints, saved more often than
65
+ every 30 minutes, or starts over cleanly, writing outputs to a scratch path and moving them into place only when
66
+ complete. A preempted run gets SIGTERM five minutes before it is stopped, time enough to save a checkpoint.
56
67
 
57
68
  ## 2. The contract
58
69
 
@@ -89,10 +100,13 @@ creates the repository under the lab's organization, registers the project, and
89
100
  To register a repository by hand instead, use **My own repository** on **New project**, or `flyloft register <url>
90
101
  --name <name>`, leaving the run command and results path blank; there is nothing to validate. Launch it with
91
102
  **Investigate a question** or **Do a task**. Every job a critic-judged campaign runs has
92
- `$FLYLOFT_PROJECT_WORK`, the project's directory on the cluster, where environments, weights, and data stay for later
93
- campaigns, and `$FLYLOFT_OUT`, whose contents come back to the board. When the account recommends an optimization, add
94
- a run command that meets section 2, validate it, and launch **Optimize a metric** on the same project: what the
95
- investigation built is still in `$FLYLOFT_PROJECT_WORK`.
103
+ `$FLYLOFT_CAMPAIGN`, the campaign's own directory on the cluster, where it builds environments, data, and the weights
104
+ of models it trains; `$FLYLOFT_DATA` and `$FLYLOFT_SHARED`, when the cluster sets them, read and never written; and
105
+ `$FLYLOFT_OUT`, whose contents come back to the board. A campaign's files are written only in its own directory; an
106
+ earlier campaign's are read, or copied in first. When the account recommends an optimization, add a run command that
107
+ meets section 2, validate it, and launch **Optimize a metric** on the same project. Its `environment_setup` builds
108
+ the environment in `$FLYLOFT_CAMPAIGN/envs/` if it is missing, under `flock "$FLYLOFT_CAMPAIGN/envs/.lock"`, and
109
+ activates it; the setup script is committed.
96
110
 
97
111
  The rest of this guide applies to projects with a run command.
98
112
 
@@ -186,6 +200,7 @@ Reading a validation's report:
186
200
  | passed | The job finished and wrote a flat object of numbers, which the report lists | The project runs: launch |
187
201
  | 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
202
  | 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 |
203
+ | interrupted | The cluster preempted the job and never restarted it; this says nothing about the project | Validate again |
189
204
 
190
205
  A failed or incomplete validation means fix the repository, push, and validate again; the report is written to be
191
206
  pasted back to you. A passed validation shows that the code runs at all, not that a campaign will succeed. A smoke
@@ -219,13 +234,13 @@ time = "01:00:00" # HH:MM:SS, Slurm's limit for one run; a run is ch
219
234
  # allow for replicate seeds sharing a node: they run in parallel
220
235
 
221
236
  [roles.implementer]
222
- harness = "claude" # a file name under harnesses/, without .toml
237
+ harness = "opencode" # a file name under harnesses/, without .toml
223
238
 
224
239
  [roles.critic]
225
- harness = "claude"
240
+ harness = "opencode"
226
241
 
227
242
  [roles.integrator]
228
- harness = "claude"
243
+ harness = "opencode"
229
244
  ```
230
245
 
231
246
  A cluster's GPU types are the ones its file in the configuration directory lists. The operator writes them, each with