outerloop-science 0.1.0.dev3__tar.gz → 0.1.0.dev4__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 (180) hide show
  1. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/CHANGELOG.md +63 -0
  2. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/PKG-INFO +1 -1
  3. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/architecture.md +5 -5
  4. outerloop_science-0.1.0.dev4/docs/design/base-reintegration.md +92 -0
  5. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/onboarding.md +5 -5
  6. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/reviewer-infra.md +3 -3
  7. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/role-cli.md +1 -1
  8. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/install.md +30 -5
  9. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/validation/author-syscalls.md +1 -1
  10. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/__init__.py +1 -1
  11. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/appauth.py +17 -0
  12. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/attempt.py +66 -8
  13. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/cli.py +64 -1
  14. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/climbboard.py +64 -22
  15. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/followup.py +2 -9
  16. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/github.py +32 -12
  17. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/harness.py +21 -29
  18. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/init.py +26 -1
  19. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/maintain.py +29 -1
  20. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/maintain_post_cli.py +4 -4
  21. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/review_summarize_cli.py +1 -1
  22. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/steward.py +2 -9
  23. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/tick.py +184 -135
  24. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_appauth.py +19 -0
  25. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_attempt.py +35 -0
  26. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_followup.py +25 -10
  27. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_github.py +17 -0
  28. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_init.py +46 -0
  29. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_maintain.py +17 -5
  30. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_start.py +99 -0
  31. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_tick.py +160 -1
  32. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/uv.lock +60 -60
  33. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/.gitignore +0 -0
  34. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/.pre-commit-config.yaml +0 -0
  35. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/.python-version +0 -0
  36. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/CLAUDE.md +0 -0
  37. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/CONTRIBUTING.md +0 -0
  38. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/LICENSE +0 -0
  39. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/NOTICE +0 -0
  40. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/README.md +0 -0
  41. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/RELEASING.md +0 -0
  42. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/SECURITY.md +0 -0
  43. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/containers/README.md +0 -0
  44. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/containers/agent-py312.def +0 -0
  45. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/assets/icon-dark.svg +0 -0
  46. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/assets/icon-light.svg +0 -0
  47. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/assets/icon.svg +0 -0
  48. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/community.md +0 -0
  49. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/compute.md +0 -0
  50. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/contract.md +0 -0
  51. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/agent-substrate.md +0 -0
  52. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/consolidation.md +0 -0
  53. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/dispatcher.md +0 -0
  54. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/eval-cache.md +0 -0
  55. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/external.md +0 -0
  56. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/github-app-auth.md +0 -0
  57. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/headline.md +0 -0
  58. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/judge-placement.md +0 -0
  59. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/meta.md +0 -0
  60. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/orchestrator-verify.md +0 -0
  61. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/public-surface.md +0 -0
  62. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/research-lines.md +0 -0
  63. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/research-loop-buildout.md +0 -0
  64. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/research-loop.md +0 -0
  65. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/resident-tick.md +0 -0
  66. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/review-placement.md +0 -0
  67. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/roles.md +0 -0
  68. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/scaling.md +0 -0
  69. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/design/session-watcher.md +0 -0
  70. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/reviewer.md +0 -0
  71. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/docs/roadmap.md +0 -0
  72. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/examples/review.yml +0 -0
  73. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/pyproject.toml +0 -0
  74. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/README.md +0 -0
  75. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/install_codex.sh +0 -0
  76. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/install_hermes.sh +0 -0
  77. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/requeue_moved_successors.sh +0 -0
  78. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/setup_branch_protection.sh +0 -0
  79. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/sweep_git_locks.sh +0 -0
  80. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/tick_chain.sbatch +0 -0
  81. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/tick_deploy.sh +0 -0
  82. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/scripts/tick_resident.sh +0 -0
  83. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/__main__.py +0 -0
  84. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/appmanifest.py +0 -0
  85. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/brief.py +0 -0
  86. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/compute.py +0 -0
  87. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/contract.py +0 -0
  88. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/contract_cli.py +0 -0
  89. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/disk.py +0 -0
  90. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/dispatch.py +0 -0
  91. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/evalcache.py +0 -0
  92. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/housekeeping.py +0 -0
  93. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/image.py +0 -0
  94. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/intake.py +0 -0
  95. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/launchlog.py +0 -0
  96. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/limits.py +0 -0
  97. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/maintain_agent_cli.py +0 -0
  98. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/markers.py +0 -0
  99. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/measure.py +0 -0
  100. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/orchestrator.py +0 -0
  101. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/panel.py +0 -0
  102. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/paths.py +0 -0
  103. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/posting.py +0 -0
  104. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/progress.py +0 -0
  105. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/py.typed +0 -0
  106. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/review.py +0 -0
  107. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/review_agent.py +0 -0
  108. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/review_agent_cli.py +0 -0
  109. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/review_post_cli.py +0 -0
  110. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/role_runner.py +0 -0
  111. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/roles.py +0 -0
  112. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/rolespec.py +0 -0
  113. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/runstate.py +0 -0
  114. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/style.py +0 -0
  115. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/syscall.py +0 -0
  116. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/syscall_cli.py +0 -0
  117. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/verifier.py +0 -0
  118. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/verify_agent.py +0 -0
  119. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/verify_agent_cli.py +0 -0
  120. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/verify_post_cli.py +0 -0
  121. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/src/outerloop/watcher.py +0 -0
  122. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/conftest.py +0 -0
  123. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/fakes.py +0 -0
  124. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/helpers.py +0 -0
  125. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_appmanifest.py +0 -0
  126. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_bot_aliases.py +0 -0
  127. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_bot_login.py +0 -0
  128. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_brief.py +0 -0
  129. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_channel_dir.py +0 -0
  130. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_climbboard.py +0 -0
  131. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_codex_harness.py +0 -0
  132. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_compute.py +0 -0
  133. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_contract.py +0 -0
  134. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_contract_cli.py +0 -0
  135. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_contract_names.py +0 -0
  136. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_disk.py +0 -0
  137. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_dispatch.py +0 -0
  138. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_env_bridge.py +0 -0
  139. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_evalcache.py +0 -0
  140. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_hardening.py +0 -0
  141. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_harness.py +0 -0
  142. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_hermes_harness.py +0 -0
  143. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_housekeeping.py +0 -0
  144. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_image.py +0 -0
  145. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_import.py +0 -0
  146. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_intake.py +0 -0
  147. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_launchlog.py +0 -0
  148. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_limits.py +0 -0
  149. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_local_compute.py +0 -0
  150. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_markers.py +0 -0
  151. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_measure.py +0 -0
  152. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_measure_and_decide.py +0 -0
  153. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_orchestrator.py +0 -0
  154. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_packaging.py +0 -0
  155. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_panel.py +0 -0
  156. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_paths.py +0 -0
  157. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_posting.py +0 -0
  158. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_progress.py +0 -0
  159. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_requeue_moved_successors.py +0 -0
  160. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_review.py +0 -0
  161. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_review_agent.py +0 -0
  162. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_review_agent_cli.py +0 -0
  163. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_review_hardening.py +0 -0
  164. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_review_policy.py +0 -0
  165. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_review_summarize.py +0 -0
  166. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_role_runner.py +0 -0
  167. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_rolespec.py +0 -0
  168. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_runstate.py +0 -0
  169. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_steward.py +0 -0
  170. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_sweep_git_locks.py +0 -0
  171. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_syscall.py +0 -0
  172. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_syscall_cli.py +0 -0
  173. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_tick_chain_successors.py +0 -0
  174. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_tick_resident.py +0 -0
  175. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_tiers.py +0 -0
  176. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_verifier.py +0 -0
  177. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_verify_agent.py +0 -0
  178. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_verify_agent_cli.py +0 -0
  179. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_version.py +0 -0
  180. {outerloop_science-0.1.0.dev3 → outerloop_science-0.1.0.dev4}/tests/test_watcher.py +0 -0
@@ -6,8 +6,58 @@ Versions follow [SemVer](https://semver.org).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Changed
10
+
11
+ - Install guide: a lab member who is not an org owner now has documented steps
12
+ for GitHub App auth — make the personal App public, request the install, an
13
+ org owner approves, then optionally transfer the App to the org — matching
14
+ what `init --github-app` prints on the personal-App fallback.
15
+ - Docs: corrected stale `autoresearch` command and module paths left by the
16
+ rename (onboarding, author-syscalls, architecture, role-cli), refreshed the
17
+ architecture component map (`budget` → `limits`, `report` →
18
+ `progress`/`climbboard`), and documented the tick scheduling knobs
19
+ `OUTERLOOP_CADENCE_MIN`, `OUTERLOOP_MAX_JOB_MINUTES`, and
20
+ `OUTERLOOP_MIN_TICK_MINUTES` in the install guide.
21
+
22
+ - The maintainer digest issue now carries the scan date in its title
23
+ (`Maintainer digest — YYYY-MM-DD`), so its freshness shows in the issue list.
24
+ The scan's long verdict and rejected-findings text now folds into a
25
+ collapsible block, keeping the top of the issue a short summary (title,
26
+ advisory line, counts).
27
+
28
+ ### Fixed
29
+
30
+ - `outerloop init --github-app` no longer dead-ends a non-owner of an
31
+ organization. Creating an org-owned App needs org-owner rights, so GitHub
32
+ makes a personal App that will not install on the org repo by default. init
33
+ now detects that the App landed under a different account and prints the path
34
+ that works — make it public, request the install, have an org owner approve
35
+ it, then rerun `init --force --github-app` (and optionally transfer the App to
36
+ the org). The install-failure help names the same steps.
37
+
38
+ ### Fixed
39
+
40
+ - The follow-up finalizer no longer gets stuck withholding a measured re-sync
41
+ when auto-merge is already off. Before pushing a re-measured head it confirms
42
+ the PR will not auto-merge by the PR's actual auto-merge state, rather than by
43
+ matching one error string — whose text varies with the repo's "Allow
44
+ auto-merge" setting and the token type — so a merge:manual repo under a GitHub
45
+ App token no longer re-withholds its successful re-measurements.
46
+
9
47
  ### Added
10
48
 
49
+ - `outerloop upgrade`: one verb for a local install to move to the newest
50
+ release (`--pre` to track pre-releases), the local counterpart of the Slurm
51
+ resident tick's `OUTERLOOP_AUTO_UPDATE` policy. It runs `pip install --upgrade`,
52
+ reports the version change, and reminds you to restart the loop.
53
+ - Base reintegration (docs/design/base-reintegration.md): when a research
54
+ line's base moves while it sleeps, its wake fetches the fresh base and tells
55
+ the agent to merge it and decide what to re-run — the same fetch-and-merge
56
+ the in-review conflict wake already uses, so a long depth run no longer
57
+ drifts behind sibling merges and opens a stale PR.
58
+ - `docs/design/base-reintegration.md`: a proposal to keep a running line's
59
+ base current by re-pinning at each wake and to reconcile a PR whose base
60
+ moved after the run ended, with the owner's five decisions and the build order.
11
61
  - A weekly maintenance digest (`maintenance-agent.yml`, reusable; the kernel's
12
62
  own caller is `maintenance.yml`): read-only lens sessions scan a repository's
13
63
  default branch for dead pathways, duplicated logic, oversized modules, stale
@@ -649,6 +699,19 @@ Versions follow [SemVer](https://semver.org).
649
699
 
650
700
  ### Changed
651
701
 
702
+ - CI and reusable-workflow Actions bumped to current majors: actions/checkout
703
+ v7, astral-sh/setup-uv v10, upload-artifact v7, download-artifact v8. The
704
+ single-name artifact downloads stay flat and the pattern download reads
705
+ recursively, so the review split is unaffected; the node24 runtime is on the
706
+ hosted runners.
707
+ - The tick reads its run records once per phase instead of once per service: a
708
+ single snapshot after the mutation phase feeds the read services, and one
709
+ feeds the board pass. About twelve full run-directory scans per tick become
710
+ five, which matters under a large fleet (the 2026-09-03 disk pressure); the
711
+ freshness guards that re-read a single record are unchanged.
712
+ - The maintenance scan gains an `architecture` lens: it proposes abstraction
713
+ simplifications and missing extension points (a new backend, benchmark, or
714
+ role should need zero kernel change), as decision-kind digest items.
652
715
  - Helpers shared across kernel modules are public in their owning module:
653
716
  `brief.code_fence`, `brief.cap`, `attempt.target_clone_url`,
654
717
  `attempt.stage_launch_job_ids`, `orchestrator.metric_from_output`,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: outerloop-science
3
- Version: 0.1.0.dev3
3
+ Version: 0.1.0.dev4
4
4
  Summary: Autonomous research agents that improve the benchmark you point them at, one verified pull request at a time
5
5
  Project-URL: Homepage, https://outerloop.science
6
6
  Project-URL: Repository, https://github.com/outerloop-science/outerloop
@@ -35,7 +35,7 @@ scope:
35
35
  roadmap: docs/roadmap.md
36
36
  ```
37
37
 
38
- The schema lives here (`autoresearch.contract`). The loader hard-codes invariants
38
+ The schema lives here (`outerloop.contract`). The loader hard-codes invariants
39
39
  no YAML can override: `.github/`, the contract file itself, and the roadmap are
40
40
  always forbidden write paths; the contract is read from the default branch only;
41
41
  **autoresearch is never a valid target of itself**.
@@ -278,7 +278,7 @@ insights across all targets and must never be part of any public flip. Raw
278
278
  metrics stay in W&B; the notebook links to runs. No database or vector store —
279
279
  grep + recency + distillation until that provably fails.
280
280
 
281
- ## Components (`src/autoresearch/`)
281
+ ## Components (`src/outerloop/`)
282
282
 
283
283
  | Module | Job |
284
284
  | --- | --- |
@@ -287,8 +287,8 @@ grep + recency + distillation until that provably fails.
287
287
  | `orchestrator` | Tick logic: sentinel, lease, task selection, session dispatch, state sync |
288
288
  | `compute` | sbatch/squeue submit-and-poll behind one interface |
289
289
  | `github` | Bot auth and push (orchestrator-side, after sessions end), PR/issue ops |
290
- | `budget` | Hard caps: $ per run, weekly $, GPU-hours, PRs per week; subscription backends metered by a session/token proxy |
291
- | `report` | Per-run research reports (takeaways + next steps), notebook writes + distillation, weekly digests, cost ledger, leaderboard history |
290
+ | `limits` | Effective session and job limits — turns, session and job minutes — with each contract wish clamped into our `[floor, ceiling]`; the tick's `OUTERLOOP_MAX_JOB_MINUTES` floors here |
291
+ | `progress` / `climbboard` | `progress` writes the human-readable benchmark-progress files into the target repo on each improvement PR; `climbboard` publishes the climb board — every ended attempt and its outcome, plus the ledger and views — to the target's `research-log` branch whenever runs end |
292
292
 
293
293
  ## Harness and context engineering
294
294
 
@@ -309,7 +309,7 @@ exactly what any given agent saw, and a bad run can be replayed from its brief:
309
309
  | Ruler: how the metric is computed, how claims get re-verified | target repo docs | fixed |
310
310
  | Lessons: distilled, bounded per-target lessons file | notebook `lessons/<target>.md` | hard cap |
311
311
  | Recent history: last N run reports for this target (incl. failures) | notebook `runs/<target>/` | hard cap, newest first |
312
- | Budget state: remaining GPU-hours/$/PRs this week | `budget` | fixed |
312
+ | Budget state: remaining GPU-hours/$/PRs this week | tick weekly accounting | fixed |
313
313
 
314
314
  Everything else is deliberately absent: no other targets' data (cross-target
315
315
  separation), no raw transcripts (distillation instead), no maintainer-private
@@ -0,0 +1,92 @@
1
+ # Base reintegration: keeping a running line current, and reconciling its PR
2
+
3
+ Status: proposal (2026-09-08). Extends `research-lines.md`, which owns the
4
+ per-agent branch and the "merge main in at run start" rule. Prompted by a
5
+ real case, PR #12 on gpt-speedrun.
6
+
7
+ ## The problem, concretely
8
+
9
+ agent-04's line started 2026-09-04 in the morning. That evening a sibling
10
+ merged #10, moving the record from 8192 to 7808. agent-04's line kept
11
+ iterating on its own 8192-era base for three more days and opened PR #12
12
+ claiming 7232, against a base that had been superseded on day one.
13
+
14
+ The claim was real: 7232 came from setting `warmdown_iters` to 6400, the same
15
+ knob #10 set to 4352, on a monotonic trend, and it holds against the current
16
+ base. But the PR conflicted, and by the time it opened, the run had
17
+ terminated, so nothing in the kernel could reconcile it. A human merged it by
18
+ hand.
19
+
20
+ Two gaps produced this:
21
+
22
+ - A line merges main only at run start. A long depth run therefore drifts
23
+ arbitrarily far from main as siblings merge, and `research-lines.md` already
24
+ names the danger ("a stale line reverting others' wins").
25
+ The kernel already reconciles a PR whose base moves AFTER it opens: an open PR
26
+ keeps its run in the in-review state, and the tick's follow-up conflict wake
27
+ (`followup.py`) fetches the moved base into the workspace and asks the agent to
28
+ merge and re-measure. So the only real gap is the first bullet — a line that
29
+ never re-syncs main mid-run and opens its PR against a base superseded days
30
+ earlier.
31
+
32
+ ## Proposed protocol
33
+
34
+ 1. **Re-pin at each wake, not only at run start.** When a line wakes for its
35
+ next depth iteration, the kernel merges main into the line and hands the
36
+ agent a short digest of what changed since it last ran — the sibling record
37
+ moves and the files they touched. The agent decides what to re-run given
38
+ the new base. This bounds drift to a single iteration instead of the whole
39
+ run.
40
+
41
+ 2. **Never mid-experiment.** A running eval keeps its base. Re-pinning happens
42
+ only at the iteration boundary, the sleep-to-wake seam, so no in-flight
43
+ measurement is invalidated and no launch is wasted.
44
+
45
+ 3. **Re-measure the baseline at the new merge-base.** This is the roadmap's
46
+ "baseline re-run at merge-base." `research-lines.md` already makes every
47
+ claim name the baseline pair it was measured against, so a re-pinned line
48
+ stays legible about which base each number used.
49
+
50
+ 4. **Keep a PR reconcilable after the run's terminal.** A line whose PR is
51
+ open stays reachable until the PR is merged or closed, so a base that moves
52
+ after the PR opens fires the existing conflict wake — merge main,
53
+ re-measure, push — instead of stranding the PR for a human.
54
+
55
+ ## Decisions (owner, 2026-09-08)
56
+
57
+ The semantic choices, settled — the mechanics follow from them.
58
+
59
+ 1. **Re-pin cadence: only when main moved.** Each wake does a cheap
60
+ fetch-and-compare; it re-merges and re-pins only when a sibling actually
61
+ landed something. No churn when nothing changed.
62
+ 2. **What the agent sees: the record move, plus each sibling PR's metric and a
63
+ one-line summary — not full diffs.** Enough to decide whether its line is
64
+ still worth pursuing, without flooding its context.
65
+ 3. **A superseded axis: tell, don't force.** The wake digest says its axis was
66
+ beaten (e.g. "warmdown was taken further by #10"); the agent decides to
67
+ pivot or push on. The kernel never kills a line for it.
68
+ 4. **Healing a PR after its base moves: already handled, no new mechanism.**
69
+ An open PR's run stays in-review, and the follow-up conflict wake already
70
+ fetches the moved base and asks the agent to merge and re-measure. The
71
+ note's earlier "post-terminal" framing was wrong (terra, #341): the run is
72
+ not terminated while its PR is open. Base reintegration is therefore stage 1
73
+ alone — closing the during-run drift; the post-open case needs nothing new.
74
+ 5. **Who pays the re-measure GPU: only when the agent keeps its change.** The
75
+ re-measure is then a normal launch against the line's own budget. No eager
76
+ baseline re-runs on every merge.
77
+
78
+ **Build order.** The whole feature is one stage: re-pin at wake with the digest
79
+ (decisions 1–3), the re-measure paid only on keep (decision 5). Decision 4 needs
80
+ no code — the existing follow-up conflict wake already covers a base that moves
81
+ after the PR opens.
82
+
83
+ ## Why this shape addresses the problem
84
+
85
+ The base pin is correct during a run — you cannot measure improvement against
86
+ a moving target. The failure is not the pin; it is holding one pin for a
87
+ multi-day run and having no reconciliation once the run ends. Waking the line to merge the fresh base keeps the stability the pin gives
88
+ while bounding the drift to a single iteration, and it reuses the exact
89
+ mechanism the in-review conflict wake already uses — the kernel fetches, the
90
+ agent merges — rather than adding new kernel git machinery. The post-open case
91
+ needs nothing more: an open PR's run stays in-review and that same conflict
92
+ wake already reconciles it.
@@ -15,7 +15,7 @@ honest when it says three steps:
15
15
 
16
16
  ## What the wizard does
17
17
 
18
- `python -m autoresearch.init` (interactive; every answer has a flag for
18
+ `python -m outerloop.init` (interactive; every answer has a flag for
19
19
  non-interactive use):
20
20
 
21
21
  - **Detect compute.** `sbatch` on PATH → Slurm mode (prompt for account and
@@ -47,7 +47,7 @@ non-interactive use):
47
47
  The doctor is re-runnable on its own (`init --doctor`) and is the first
48
48
  thing support asks for.
49
49
  - **Print the start command.** Nothing starts implicitly. The command is
50
- `autoresearch start` on both paths: it submits the resident tick where
50
+ `outerloop start` on both paths: it submits the resident tick where
51
51
  `sbatch` exists and runs the local loop elsewhere, taking the root and
52
52
  placement from flags, the environment, or the `.env` the wizard wrote.
53
53
 
@@ -73,7 +73,7 @@ simultaneously the zero-Slurm on-ramp and the paper's Karpathy-loop
73
73
  ablation cell (width 1 × serialized) — the same kernel, the compute seam
74
74
  swapped, nothing else different.
75
75
 
76
- - **The chain is a loop.** No sbatch → `autoresearch start` runs
76
+ - **The chain is a loop.** No sbatch → `outerloop start` runs
77
77
  `tick --loop`, a tick every cadence in the foreground. State stays in records on
78
78
  disk, so killing and restarting the loop resumes exactly like the Slurm
79
79
  chain surviving a dead tick.
@@ -98,8 +98,8 @@ swapped, nothing else different.
98
98
  `OUTERLOOP_COMPUTE` selection at the two `SlurmCompute()` sites,
99
99
  `tick --loop`, local-mode wake-arming skip, per-mode required-env
100
100
  relaxation (no account/partition in local mode), and the one launch
101
- command `autoresearch start` (`src/autoresearch/cli.py`). Done.
102
- 2. **The wizard**: `autoresearch.init` — detection, prompts, `.env` writer,
101
+ command `outerloop start` (`src/outerloop/cli.py`). Done.
102
+ 2. **The wizard**: `outerloop.init` — detection, prompts, `.env` writer,
103
103
  doctor (PAT path first).
104
104
  3. **The manifest flow**: App mint + installation discovery inside the
105
105
  wizard.
@@ -252,9 +252,9 @@ cron (weekly) or by hand. It fans out one read-only lens session per digest
252
252
  section (`maintain.MAINTENANCE_LENSES`, plus `general` for the whole
253
253
  checklist) over a checkout of the calling repository's default branch, merges
254
254
  the opinions with the same summarizer the wide review round uses, and posts
255
- ONE rolling issue ("Maintainer digest") whose body each scan replaces;
256
- earlier digests stay in the edit history, and a short comment per scan
257
- notifies watchers. The role is `roles.maintainer_spec`: the reviewer's tools
255
+ ONE rolling issue (titled "Maintainer digest — <date>", the date of the digest
256
+ it currently shows) whose body each scan replaces; earlier digests stay in the
257
+ edit history, and a short comment per scan notifies watchers. The role is `roles.maintainer_spec`: the reviewer's tools
258
258
  and verdict shape (findings through the syscall tool, no scope), a larger
259
259
  budget because a tree is more to read than a diff. Items carry a section
260
260
  (`--category`), an effort and risk estimate, and a kind: `change` is
@@ -30,7 +30,7 @@ The invariants, proven on #132/#133 and non-negotiable everywhere:
30
30
  kernel-side (authoritative validators, PAT-out-of-session, scope, budgets).
31
31
  The tool is ergonomics and fast feedback; authority stays in the kernel.
32
32
  2. **Sandbox-side tools are standalone** (stdlib-only — the target repo has no
33
- autoresearch), which duplicates a little advisory validation, pinned by
33
+ outerloop), which duplicates a little advisory validation, pinned by
34
34
  parity tests (`test_artifact_path_check_matches_the_kernel`).
35
35
  **Orchestrator-side roles** (reviewer/verifier/panel/planner/steward run on
36
36
  our own checkout, not a target sandbox) import the real validators — no
@@ -105,8 +105,9 @@ always exits successfully so your PR stays green.
105
105
 
106
106
  The same reviewer, pointed at your whole repository once a week: dead code,
107
107
  duplicated logic, oversized modules, stale pins, slow tests, repeated work on
108
- the hot path, documentation drift. It writes one issue, "Maintainer digest",
109
- and replaces its body each scan. It changes no code and opens no work orders.
108
+ the hot path, documentation drift. It writes one issue, titled "Maintainer
109
+ digest — <date>" (the date of the digest it shows), and replaces its body each
110
+ scan. It changes no code and opens no work orders.
110
111
  Add `.github/workflows/maintenance.yml` with the same secret as the reviewer:
111
112
 
112
113
  ```yaml
@@ -227,6 +228,21 @@ declares Contents, Issues and Pull requests read-write and Metadata read,
227
228
  nothing else. If the install step was cut short, run
228
229
  `outerloop init --force --github-app` to finish and re-check it.
229
230
 
231
+ **If you are a member, not an owner, of the organization.** Creating an App
232
+ *owned by the org* needs org-owner rights, so init falls back to creating one
233
+ under your personal account — and a personal App does not install on an org
234
+ repo by default. You do not need an org-owned App or a shared key:
235
+
236
+ 1. In the App's settings, make it **public** (a private App installs only on
237
+ its owner's account, which is why the org repo is not offered).
238
+ 2. Request its installation on your repo; an **org owner approves** the request.
239
+ 3. Run `outerloop init --force --github-app` to record the installation.
240
+
241
+ Your key never leaves your machine. To let the lab own the App centrally later,
242
+ transfer it to the org from the App's Advanced settings — the same key keeps
243
+ working, so nothing has to be recreated. init prints these steps itself when it
244
+ detects the personal-App fallback.
245
+
230
246
  **A fine-grained PAT — the fallback.** For an org that already runs a machine
231
247
  user, or one where you cannot create Apps: pick `pat` (or pass `--pat-file`).
232
248
  Mint the token on the machine user with these settings:
@@ -284,8 +300,10 @@ release tag at the next cadence (pre-releases included; the repo is public, so
284
300
  no credential is needed). `main` follows every merge and is meant for the
285
301
  kernel's own developers. Whatever moves the checkout, you or the policy, the
286
302
  environment is synced to the commit that is checked out, or the deploy rolls
287
- back to the last commit whose environment was installed. The local loop runs the installed package: upgrade it with
288
- `pip install -U outerloop-science` and start it again.
303
+ back to the last commit whose environment was installed. The local loop runs
304
+ the installed package, which has no such policy: run `outerloop upgrade` to move
305
+ it to the newest release (add `--pre` to track pre-releases), then start it
306
+ again. That is `pip install --upgrade outerloop-science` under one verb.
289
307
 
290
308
  Experiments run wherever your `compute` backend says. Slurm is the first
291
309
  backend; the interface is small (submit a job, poll for completion), so a CI
@@ -318,7 +336,14 @@ still read). A Codex author always runs contained, so it also needs the image
318
336
  cluster, evals run inside the Apptainer image at `OUTERLOOP_IMAGE` (default
319
337
  `~/outerloop-images/agent-py312.sif`) in a jail that binds only the
320
338
  checked-out tree — an eval that needs data must fetch it into the tree, and
321
- GPU jobs are requested per node (`--gpus-per-node`).
339
+ GPU jobs are requested per node (`--gpus-per-node`). The tick has three
340
+ scheduling knobs. `OUTERLOOP_CADENCE_MIN`, read from the `.env`, is how often the
341
+ chain ticks (minutes; default 30). Two finer ones are read from the tick's own
342
+ environment (set at launch, not the per-tick `.env`): `OUTERLOOP_MIN_TICK_MINUTES`
343
+ coalesces ticks that land too close together (0 disables; unset defaults to the
344
+ lesser of 10 minutes and half the cadence), and `OUTERLOOP_MAX_JOB_MINUTES` caps
345
+ the walltime the tick requests for the climb and author-sleep wake jobs it sizes
346
+ (clamped under a code ceiling).
322
347
 
323
348
  **Local mode without an image.** On a machine with no Apptainer image,
324
349
  `OUTERLOOP_COMPUTE=local` still runs. Sessions run under the harness's own
@@ -22,7 +22,7 @@ Then run one climb by hand (not via the tick):
22
22
 
23
23
  ```bash
24
24
  source env.sh
25
- uv run python -m autoresearch.attempt \
25
+ uv run python -m outerloop.attempt \
26
26
  --target <org/repo> --benchmark <a-cheap-benchmark> \
27
27
  --run-root <run-root> --image <image.sif> \
28
28
  <the account/partition/limit args the tick normally passes>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
 
5
- __version__ = "0.1.0.dev3"
5
+ __version__ = "0.1.0.dev4"
6
6
 
7
7
 
8
8
  def _bridge_legacy_env() -> None:
@@ -17,8 +17,10 @@ network; the production signer lives behind the `app-auth` extra.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import argparse
20
21
  import base64
21
22
  import json
23
+ import os
22
24
  import time
23
25
  import urllib.error
24
26
  import urllib.parse
@@ -29,6 +31,7 @@ from pathlib import Path
29
31
  from typing import Any
30
32
 
31
33
  from outerloop.github import AUTH_SAFE_OPENER, FileTokenProvider, TokenProvider
34
+ from outerloop.paths import CONFIG_DIR
32
35
 
33
36
  # RS256-sign the JWT signing input, returning the raw signature bytes.
34
37
  Signer = Callable[[bytes], bytes]
@@ -211,3 +214,17 @@ def resolve_bot_auth(pat_file: str | Path, app_file: str | Path = "") -> TokenPr
211
214
  if str(app_file).strip():
212
215
  return app_provider_from_file(Path(str(app_file)).expanduser())
213
216
  return FileTokenProvider(Path(str(pat_file)).expanduser())
217
+
218
+
219
+ def add_credential_args(parser: argparse.ArgumentParser) -> None:
220
+ """Add the shared bot-auth options to a role parser: `--pat-file`, and
221
+ `--github-app-file`, which supplies installation tokens instead of the PAT
222
+ when set. `resolve_bot_auth` reads the pair. One owner so the defaults and
223
+ help cannot drift between attempt, followup, and steward."""
224
+ parser.add_argument("--pat-file", default=str(CONFIG_DIR / "bot_pat"))
225
+ parser.add_argument(
226
+ "--github-app-file",
227
+ default=os.environ.get("OUTERLOOP_GITHUB_APP_FILE", ""),
228
+ help="GitHub App config (JSON: app_id, installation_id, private_key); "
229
+ "when set, installation tokens replace the PAT",
230
+ )
@@ -26,7 +26,7 @@ from functools import partial
26
26
  from pathlib import Path
27
27
  from typing import Any, cast
28
28
 
29
- from outerloop.appauth import resolve_bot_auth
29
+ from outerloop.appauth import add_credential_args, resolve_bot_auth
30
30
  from outerloop.brief import BudgetState, distill_lessons
31
31
  from outerloop.compute import LocalCompute
32
32
  from outerloop.contract import Benchmark, Contract, contract_text_in_tree, load_contract
@@ -928,6 +928,22 @@ def _wake_author_sleep(
928
928
  if extra_update:
929
929
  # a submitted park's gate/panel feedback leads; launch results follow
930
930
  wake_text = f"{extra_update}\n\n{wake_text}"
931
+ # A research line whose base moved while it slept RE-PINS to the fresh base
932
+ # and is told to merge it: the agent does the merge (mirroring the in-review
933
+ # conflict wake, followup.py), the kernel only fetches and re-pins. Re-pinning
934
+ # base_sha to the fresh head is what makes the gate baseline and the scope
935
+ # base the CURRENT base (like followup's base_sha_at_fetch), so a sibling's
936
+ # merged work is never credited to this line and a forbidden conflict
937
+ # resolution (differing from the fresh base) is still scope-checked. Non-line
938
+ # runs and an unmoved base are untouched.
939
+ if _line_ref_for(bench, config.agent_id):
940
+ fresh_base = _line_base_advanced(ws, base_branch, base_sha)
941
+ if fresh_base:
942
+ digest = _reintegration_digest(ws, base_sha, fresh_base)
943
+ base_sha = fresh_base
944
+ wake_text = (
945
+ REINTEGRATE_PROMPT.format(base_branch=base_branch, digest=digest) + wake_text
946
+ )
931
947
  _best_effort(
932
948
  "budget refresh",
933
949
  lambda: write_budget(
@@ -1296,6 +1312,54 @@ def _line_ref_for(bench: Benchmark | None, agent_id: str) -> str:
1296
1312
  return f"agents/{agent_id}"
1297
1313
 
1298
1314
 
1315
+ # The base moved under a line while it slept: rather than the kernel doing a
1316
+ # git merge (which mishandles the agent's in-flight tree, its conflicts, and
1317
+ # the scope gate), the wake mirrors the in-review conflict wake — fetch the
1318
+ # fresh base into the workspace and TELL THE AGENT to merge it. The agent has
1319
+ # git and already resolves the run-start merge as its first task; only the
1320
+ # credential-bearing fetch/push are the kernel's. No commit text goes in the
1321
+ # prompt (no cross-agent prompt-injection surface); the agent reads what
1322
+ # landed from git itself.
1323
+ REINTEGRATE_PROMPT = (
1324
+ "# The base moved while you were asleep\n"
1325
+ "`origin/{base_branch}` advanced since your last run and is fetched into "
1326
+ "your workspace. What landed:\n{digest}\n"
1327
+ "Merge it into your line and resolve any conflicts honestly, then decide "
1328
+ "what to re-run given what landed — if a sibling took your direction "
1329
+ "further, pivot or say so plainly rather than pushing on. Your change is "
1330
+ "measured against the current base.\n\n"
1331
+ )
1332
+
1333
+
1334
+ def _reintegration_digest(ws: Workspace, base_sha: str, fresh_head: str) -> str:
1335
+ """A short 'what landed' list: the subjects of the commits merged into the
1336
+ base since this line's base, newest first, capped. These are MERGED commits
1337
+ — vetted by the human-merge gate — so they are context, not untrusted input;
1338
+ the agent also has them in git to read in full."""
1339
+ try:
1340
+ out = ws.git("log", "--no-merges", "--format=%s", f"{base_sha}..{fresh_head}")
1341
+ except Exception:
1342
+ return " (recent changes on the base; see `git log`)"
1343
+ subjects = [ln.strip() for ln in out.splitlines() if ln.strip()][:12]
1344
+ return "\n".join(f" - {s}" for s in subjects) or " (a merge on the base; see `git log`)"
1345
+
1346
+
1347
+ def _line_base_advanced(ws: Workspace, base_branch: str, base_sha: str) -> str:
1348
+ """Fetch `origin/<base_branch>` and return its head when it has advanced
1349
+ past the line's pinned base, else "". The fetch doubles as making the
1350
+ fresh base available for the agent to merge and refreshes the origin refs
1351
+ the scope/measure path pairs against. Best-effort: any git failure returns
1352
+ "" and the wake proceeds exactly as today."""
1353
+ try:
1354
+ ws.fetch_origin()
1355
+ new = ws.git("rev-parse", f"refs/remotes/origin/{base_branch}").strip()
1356
+ merge_base = ws.git("merge-base", new, base_sha).strip()
1357
+ return new if new and merge_base != new else ""
1358
+ except Exception as exc:
1359
+ log.warning("base-moved check failed (%s); wake proceeds unchanged", type(exc).__name__)
1360
+ return ""
1361
+
1362
+
1299
1363
  def _push_line_snapshot(
1300
1364
  ws: Workspace,
1301
1365
  line_ref: str,
@@ -3447,13 +3511,7 @@ def main() -> int:
3447
3511
  default=120.0,
3448
3512
  help="how long before the walltime the self-deadline fires (floor 60)",
3449
3513
  )
3450
- parser.add_argument("--pat-file", default=str(CONFIG_DIR / "bot_pat"))
3451
- parser.add_argument(
3452
- "--github-app-file",
3453
- default=os.environ.get("OUTERLOOP_GITHUB_APP_FILE", ""),
3454
- help="GitHub App config (JSON: app_id, installation_id, private_key); "
3455
- "when set, installation tokens replace the PAT",
3456
- )
3514
+ add_credential_args(parser)
3457
3515
  parser.add_argument(
3458
3516
  "--key-file",
3459
3517
  default="",
@@ -499,6 +499,55 @@ def start(args: argparse.Namespace) -> int:
499
499
  return 0
500
500
 
501
501
 
502
+ DIST = "outerloop-science" # the PyPI distribution; imported as `outerloop`
503
+
504
+
505
+ def _installed_version(python: str) -> str:
506
+ """The installed version of the distribution, read from a fresh interpreter
507
+ so it reflects what pip just wrote rather than this process's imported copy."""
508
+ proc = subprocess.run(
509
+ [python, "-c", f"import importlib.metadata as m; print(m.version({DIST!r}))"],
510
+ capture_output=True,
511
+ text=True,
512
+ check=False,
513
+ )
514
+ return proc.stdout.strip() or "unknown"
515
+
516
+
517
+ def upgrade(args: argparse.Namespace) -> int:
518
+ """Upgrade the installed package in place, the local adopter's one verb.
519
+
520
+ On Slurm the resident tick self-updates through OUTERLOOP_AUTO_UPDATE; a local
521
+ install has no such loop, so this is the equivalent: `pip install --upgrade`,
522
+ then you restart the loop to pick the new code up. Pip already picks the newest
523
+ release and only falls back to a pre-release when that is all that is published;
524
+ --pre forces pre-releases even once a stable exists."""
525
+ cmd = [sys.executable, "-m", "pip", "install", "--upgrade", DIST]
526
+ if args.pre:
527
+ cmd.append("--pre")
528
+ if args.dry_run:
529
+ print(shlex.join(cmd))
530
+ return 0
531
+ before = _installed_version(sys.executable)
532
+ proc = subprocess.run(cmd, check=False)
533
+ if proc.returncode != 0:
534
+ print(
535
+ f"upgrade failed (pip exited {proc.returncode}). If this environment has no "
536
+ f"pip, upgrade through its installer instead, e.g. `uv pip install --upgrade {DIST}`.",
537
+ file=sys.stderr,
538
+ )
539
+ return proc.returncode
540
+ after = _installed_version(sys.executable)
541
+ if before == after:
542
+ print(f"already up to date: outerloop {after}.")
543
+ else:
544
+ print(
545
+ f"upgraded outerloop {before} -> {after}. Restart the loop to pick it up: "
546
+ f"stop the running tick, then `outerloop start`."
547
+ )
548
+ return 0
549
+
550
+
502
551
  def main(argv: list[str] | None = None) -> int:
503
552
  from outerloop import __version__
504
553
 
@@ -539,6 +588,17 @@ def main(argv: list[str] | None = None) -> int:
539
588
  help="guided setup: write ~/.config/outerloop/.env and the PAT file",
540
589
  add_help=False,
541
590
  )
591
+ up = sub.add_parser(
592
+ "upgrade",
593
+ help="upgrade the installed package, then restart the loop to pick it up",
594
+ description="Upgrade outerloop-science in this environment with pip. On Slurm the "
595
+ "resident tick self-updates through OUTERLOOP_AUTO_UPDATE; this is the equivalent "
596
+ "for a local install. Restart the loop afterwards to run the new code.",
597
+ )
598
+ up.add_argument(
599
+ "--pre", action="store_true", help="include pre-releases even once a stable exists"
600
+ )
601
+ up.add_argument("--dry-run", action="store_true", help="print the command and exit")
542
602
  argv = sys.argv[1:] if argv is None else list(argv)
543
603
  if argv[:1] == ["tick"]:
544
604
  # the tick entry owns its own parser; hand it the rest untouched
@@ -551,7 +611,10 @@ def main(argv: list[str] | None = None) -> int:
551
611
  from outerloop import init
552
612
 
553
613
  return init.main(argv[1:])
554
- return start(parser.parse_args(argv))
614
+ args = parser.parse_args(argv)
615
+ if args.command == "upgrade":
616
+ return upgrade(args)
617
+ return start(args)
555
618
 
556
619
 
557
620
  if __name__ == "__main__":