outerloop-science 0.2.0rc3__tar.gz → 0.2.0rc4__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.
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/CHANGELOG.md +3 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/CITATION.cff +1 -1
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/PKG-INFO +1 -1
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/install.md +14 -4
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/pyproject.toml +5 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/README.md +1 -1
- outerloop_science-0.2.0rc4/scripts/install_claude.sh +82 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/tick_deploy.sh +22 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/__init__.py +1 -1
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/harness.py +1 -1
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/init.py +48 -1
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/measure.py +42 -2
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/conftest.py +8 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_init.py +3 -2
- outerloop_science-0.2.0rc4/tests/test_install_harness.py +218 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_measure.py +100 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_start.py +1 -3
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/.gitignore +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/.pre-commit-config.yaml +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/.python-version +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/CLAUDE.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/CONTRIBUTING.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/LICENSE +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/NOTICE +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/README.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/RELEASING.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/SECURITY.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/containers/README.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/containers/agent-py312.def +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/assets/icon-dark.svg +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/assets/icon-light.svg +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/assets/icon.svg +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/community.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/compute.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/contract.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/agent-protocols.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/agent-substrate.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/architecture.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/base-reintegration.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/consolidation.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/dispatcher.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/eval-cache.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/external.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/github-app-auth.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/headline.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/judge-placement.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/lifecycle.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/meta.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/onboarding.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/orchestrator-verify.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/public-surface.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/research-lines.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/research-loop-buildout.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/research-loop.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/resident-tick.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/review-placement.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/reviewer-infra.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/role-cli.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/roles.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/scaling.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/design/session-watcher.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/reviewer.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/roadmap.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/docs/validation/author-syscalls.md +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/examples/review.yml +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/install_codex.sh +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/install_hermes.sh +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/requeue_moved_successors.sh +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/setup_branch_protection.sh +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/sweep_git_locks.sh +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/tick_chain.sbatch +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/scripts/tick_resident.sh +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/__main__.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/appauth.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/appmanifest.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/attempt.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/brief.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/climbboard.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/compute.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/contract.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/contract_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/disk.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/dispatch.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/evalcache.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/github.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/housekeeping.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/hypothesis.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/image.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/inbox.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/intake.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/launchlog.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/limits.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/maintain.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/maintain_agent_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/maintain_post_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/markers.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/orchestrator.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/panel.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/paths.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/posting.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/progress.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/py.typed +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/review.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/review_agent.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/review_agent_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/review_post_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/review_summarize_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/role_runner.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/roles.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/rolespec.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/runstate.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/steward.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/style.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/syscall.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/syscall_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/tick.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/verifier.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/verify_agent.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/verify_agent_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/verify_post_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/src/outerloop/watcher.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/fakes.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/helpers.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_app_permissions.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_appauth.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_appmanifest.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_attempt.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_attempt_review.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_bot_aliases.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_bot_login.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_brief.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_channel_dir.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_climbboard.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_codex_harness.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_compute.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_contract.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_contract_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_contract_names.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_disk.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_dispatch.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_evalcache.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_github.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_hardening.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_harness.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_hermes_harness.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_housekeeping.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_image.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_import.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_inbox.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_intake.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_launchlog.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_lifecycle_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_limits.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_local_compute.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_maintain.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_markers.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_measure_and_decide.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_messages.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_orchestrator.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_packaging.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_panel.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_paths.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_posting.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_progress.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_requeue_moved_successors.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_review.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_review_agent.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_review_agent_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_review_hardening.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_review_policy.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_review_summarize.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_role_runner.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_rolespec.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_runstate.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_steward.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_sweep_git_locks.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_syscall.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_syscall_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_tick.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_tick_chain_successors.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_tick_resident.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_tiers.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_verifier.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_verify_agent.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_verify_agent_cli.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_version.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/tests/test_watcher.py +0 -0
- {outerloop_science-0.2.0rc3 → outerloop_science-0.2.0rc4}/uv.lock +0 -0
|
@@ -22,6 +22,8 @@ tasks to finish.
|
|
|
22
22
|
|
|
23
23
|
### Added
|
|
24
24
|
|
|
25
|
+
- `outerloop init` installs a missing author CLI and records its path; `--no-install-harness` opts out. Claude has a pinned, SHA256-verified installer.
|
|
26
|
+
|
|
25
27
|
- `OUTERLOOP_TICK_HOST=login` (or `--tick-host login`) runs a niced foreground tick loop against Slurm, with one tick lease per state root.
|
|
26
28
|
- `OUTERLOOP_QOS` sets the QOS for every submitted Slurm job.
|
|
27
29
|
- `OUTERLOOP_APPTAINER_BIN` selects the container executable on compute nodes.
|
|
@@ -33,6 +35,7 @@ tasks to finish.
|
|
|
33
35
|
|
|
34
36
|
### Fixed
|
|
35
37
|
|
|
38
|
+
- The measurer waits up to 90 seconds for the files of a finished evaluation job. A filesystem delay no longer ends a measured improvement as a negative result.
|
|
36
39
|
- `outerloop start` refuses to run when the configured author's CLI is missing: it checks the recorded path, else PATH, else `~/.local/bin`, and names the install command and `outerloop init --force`.
|
|
37
40
|
|
|
38
41
|
- Ending line snapshots push once, with the kernel's GitHub auth. Without auth nothing is pushed and the log says so. A refused push is retried only when the remote line moved.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: outerloop-science
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.0rc4
|
|
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
|
|
@@ -295,17 +295,27 @@ says so — rerun `outerloop init --force` online, or set the login in the
|
|
|
295
295
|
|
|
296
296
|
### 2c. Run the loop
|
|
297
297
|
|
|
298
|
+
When the configured author's CLI is missing, `outerloop init` installs it from
|
|
299
|
+
a pinned release and records its path. An existing executable is kept.
|
|
300
|
+
Use `--no-install-harness` to skip this step. To install by hand from the
|
|
301
|
+
outerloop checkout, run `bash scripts/install_claude.sh [target_path]` or
|
|
302
|
+
`bash scripts/install_codex.sh [target_path]`, then rerun `outerloop init --force`.
|
|
303
|
+
The default target is `$OUTERLOOP_<BACKEND>_BIN`, else `~/.local/bin/<backend>`.
|
|
304
|
+
Claude 2.1.272 is pinned for Linux x64 (glibc/musl) and ARM64; other platforms
|
|
305
|
+
are refused. Installation needs `curl`, `sha256sum`, and a writable target
|
|
306
|
+
directory. Hermes remains a review backend, provisioned with
|
|
307
|
+
`bash scripts/install_hermes.sh [target_dir]`.
|
|
308
|
+
|
|
298
309
|
**Host prerequisites for model backends.** From the outerloop checkout:
|
|
299
310
|
|
|
300
|
-
- Claude, as author or reviewer: the Claude Code CLI
|
|
301
|
-
|
|
302
|
-
`curl -fsSL https://claude.ai/install.sh | bash`.
|
|
311
|
+
- Claude, as author or reviewer: the pinned Claude Code CLI; install with
|
|
312
|
+
`bash scripts/install_claude.sh`.
|
|
303
313
|
- Codex, as author or reviewer: the pinned Codex CLI; install with
|
|
304
314
|
`bash scripts/install_codex.sh`.
|
|
305
315
|
- Hermes, as reviewer only (not an author backend): the pinned hermes-agent
|
|
306
316
|
source checkout, run through `uv`; install with `bash scripts/install_hermes.sh`.
|
|
307
317
|
|
|
308
|
-
|
|
318
|
+
`init` records the absolute Claude
|
|
309
319
|
or Codex path found on PATH (or in `~/.local/bin`) as `OUTERLOOP_<BACKEND>_BIN`.
|
|
310
320
|
`start` checks only the configured author's CLI: the recorded path takes
|
|
311
321
|
precedence, otherwise it searches PATH, then `~/.local/bin`. A missing or non-executable CLI stops
|
|
@@ -59,6 +59,11 @@ path = "src/outerloop/__init__.py"
|
|
|
59
59
|
[tool.hatch.build.targets.wheel]
|
|
60
60
|
packages = ["src/outerloop"]
|
|
61
61
|
|
|
62
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
63
|
+
"scripts/install_claude.sh" = "outerloop/_installers/install_claude.sh"
|
|
64
|
+
"scripts/install_codex.sh" = "outerloop/_installers/install_codex.sh"
|
|
65
|
+
"scripts/install_hermes.sh" = "outerloop/_installers/install_hermes.sh"
|
|
66
|
+
|
|
62
67
|
[tool.ruff]
|
|
63
68
|
line-length = 100
|
|
64
69
|
target-version = "py312"
|
|
@@ -12,4 +12,4 @@ runs the local loop elsewhere, reading placement from `~/.config/outerloop/.env`
|
|
|
12
12
|
| `tick_resident.sh` | The resident loop: deploy → one tick under a timeout → sleep to the slot, one `afterany:self` successor, pause exits clean, shim changes resubmit the successor. |
|
|
13
13
|
| `requeue_moved_successors.sh` | Cancel pending same-name jobs that are no longer on the requested partition. The tick chain runs this before adding successors. |
|
|
14
14
|
| `sweep_git_locks.sh` | Remove stale `.git/*.lock` files from a checkout (older than N minutes, no live git process); the chain runs it before each deploy. |
|
|
15
|
-
| `install_codex.sh`, `install_hermes.sh` | Pin and install
|
|
15
|
+
| `install_claude.sh`, `install_codex.sh`, `install_hermes.sh` | Pin and install author/judge backends on the host. |
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Idempotent install of the harness-verified claude author binary.
|
|
3
|
+
#
|
|
4
|
+
# claude is a HOST prerequisite for the claude author backend (like uv, apptainer,
|
|
5
|
+
# and the .sif image) — deliberately NOT baked into the image, so it updates by
|
|
6
|
+
# swapping one host binary. Safe to run repeatedly: the fast path is a local
|
|
7
|
+
# `claude --version`, so it only touches the network on a version mismatch or a
|
|
8
|
+
# missing binary. Run at host setup, or best-effort from the tick chain.
|
|
9
|
+
#
|
|
10
|
+
# Usage: install_claude.sh [target_path]
|
|
11
|
+
# target_path where to place the binary
|
|
12
|
+
# (default: $OUTERLOOP_CLAUDE_BIN, else ~/.local/bin/claude)
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
# Pinned release and per-platform SHA256. To bump WANT, hash the new binaries
|
|
16
|
+
# and update the pins together; downloaded bytes are verified before execution.
|
|
17
|
+
WANT="2.1.272"
|
|
18
|
+
TARGET="${1:-${OUTERLOOP_CLAUDE_BIN:-$HOME/.local/bin/claude}}"
|
|
19
|
+
|
|
20
|
+
have="$("$TARGET" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
|
|
21
|
+
if [ "$have" = "$WANT" ]; then
|
|
22
|
+
echo "install_claude: claude $WANT already at $TARGET"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
platform="$(uname -s)-$(uname -m)"
|
|
27
|
+
case "$platform" in
|
|
28
|
+
Linux-x86_64)
|
|
29
|
+
platform="linux-x64"
|
|
30
|
+
WANT_SHA256="d81396a668eb76fbddb49a2a5841f1b5d7af96b4c1f6500ced92f2c988f5bcd4"
|
|
31
|
+
if compgen -G '/lib/libc.musl-*' > /dev/null; then
|
|
32
|
+
platform="linux-x64-musl"
|
|
33
|
+
WANT_SHA256="e30bb3ac07c4f1c3f63b47312e9a73ba6875256b7768c4cdb784d2b179417489"
|
|
34
|
+
fi
|
|
35
|
+
;;
|
|
36
|
+
Linux-aarch64|Linux-arm64)
|
|
37
|
+
platform="linux-arm64"
|
|
38
|
+
WANT_SHA256="214a90efdd16ee0ea81132ffecced588dba394d178cc494f285ba04b5288c8de"
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
echo "install_claude: unsupported platform $platform (no sha256 pin)" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
for tool in curl sha256sum; do
|
|
46
|
+
command -v "$tool" >/dev/null || {
|
|
47
|
+
echo "install_claude: required tool $tool is missing" >&2
|
|
48
|
+
exit 1
|
|
49
|
+
}
|
|
50
|
+
done
|
|
51
|
+
echo "install_claude: installing claude $WANT -> $TARGET (have '${have:-none}')"
|
|
52
|
+
url="https://downloads.claude.ai/claude-code-releases/$WANT/$platform/claude"
|
|
53
|
+
tmp="$(mktemp -d)"
|
|
54
|
+
staged="$TARGET.tmp.$$"
|
|
55
|
+
# clean BOTH the work dir and any staged binary on every exit, so a failed
|
|
56
|
+
# download/mv never leaves a stale temp next to the target
|
|
57
|
+
trap 'rm -rf "$tmp" "$staged"' EXIT
|
|
58
|
+
# bounded so a stalled download can't eat the tick's own walltime: this runs
|
|
59
|
+
# INSIDE the ~15-min tick job, so cap the fetch well under it (single attempt —
|
|
60
|
+
# a transient failure is retried by the next tick, since the install is idempotent)
|
|
61
|
+
curl -fsSL --connect-timeout 15 --max-time 120 --retry 0 "$url" -o "$tmp/claude"
|
|
62
|
+
# INTEGRITY GATE: verify the binary against the pin BEFORE installing or
|
|
63
|
+
# executing anything from it.
|
|
64
|
+
got_sha=$(sha256sum "$tmp/claude" | cut -d' ' -f1)
|
|
65
|
+
if [ "$got_sha" != "$WANT_SHA256" ]; then
|
|
66
|
+
echo "install_claude: sha256 mismatch (got $got_sha, want $WANT_SHA256) — refusing" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
# the target dir must exist BEFORE staging into it (the staged temp lives beside
|
|
70
|
+
# TARGET so the final mv is atomic on the same filesystem)
|
|
71
|
+
mkdir -p "$(dirname "$TARGET")"
|
|
72
|
+
install -m 0755 "$tmp/claude" "$staged"
|
|
73
|
+
# secondary sanity (integrity is already the sha256 gate above, so this runs
|
|
74
|
+
# VERIFIED bytes): the staged binary reports the pinned version before the mv
|
|
75
|
+
got="$("$staged" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
|
|
76
|
+
if [ "$got" != "$WANT" ]; then
|
|
77
|
+
echo "install_claude: downloaded claude reports '$got', wanted '$WANT' — not installing" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
# atomic replace on the same filesystem: never leave a half-written binary
|
|
81
|
+
mv -f "$staged" "$TARGET"
|
|
82
|
+
echo "install_claude: installed claude $got at $TARGET"
|
|
@@ -213,6 +213,28 @@ if [ -n "$ENV_TRUSTED" ]; then
|
|
|
213
213
|
done
|
|
214
214
|
fi
|
|
215
215
|
|
|
216
|
+
# The configured author's CLI is a host prerequisite (pinned installers under
|
|
217
|
+
# scripts/); provision it when missing. Best-effort: a failure here must never
|
|
218
|
+
# break the chain — start's preflight and the climb report a missing CLI.
|
|
219
|
+
case "${OUTERLOOP_AUTHOR_BACKEND:-claude}" in
|
|
220
|
+
claude)
|
|
221
|
+
# a recorded path is the one the jobs will use, so it alone decides;
|
|
222
|
+
# otherwise an absolute PATH hit or the default install location counts
|
|
223
|
+
# (jobs ignore relative PATH entries, so the check does too)
|
|
224
|
+
if [ -n "${OUTERLOOP_CLAUDE_BIN:-}" ]; then
|
|
225
|
+
target="$OUTERLOOP_CLAUDE_BIN"; found=""
|
|
226
|
+
[ -x "$target" ] && found="$target"
|
|
227
|
+
else
|
|
228
|
+
target="$HOME/.local/bin/claude"
|
|
229
|
+
found="$(command -v claude 2>/dev/null || true)"
|
|
230
|
+
case "$found" in /*) ;; *) found="" ;; esac
|
|
231
|
+
[ -z "$found" ] && [ -x "$target" ] && found="$target"
|
|
232
|
+
fi
|
|
233
|
+
if [ -z "$found" ]; then
|
|
234
|
+
bash "$OUTERLOOP_HOME/scripts/install_claude.sh" "$target" || echo "deploy: claude install failed"
|
|
235
|
+
fi
|
|
236
|
+
;;
|
|
237
|
+
esac
|
|
216
238
|
# The codex author binary is a host prerequisite; install it (idempotent, fast
|
|
217
239
|
# path is a local version check) when ANY codex role is deployed — the fleet
|
|
218
240
|
# author, or a codex panel lens (a claude-author/codex-panel rollout still
|
|
@@ -210,7 +210,7 @@ def redact(text: str, secrets: tuple[str, ...]) -> str:
|
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
HARNESS_INSTALL = {
|
|
213
|
-
"claude": "
|
|
213
|
+
"claude": "bash scripts/install_claude.sh",
|
|
214
214
|
"codex": "bash scripts/install_codex.sh",
|
|
215
215
|
"hermes": "bash scripts/install_hermes.sh",
|
|
216
216
|
}
|
|
@@ -21,6 +21,8 @@ import getpass
|
|
|
21
21
|
import json
|
|
22
22
|
import logging
|
|
23
23
|
import os
|
|
24
|
+
import shlex
|
|
25
|
+
import subprocess
|
|
24
26
|
import sys
|
|
25
27
|
import time
|
|
26
28
|
import urllib.error
|
|
@@ -108,11 +110,16 @@ def author_bin_env(backend: str) -> str:
|
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
def locate_harness(backend: str) -> str:
|
|
111
|
-
"""The absolute path of `backend`'s CLI
|
|
113
|
+
"""The absolute path of `backend`'s CLI: configured environment path, then PATH, then
|
|
112
114
|
~/.local/bin (where the native installers put it and where a Slurm job,
|
|
113
115
|
with no login PATH, would not find it); "" when absent. Recorded in .env so
|
|
114
116
|
every job spawns the same binary the operator installed."""
|
|
115
117
|
name = backend or AUTHOR_BACKENDS[0]
|
|
118
|
+
recorded = os.environ.get(author_bin_env(name), "")
|
|
119
|
+
if recorded:
|
|
120
|
+
# an operator's explicit path is kept when it works and reported when it does not
|
|
121
|
+
path = Path(recorded).expanduser().absolute()
|
|
122
|
+
return str(path) if path.is_file() and os.access(path, os.X_OK) else ""
|
|
116
123
|
# the harness's own lookup, minus any path already recorded: absolute PATH
|
|
117
124
|
# entries only (never the working directory), then ~/.local/bin
|
|
118
125
|
env = {k: v for k, v in os.environ.items() if k != f"OUTERLOOP_{name.upper()}_BIN"}
|
|
@@ -120,6 +127,34 @@ def locate_harness(backend: str) -> str:
|
|
|
120
127
|
return found if os.path.isabs(found) else ""
|
|
121
128
|
|
|
122
129
|
|
|
130
|
+
def cli_install_wanted(author_bin: str, args: argparse.Namespace) -> bool:
|
|
131
|
+
"""Install the author's CLI only in the full setup, when none was found and
|
|
132
|
+
the operator did not opt out; the focused --github-app run sets up the App."""
|
|
133
|
+
return not author_bin and not args.no_install_harness and not args.github_app
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def install_harness(backend: str) -> str:
|
|
137
|
+
"""Install a missing author CLI and return its executable host path."""
|
|
138
|
+
name = backend or AUTHOR_BACKENDS[0]
|
|
139
|
+
target = Path(os.environ.get(author_bin_env(name)) or Path.home() / ".local/bin" / name)
|
|
140
|
+
target = target.expanduser().absolute()
|
|
141
|
+
command = shlex.split(HARNESS_INSTALL[name])
|
|
142
|
+
script = Path(__file__).resolve().parents[2] / command[1]
|
|
143
|
+
if not script.is_file():
|
|
144
|
+
script = Path(__file__).resolve().parent / "_installers" / Path(command[1]).name
|
|
145
|
+
argv = [command[0], str(script), str(target)]
|
|
146
|
+
try:
|
|
147
|
+
subprocess.run(argv, check=True)
|
|
148
|
+
if not target.is_file() or not os.access(target, os.X_OK):
|
|
149
|
+
raise OSError(f"installer did not produce an executable at {target}")
|
|
150
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"{name} installation failed: {exc}. Run manually: {shlex.join(argv)}"
|
|
153
|
+
) from exc
|
|
154
|
+
print(f"installed {name} at {target}")
|
|
155
|
+
return str(target)
|
|
156
|
+
|
|
157
|
+
|
|
123
158
|
def _harness_hint(answers: InitAnswers) -> None:
|
|
124
159
|
if not answers.author_bin:
|
|
125
160
|
name = answers.author_backend or AUTHOR_BACKENDS[0]
|
|
@@ -643,6 +678,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
643
678
|
dest="author_backend",
|
|
644
679
|
help=f"climbing author's backend ({' or '.join(AUTHOR_BACKENDS)}; default claude)",
|
|
645
680
|
)
|
|
681
|
+
parser.add_argument(
|
|
682
|
+
"--no-install-harness",
|
|
683
|
+
action="store_true",
|
|
684
|
+
help="skip installing a missing author CLI",
|
|
685
|
+
)
|
|
646
686
|
parser.add_argument("--author-model", dest="author_model", help="climbing author's model")
|
|
647
687
|
image_flags = parser.add_mutually_exclusive_group() # one or the other, never both
|
|
648
688
|
image_flags.add_argument(
|
|
@@ -727,6 +767,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
727
767
|
answers.author_key_file = str(key_path)
|
|
728
768
|
print(f"wrote {key_path} (0600)")
|
|
729
769
|
|
|
770
|
+
if cli_install_wanted(answers.author_bin, args):
|
|
771
|
+
try:
|
|
772
|
+
answers.author_bin = install_harness(answers.author_backend)
|
|
773
|
+
except ValueError as exc:
|
|
774
|
+
print(f"outerloop init: {exc}", file=sys.stderr)
|
|
775
|
+
return 1
|
|
776
|
+
|
|
730
777
|
# The image, only now: every check that could still end the run has passed
|
|
731
778
|
# and the overwrite question is answered, so a download is never wasted.
|
|
732
779
|
# An existing image is used, else the published one is fetched on a machine
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import hashlib
|
|
21
21
|
import json
|
|
22
22
|
import logging
|
|
23
|
+
import time
|
|
23
24
|
from dataclasses import dataclass
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import Any
|
|
@@ -148,6 +149,12 @@ def _baseline_cache_path(cache_dir: Path, benchmark: str, base_sha: str) -> Path
|
|
|
148
149
|
return cache_dir / f"{benchmark}@{base_sha}.json"
|
|
149
150
|
|
|
150
151
|
|
|
152
|
+
# how long a finished job's files may lag the scheduler's terminal state on a
|
|
153
|
+
# shared filesystem before the measurer calls it "no result"
|
|
154
|
+
RESULT_SETTLE_S = 90.0
|
|
155
|
+
RESULT_POLL_S = 3.0
|
|
156
|
+
|
|
157
|
+
|
|
151
158
|
def _no_result_note(job_id: str, state: str) -> str:
|
|
152
159
|
"""The note for a job that ended with no result. A TIMEOUT is the walltime
|
|
153
160
|
the submit declared (or the contract's default), and the author must
|
|
@@ -332,6 +339,32 @@ class DispatchedMeasurer:
|
|
|
332
339
|
def _done(self, m: Measure) -> bool:
|
|
333
340
|
return (self._ev(m) / "exit-code").exists()
|
|
334
341
|
|
|
342
|
+
def _readable(self, m: Measure) -> bool:
|
|
343
|
+
"""The result can be read: a nonzero exit-code is final on its own (the
|
|
344
|
+
job writer records a failed checkout as exit 97 with no stdout); a
|
|
345
|
+
clean exit also needs its stdout, which can arrive later on a lagging
|
|
346
|
+
filesystem."""
|
|
347
|
+
ev = self._ev(m)
|
|
348
|
+
code, out = ev / "exit-code", ev / "stdout"
|
|
349
|
+
if not code.exists():
|
|
350
|
+
return False
|
|
351
|
+
if code.read_text().strip() != "0":
|
|
352
|
+
return True
|
|
353
|
+
return out.exists() and out.stat().st_size > 0
|
|
354
|
+
|
|
355
|
+
def _settled(self, m: Measure) -> bool:
|
|
356
|
+
"""Wait a little for a finished job's files. A shared filesystem can
|
|
357
|
+
show the scheduler's terminal state before the job's exit-code and
|
|
358
|
+
stdout reach the node reading them (Empire AI Alpha, 2026-09-15: a
|
|
359
|
+
measured improvement was read as "no result"). True once the result
|
|
360
|
+
is readable; False after RESULT_SETTLE_S."""
|
|
361
|
+
deadline = time.monotonic() + RESULT_SETTLE_S
|
|
362
|
+
while not self._readable(m):
|
|
363
|
+
if time.monotonic() >= deadline:
|
|
364
|
+
return False
|
|
365
|
+
time.sleep(RESULT_POLL_S)
|
|
366
|
+
return True
|
|
367
|
+
|
|
335
368
|
def _ended_without_result(self, m: Measure) -> str:
|
|
336
369
|
"""Why a dispatched job produced no result; a TIMEOUT means the eval
|
|
337
370
|
needs more walltime."""
|
|
@@ -405,7 +438,10 @@ class DispatchedMeasurer:
|
|
|
405
438
|
pending.append(live) # queued or running — the real job id
|
|
406
439
|
continue
|
|
407
440
|
if self._marker(m):
|
|
408
|
-
# was dispatched, not live, no result -> died before result
|
|
441
|
+
# was dispatched, not live, no result -> died before result,
|
|
442
|
+
# unless the files are merely late on the shared filesystem
|
|
443
|
+
if self._settled(m):
|
|
444
|
+
continue
|
|
409
445
|
raise EvalError(f"measure {m.name}: {self._ended_without_result(m)}")
|
|
410
446
|
# No marker, not live, no result -> never dispatched -> dispatch.
|
|
411
447
|
# RESIDUAL (bounded, accepted): if a prior process died in the
|
|
@@ -425,13 +461,17 @@ class DispatchedMeasurer:
|
|
|
425
461
|
if state and is_terminal(state):
|
|
426
462
|
# the job already ENDED without writing a result (a local
|
|
427
463
|
# timeout, an instant cluster failure): parking would wait on
|
|
428
|
-
# a job that will never deliver — fail like a vanished job
|
|
464
|
+
# a job that will never deliver — fail like a vanished job,
|
|
465
|
+
# once the filesystem has had its moment
|
|
466
|
+
if self._settled(m):
|
|
467
|
+
continue
|
|
429
468
|
raise EvalError(f"measure {m.name}: {_no_result_note(job_id, state)}")
|
|
430
469
|
pending.append(job_id)
|
|
431
470
|
if pending or blind:
|
|
432
471
|
raise MeasurementPending(tuple(pending))
|
|
433
472
|
out: dict[str, float] = {}
|
|
434
473
|
for m in measures:
|
|
474
|
+
self._settled(m) # exit-code seen, stdout possibly still on its way
|
|
435
475
|
try:
|
|
436
476
|
out[m.name] = read_eval_result(self.run_dir, self._slot(m), m.metric)
|
|
437
477
|
except EvalError as exc:
|
|
@@ -66,3 +66,11 @@ def _no_tick_lease_settle(monkeypatch):
|
|
|
66
66
|
"""The settle read-back after a fresh tick lease is a one-second sleep in
|
|
67
67
|
production; tests that want it pass settle_s explicitly."""
|
|
68
68
|
monkeypatch.setattr("outerloop.runstate.TICK_LEASE_SETTLE_S", 0.0)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture(autouse=True)
|
|
72
|
+
def _no_result_settle(monkeypatch):
|
|
73
|
+
"""The measurer waits up to RESULT_SETTLE_S for a finished job's files;
|
|
74
|
+
tests that want the wait set it explicitly."""
|
|
75
|
+
monkeypatch.setattr("outerloop.measure.RESULT_SETTLE_S", 0.0)
|
|
76
|
+
monkeypatch.setattr("outerloop.measure.RESULT_POLL_S", 0.0)
|
|
@@ -142,6 +142,7 @@ def test_main_yes_writes_config(tmp_path: Path, monkeypatch, capsys) -> None:
|
|
|
142
142
|
def _no_image_download(monkeypatch, request):
|
|
143
143
|
"""init never reaches the network in tests: no image is found or fetched, the
|
|
144
144
|
token's login is not looked up, and no harness binary is found on this machine."""
|
|
145
|
+
monkeypatch.setattr(init, "install_harness", lambda backend: "")
|
|
145
146
|
monkeypatch.setattr(init, "ensure_image", lambda **kw: "")
|
|
146
147
|
monkeypatch.setattr(init, "_token_login", lambda token: "")
|
|
147
148
|
if not request.node.get_closest_marker("real_locate_harness"):
|
|
@@ -694,9 +695,9 @@ def test_init_records_the_harness_binary_or_says_how_to_install_it(
|
|
|
694
695
|
assert init.main([*base, "--author-backend", "codex"]) == 0
|
|
695
696
|
assert "OUTERLOOP_CODEX_BIN=/opt/bin/codex" in (tmp_path / ".env").read_text()
|
|
696
697
|
monkeypatch.setattr(init, "locate_harness", lambda backend: "")
|
|
697
|
-
assert init.main([*base, "--force"]) == 0
|
|
698
|
+
assert init.main([*base, "--force", "--no-install-harness"]) == 0
|
|
698
699
|
out = capsys.readouterr().out
|
|
699
|
-
assert "no `claude` binary" in out and "
|
|
700
|
+
assert "no `claude` binary" in out and "bash scripts/install_claude.sh" in out
|
|
700
701
|
assert "OUTERLOOP_CLAUDE_BIN" not in (tmp_path / ".env").read_text()
|
|
701
702
|
|
|
702
703
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Pinned installation and init's missing-only provisioning."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from outerloop import init
|
|
12
|
+
from outerloop.harness import HARNESS_INSTALL
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_installer_mapping():
|
|
18
|
+
assert set(init.AUTHOR_BACKENDS) <= HARNESS_INSTALL.keys()
|
|
19
|
+
assert "hermes" in HARNESS_INSTALL
|
|
20
|
+
for command in HARNESS_INSTALL.values():
|
|
21
|
+
assert (ROOT / command.split()[1]).is_file()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.parametrize("backend", init.AUTHOR_BACKENDS)
|
|
25
|
+
@pytest.mark.parametrize(
|
|
26
|
+
"mode", ["missing", "present", "skip", "failure", "no_output", "permission"]
|
|
27
|
+
)
|
|
28
|
+
def test_init_install(tmp_path, monkeypatch, capsys, backend, mode):
|
|
29
|
+
monkeypatch.setattr(init, "CONFIG_DIR", tmp_path / "config")
|
|
30
|
+
monkeypatch.setattr(init, "ensure_image", lambda **kw: "")
|
|
31
|
+
target = tmp_path / "bin" / backend
|
|
32
|
+
monkeypatch.setenv(init.author_bin_env(backend), str(target))
|
|
33
|
+
monkeypatch.setattr(init, "locate_harness", lambda _: str(target) if mode == "present" else "")
|
|
34
|
+
calls = []
|
|
35
|
+
|
|
36
|
+
def run(argv, *, check):
|
|
37
|
+
calls.append(argv)
|
|
38
|
+
assert check
|
|
39
|
+
assert argv == ["bash", str(ROOT / f"scripts/install_{backend}.sh"), str(target)]
|
|
40
|
+
if mode == "permission":
|
|
41
|
+
raise PermissionError("read-only target")
|
|
42
|
+
if mode == "failure":
|
|
43
|
+
raise subprocess.CalledProcessError(1, argv)
|
|
44
|
+
if mode != "no_output":
|
|
45
|
+
target.parent.mkdir()
|
|
46
|
+
target.write_text("#!/bin/sh\n")
|
|
47
|
+
target.chmod(0o755)
|
|
48
|
+
|
|
49
|
+
monkeypatch.setattr(init.subprocess, "run", run)
|
|
50
|
+
args = ["--yes", "--compute", "local", "--target", "o/r", "--author-backend", backend]
|
|
51
|
+
if mode == "skip":
|
|
52
|
+
args.append("--no-install-harness")
|
|
53
|
+
rc = init.main(args)
|
|
54
|
+
if mode in ("failure", "no_output", "permission"):
|
|
55
|
+
assert rc == 1
|
|
56
|
+
assert "Run manually: bash " in capsys.readouterr().err
|
|
57
|
+
assert not (tmp_path / "config" / ".env").exists()
|
|
58
|
+
else:
|
|
59
|
+
assert rc == 0
|
|
60
|
+
env = (tmp_path / "config" / ".env").read_text()
|
|
61
|
+
assert (f"{init.author_bin_env(backend)}={target}" in env) == (mode != "skip")
|
|
62
|
+
assert len(calls) == (mode not in ("present", "skip"))
|
|
63
|
+
if mode == "missing":
|
|
64
|
+
monkeypatch.setattr(init, "locate_harness", lambda _: str(target))
|
|
65
|
+
assert init.main([*args, "--force"]) == 0
|
|
66
|
+
assert len(calls) == 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize("valid", [True, False])
|
|
70
|
+
def test_claude_checksum_gate(tmp_path, valid):
|
|
71
|
+
# Substitute only the test copy's pins, never the production verification
|
|
72
|
+
# or hash tool: a small executable stands in for the 227 MB release asset.
|
|
73
|
+
payload = b"#!/bin/sh\necho '2.1.272 (Claude Code)'\n"
|
|
74
|
+
asset = tmp_path / "asset"
|
|
75
|
+
asset.write_bytes(payload if valid else b"untrusted")
|
|
76
|
+
script = tmp_path / "installer.sh"
|
|
77
|
+
source = (ROOT / "scripts/install_claude.sh").read_text()
|
|
78
|
+
script.write_text(
|
|
79
|
+
re.sub(
|
|
80
|
+
r'WANT_SHA256="[0-9a-f]{64}"',
|
|
81
|
+
f'WANT_SHA256="{hashlib.sha256(payload).hexdigest()}"',
|
|
82
|
+
source,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
shim = tmp_path / "shim"
|
|
86
|
+
shim.mkdir()
|
|
87
|
+
for name, body in {
|
|
88
|
+
"curl": '#!/bin/bash\nwhile [ "$1" != "-o" ]; do shift; done\ncp "$ASSET" "$2"\n',
|
|
89
|
+
"uname": '#!/bin/sh\ncase "$1" in -s) echo Linux;; -m) echo x86_64;; esac\n',
|
|
90
|
+
}.items():
|
|
91
|
+
path = shim / name
|
|
92
|
+
path.write_text(body)
|
|
93
|
+
path.chmod(0o755)
|
|
94
|
+
target = tmp_path / "installed" / "claude"
|
|
95
|
+
env = {**os.environ, "PATH": f"{shim}:{os.environ['PATH']}", "ASSET": str(asset)}
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
["bash", str(script), str(target)], env=env, capture_output=True, text=True
|
|
98
|
+
)
|
|
99
|
+
assert (result.returncode == 0) == valid, result.stderr
|
|
100
|
+
assert target.exists() == valid
|
|
101
|
+
assert not list(tmp_path.rglob("*.tmp.*"))
|
|
102
|
+
if valid:
|
|
103
|
+
assert target.read_bytes() == payload
|
|
104
|
+
assert os.access(target, os.X_OK)
|
|
105
|
+
(shim / "curl").write_text("#!/bin/sh\nexit 99\n")
|
|
106
|
+
assert subprocess.run(["bash", str(script), str(target)], env=env).returncode == 0
|
|
107
|
+
else:
|
|
108
|
+
assert "sha256 mismatch" in result.stderr
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _claude_provisioning_block(deploy: str) -> str:
|
|
112
|
+
start = deploy.index("# The configured author's CLI is a host prerequisite")
|
|
113
|
+
end = deploy.index("\nesac\n", start) + len("\nesac\n") # the outer case, at column 0
|
|
114
|
+
return deploy[start:end]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize(
|
|
118
|
+
("author", "present", "on_path", "recorded", "relative_path", "expected"),
|
|
119
|
+
[
|
|
120
|
+
(
|
|
121
|
+
"claude",
|
|
122
|
+
False,
|
|
123
|
+
False,
|
|
124
|
+
"",
|
|
125
|
+
False,
|
|
126
|
+
"default",
|
|
127
|
+
), # missing everywhere: install to ~/.local/bin
|
|
128
|
+
("claude", True, False, "", False, ""), # already under ~/.local/bin
|
|
129
|
+
("claude", False, True, "", False, ""), # already on PATH (absolute entry)
|
|
130
|
+
(
|
|
131
|
+
"claude",
|
|
132
|
+
False,
|
|
133
|
+
True,
|
|
134
|
+
"",
|
|
135
|
+
True,
|
|
136
|
+
"default",
|
|
137
|
+
), # only through a relative PATH entry: jobs cannot use it
|
|
138
|
+
(
|
|
139
|
+
"claude",
|
|
140
|
+
False,
|
|
141
|
+
True,
|
|
142
|
+
"stale",
|
|
143
|
+
False,
|
|
144
|
+
"recorded",
|
|
145
|
+
), # recorded path gone: PATH does not excuse it
|
|
146
|
+
("claude", True, False, "present", False, ""), # recorded path present
|
|
147
|
+
("codex", False, False, "", False, ""), # not the configured author
|
|
148
|
+
],
|
|
149
|
+
)
|
|
150
|
+
def test_deploy_provisions_the_configured_author_cli(
|
|
151
|
+
tmp_path, author, present, on_path, recorded, relative_path, expected
|
|
152
|
+
):
|
|
153
|
+
"""The chain's deploy step runs the pinned claude installer only when the
|
|
154
|
+
configured author is claude and the CLI the jobs will use is missing: the
|
|
155
|
+
recorded path when there is one, else an absolute PATH hit or ~/.local/bin."""
|
|
156
|
+
home = tmp_path / "checkout"
|
|
157
|
+
(home / "scripts").mkdir(parents=True)
|
|
158
|
+
log = tmp_path / "calls"
|
|
159
|
+
(home / "scripts" / "install_claude.sh").write_text(f'#!/bin/bash\necho "claude $1" >> {log}\n')
|
|
160
|
+
userhome = tmp_path / "userhome"
|
|
161
|
+
(userhome / ".local" / "bin").mkdir(parents=True)
|
|
162
|
+
path_dir = tmp_path / "path"
|
|
163
|
+
path_dir.mkdir()
|
|
164
|
+
if present:
|
|
165
|
+
binary = userhome / ".local" / "bin" / "claude"
|
|
166
|
+
binary.write_text("#!/bin/sh\n")
|
|
167
|
+
binary.chmod(0o755)
|
|
168
|
+
if on_path:
|
|
169
|
+
binary = path_dir / "claude"
|
|
170
|
+
binary.write_text("#!/bin/sh\n")
|
|
171
|
+
binary.chmod(0o755)
|
|
172
|
+
env = {
|
|
173
|
+
"HOME": str(userhome),
|
|
174
|
+
"OUTERLOOP_HOME": str(home),
|
|
175
|
+
"OUTERLOOP_AUTHOR_BACKEND": author,
|
|
176
|
+
"PATH": f"{path_dir}:/usr/bin:/bin",
|
|
177
|
+
}
|
|
178
|
+
recorded_path = tmp_path / "recorded" / "claude"
|
|
179
|
+
if recorded == "present":
|
|
180
|
+
recorded_path.parent.mkdir()
|
|
181
|
+
recorded_path.write_text("#!/bin/sh\n")
|
|
182
|
+
recorded_path.chmod(0o755)
|
|
183
|
+
if recorded:
|
|
184
|
+
env["OUTERLOOP_CLAUDE_BIN"] = str(recorded_path)
|
|
185
|
+
if relative_path:
|
|
186
|
+
env["PATH"] = ".:/usr/bin:/bin"
|
|
187
|
+
block = _claude_provisioning_block((ROOT / "scripts/tick_deploy.sh").read_text())
|
|
188
|
+
subprocess.run(
|
|
189
|
+
["/bin/bash", "-c", block],
|
|
190
|
+
env=env,
|
|
191
|
+
check=True,
|
|
192
|
+
capture_output=True,
|
|
193
|
+
text=True,
|
|
194
|
+
cwd=path_dir,
|
|
195
|
+
)
|
|
196
|
+
calls = log.read_text().splitlines() if log.exists() else []
|
|
197
|
+
want = {
|
|
198
|
+
"": [],
|
|
199
|
+
"default": [f"claude {userhome / '.local' / 'bin' / 'claude'}"],
|
|
200
|
+
"recorded": [f"claude {recorded_path}"],
|
|
201
|
+
}[expected]
|
|
202
|
+
assert calls == want
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize(
|
|
206
|
+
("author_bin", "github_app", "no_install", "wanted"),
|
|
207
|
+
[
|
|
208
|
+
("", False, False, True),
|
|
209
|
+
("/usr/local/bin/claude", False, False, False),
|
|
210
|
+
("", True, False, False), # the focused App run sets up the App only
|
|
211
|
+
("", False, True, False),
|
|
212
|
+
],
|
|
213
|
+
)
|
|
214
|
+
def test_cli_install_wanted(author_bin, github_app, no_install, wanted):
|
|
215
|
+
import argparse
|
|
216
|
+
|
|
217
|
+
args = argparse.Namespace(github_app=github_app, no_install_harness=no_install)
|
|
218
|
+
assert init.cli_install_wanted(author_bin, args) is wanted
|