Read-only maintainer scan of main at 0b3400e (after #330), run 2026-09-07 by a subagent; no code was changed. Items marked "(decision)" wait for the maintainer; the rest are candidates for agent PRs in the groupings at the end. The two .env readers in section 2 and the tick_deploy.sh line numbers predate #331.
Healthy: ruff, ruff format, mypy and uv lock --check are all clean; 1322 tests pass in 60s under xdist; no bare except:; every installer verifies a sha256 or commit pin; the tick reads GitHub only, never writes, on its steady-state path.
Most worth doing (1): cut the tick's read amplification: list_runs re-reads every run record up to 13 times per tick, each in-review PR is fetched twice plus three comment lists, and the open-issue list is fetched up to five times by four services.
Most worth doing (2): the dependency and CI surface is stale in a mechanical way: actions/checkout v4 (latest v7), setup-uv v6 (v10), upload/download-artifact v4 (v7/v8), gitleaks 8.24.3 (8.30.1, pinned twice without a checksum), ruff 0.16.1 (0.16.6) with the pre-commit rev to match.
Most worth doing (3): the three big modules (attempt.py 3725, tick.py 3394, followup.py 2179 lines) hold 21 functions over 150 lines and 91 in-function imports that exist only to break the attempt/tick/followup import cycle; a small set of leaf modules (clone URL, seal, wake spec) would let them split.
Decisions needed from the maintainer: when to drop the AUTORESEARCH_/.autoresearch shims (all say "release after 0.1"), whether codex 0.130.0 to 0.153.x and hermes v2026.8.13 to v2026.9.7 get re-verified, and who owns seal-and-measure (attempt vs followup) and the operator .env reader (cli.py vs tick_deploy.sh).
Measurement basis: wc -l, an AST pass over src/outerloop, uv pip list --outdated, uv run ruff check --select ALL --statistics, uv run mypy --strict src, uv run pytest --durations=25, GitHub releases API for upstream pins. All paths are repo-relative. Effort S/M/L, risk low/med/high; "(decision)" marks items that need the maintainer's call before an agent PR.
1. Dead or unused pathways
Two top-level symbols have no references anywhere. containment_available (src/outerloop/image.py:193) and STEWARD_LABEL (src/outerloop/intake.py:34) are never read by src, tests, scripts or docs (AST scan of 904 top-level defs). S, low, mechanical.
Seven src symbols are referenced only from tests. compute.gpus_in_gres (compute.py:634), dispatch.result_summary (dispatch.py:598), dispatch.parse_result_json (dispatch.py:614), housekeeping.shed_candidates (housekeeping.py:41, not called by shed_ended_workspaces), tick.read_pending (tick.py:2229, only list_pendings is used), plus two test doubles that live in src: harness.FakeHarness (harness.py:1446) and tick.RecordingDispatcher (tick.py:150). Delete the first five or wire them in; move the two doubles to tests/. S, low, mechanical for the doubles; (decision) for gpus_in_gres/result_summary/parse_result_json, which look like half-finished features.
Legacy rename shims: eleven sites, one removal date. All say "dropped in the release after 0.1" and the version is 0.1.0.dev2, so none can go yet; listing them so the removal PR is one sweep:
src/outerloop/__init__.py:8-18 _bridge_legacy_env copies AUTORESEARCH_* to OUTERLOOP_* at import.
src/outerloop/cli.py:108-120 legacy spelling inside env_file_values; cli.py:193 AUTORESEARCH_HOME.
src/outerloop/cli.py:32 LEGACY_RESIDENT_JOB_NAME = "autoresearch-resident" (squeue lookups at 312, 416); cli.py:35 LEGACY_LOCAL_ROOT_NAME = ".autoresearch" (298).
src/outerloop/paths.py:16 CONFIG_DIR_NAMES = ("outerloop", "autoresearch").
scripts/tick_chain.sbatch:41-46 env bridge loop; scripts/tick_deploy.sh:92 legacy config dir and :113 twin lookup.
src/outerloop/syscall.py:55 CHANNEL_DIR_NAMES = (".outerloop", ".autoresearch") with hardening at 571-608 and 1098 that exists to guard the legacy name.
src/outerloop/contract.py:30 CONTRACT_NAMES = (".outerloop.yaml", ".autoresearch.yaml").
- Tests that go with them:
tests/test_env_bridge.py (11 refs), tests/test_contract_names.py, and 9 test files that still write .autoresearch.yaml fixtures (tests/test_attempt.py:637,1141,2295,...).
The contract-name and channel-dir shims guard files committed in target repos; the others guard operator config only, so they could be dropped on different schedules. M, med, (decision) on timing; the removal itself is mechanical.
Twelve OUTERLOOP_* knobs are read by src and named in no doc. OUTERLOOP_CADENCE_MIN, _CLAUDE_BIN, _CODEX_BIN, _HARNESS_KEY_FILE, _MAX_JOB_MINUTES, _MIN_TICK_MINUTES, _PANEL_CODEX_KEY_FILE, _PANEL_HERMES_KEY_FILE, _PANEL_KEY_FILE, _RESIDENT_MINUTES, _SETUP_URL, _STEWARD_KEY_FILE (grep of docs/ README.md CONTRIBUTING.md examples/). Conversely OUTERLOOP_TICK_TIMEOUT and OUTERLOOP_RESIDENT_{CADENCE,MARGIN,RETRY}_S exist only in scripts/tick_resident.sh:15-21. S, low, mechanical (docs only).
CLI flags with no test or doc coverage. --app-name, --org (init.py), --claude-bin (steward.py, followup.py), --deadline-margin-s, --work-order-b64 (steward.py), --run-id (followup.py), --issue (attempt.py, steward.py). The steward/followup ones are built by the tick's argv assembly, so they are live but untested; the two init flags are user-facing and undocumented. S, low, mechanical.
Markers declared but unused. slow, llm, slurm are declared in pyproject.toml and deselected in tests/conftest.py:40, but zero tests carry them; serial has one. Either keep as intended tiers or drop the config. S, low, (decision).
2. Duplicated logic that should have one owner
The operator .env reader is implemented twice. src/outerloop/cli.py:82-121 (env_file_values) and scripts/tick_deploy.sh:91-130 both implement: refuse unless owned and not group/world-writable, last assignment wins, strip one quote pair and a trailing CR, presence-based off-switch, legacy twin loses to the canonical key. tests/test_start.py:88-92 keeps the key lists identical but not the parsing rules. The shell copy exists because deploy must run even when uv sync failed (tick_deploy.sh:43-77). M, med, (decision): accept the duplication with a shared conformance test, or have the shell call a tiny stdlib-only Python that needs no venv.
Seal-and-measure has two owners. followup.py:1503 _commit_sealed_tree, :1521 _followup_measure, :1547 _seal_and_measure, :1595 _park_remeasure, :1673 _resume_measure (275 lines) re-implement the seal/snapshot/measure/park sequence that attempt.py:1298 _push_line_snapshot / :1317 _seal_and_push and orchestrator.py:795 measure_and_decide (252 lines) already perform, importing with_seed, LINE_MEMORY_PATHS, _target_clone_url lazily from attempt (followup.py:1563-1569). L, med, (decision) on the target module (a seal.py leaf both import).
Private helpers are imported across modules. _target_clone_url from attempt in followup.py:524,1711 and tick.py:1999 (belongs in github.py); _fence/_cap from brief in intake.py:16, followup.py:622, steward.py:383, syscall.py:46, panel.py:21 (belongs in style.py); _emit/_pull_request from review_agent in verify_agent.py:23, review_summarize_cli.py:27, verify_post_cli.py:23, review_post_cli.py:30; _stage_launch_job_ids from attempt in tick.py:1221; _metric_from_output from orchestrator in dispatch.py:46. Rename to public in the owning module. S, low, mechanical.
Argparse groups are copied across five entry points. --target, --run-root, --pat-file, --max-turns, --partition, --account, --image, --gpu-partition, --gpu-account, --claude-bin, --session-minutes are each defined in three to five of attempt.py, followup.py, steward.py, tick.py, init.py, cli.py; attempt.main alone is 390 lines (attempt.py:3332). Shared add_placement_args / add_identity_args / add_session_args helpers. M, low, mechanical.
The same GitHub list is fetched by four services. github.list_open_issues(target) is called by the alarm finder (tick.py:531), the research log twice (tick.py:1400,1419), intake (intake.py:72) and steward twice (steward.py:243,326), each with its own filtering loop. One per-tick issue snapshot passed down. M, low, mechanical (see section 6).
The backend version pins live in four places each. codex 0.130.0: scripts/install_codex.sh:18, .github/workflows/advisory-review-summarize.yml:115, review-agent.yml:76, advisory-review-agent.yml:191. hermes v2026.8.13 plus its commit sha: scripts/install_hermes.sh:19-20, advisory-review-summarize.yml:93, review-agent.yml:71, advisory-review-agent.yml:158. A single pins file both read would make a bump a one-line change. S, low, mechanical.
3. Oversized modules and functions
Ten largest files (wc -l, src total 28,223): attempt.py 3725, tick.py 3394, followup.py 2179, orchestrator.py 2011, github.py 1494, harness.py 1460, climbboard.py 1353, syscall.py 1190, steward.py 852, syscall_cli.py 758. Tests mirror it: test_attempt.py 5099, test_tick.py 4523, test_followup.py 3113.
Five longest functions (AST; 748 functions total, 37 over 100 lines, 21 over 150): followup._respond 817 lines (followup.py:415), orchestrator.attempt_once 712 (orchestrator.py:1140), attempt.resume_run 701 (attempt.py:1604), attempt.live_attempt 687 (attempt.py:2563), climbboard.render_html 526 (climbboard.py:323). Next: attempt.main 390, steward.live_steward 336, attempt._wake_author_sleep 290, tick.service_in_review 289, followup._resume_measure 275.
attempt.py split seams. 54 top-level defs and no section markers. Natural pieces: author sleep/wake (:752-1042), line snapshot and seal (:1298-1330), resume_run (:1604-2305), live_attempt (:2563-3250), argparse main (:3332-3722). L, med, (decision) on the package layout (attempt/{wake,seal,resume,cli}.py).
tick.py split seams. Eight service_* functions, wake-spec files (:987-1143), pending/tombstone markers (:2165-2282), env parsing _*_from_env (:3037-3260), main (:3262). L, med, (decision).
The import cycle drives 188 in-function imports (ruff PLC0415). 39 in tick.py, 27 in followup.py, 25 in attempt.py. attempt imports tick (attempt.py:587,2651); tick imports attempt and followup (tick.py:600,1221,1999,2312,2333); followup imports attempt eight times (followup.py:524,1249,1326,1563,1568,1711,2056,2129). Moving the shared leaves (clone URL, seal, wake spec, MAX_CLAIM_CHARS, LINE_MEMORY_PATHS) into small modules removes most of the cycle before any big split. M, med, mechanical once the leaf modules are named.
climbboard.render_html is an HTML template in Python. 205 of its 526 lines are markup, CSS and JS (climbboard.py:323-849). Move the template to package data and render with string.Template. M, low, mechanical.
4. Dependency and tooling upgrades
Python dependencies (uv pip list --outdated). ruff 0.16.1 to 0.16.6, mypy 2.3.0 to 2.3.1, pydantic 2.13.4 to 2.13.5 (pydantic-core 2.46.4 to 2.48.0), pre-commit 4.6.1 to 4.6.2, plus transitive (filelock, platformdirs, virtualenv, pygments). uv lock --upgrade then bump .pre-commit-config.yaml ruff-pre-commit rev: v0.16.1 in the same PR so hooks and CI agree. S, low, mechanical.
GitHub Actions are three major versions behind. actions/checkout@v4 (14 uses; latest v7.0.1), astral-sh/setup-uv@v6 (9; v10.0.1), actions/upload-artifact@v4 (4; v7.0.1), actions/download-artifact@v4 (5; v8.0.1). eWaterCycle/setup-apptainer@v2 and pypa/gh-action-pypi-publish@release/v1 are current. checkout v5+ requires node24 runners and download-artifact v5+ changed default extraction paths, so run the review workflows once after bumping. M, med, mechanical with a CI verification.
gitleaks is pinned twice at 8.24.3 without a checksum. .github/workflows/ci.yml:41 curls the release tarball straight into tar; .pre-commit-config.yaml pins rev: v8.24.3. Latest is v8.30.1. Bump both and add a sha256 check the way scripts/install_codex.sh:22-49 does. S, low, mechanical.
codex pinned at 0.130.0; upstream is rust-v0.153.4 (2026-09-04). scripts/install_codex.sh:16-18 explains the hold: 0.149.x needs the code-mode-host helper and bubblewrap on the author path. Any bump is a harness re-verification (tests/test_codex_harness.py) and a live canary. M, high, (decision).
hermes-agent pinned at v2026.8.13; upstream is v2026.9.7. scripts/install_hermes.sh:19-20 and HERMES_REF in three workflows; the panel lens is bench-only today. M, med, (decision).
Runner images are mixed. 11 jobs use ubuntu-latest, one uses ubuntu-24.04 (build-image.yml:24). Pin all to ubuntu-24.04 for reproducible review runs. S, low, mechanical.
ruff: four rule families cost nothing to enable. On src today: RET 0, PIE 0, PERF 3, N 7, ARG 16, TC/TCH 41 (auto-fixable typing-only imports), PTH 46. Larger: C90 48 (complexity), BLE 101 (blind except), TRY 288, PL 435, D 1192. Recommend adding RET, PIE, PERF, N, TC now; track PLC0415 (188) as the cycle-break metric. S, low, mechanical.
mypy can be tightened cheaply. --disallow-untyped-defs src reports 9 errors in 5 files; --strict src reports 59 in 18. Enable disallow_untyped_defs for src. S, low, mechanical.
Only Python 3.12 is exercised. requires-python >=3.12, .python-version 3.12, no matrix in ci.yml. CI is deliberately one job for billing (ci.yml:1-3), so a 3.13 run would be a second job. S, low, (decision).
Stale planning comment in pyproject. pyproject.toml:20-22 still says "Planned: github: PyGithub or gh-CLI wrappers" while github.py is 1494 lines of urllib. S, low, mechanical.
5. Test-suite health
Suite: 1322 passed in 59.7s (-n auto), 0 skipped tiers in use. Slowest 25 range 2.7s to 15.0s.
One test spends 15s in a real sleep. tests/test_syscall.py::test_sync_cli_waits_for_the_done_marker (15.01s serial and parallel) calls cmd_sync, which polls with a hard time.sleep(15) (src/outerloop/syscall_cli.py:508); the neighbouring test patches time.sleep but this one does not. Patch it or make the poll interval a parameter. Saves a quarter of the slowest worker's wall time. S, low, mechanical.
The 5-7s followup tests are xdist contention, not test cost. test_a_capped_out_or_degraded_reread_leaves_the_findings_to_a_human and test_followup_row_update_respects_the_cross_seed_floor take 6.9s/5.7s under -n auto but 2.5s each serially. No action beyond the fixture consolidation below.
Fixed sleeps around real subprocesses. tests/test_sweep_git_locks.py:89,116,165,186,208 (time.sleep(0.3) after a bash Popen), tests/test_harness.py:443, tests/test_syscall_cli.py:473, tests/test_watcher.py:198 (0.05s), tests/test_local_compute.py:238 ("let the SIGKILL land"). Replace with a bounded poll on the condition. S, low, mechanical.
Fakes are defined per file. FakePopen ten times (tests/test_codex_harness.py:43,131,166,193,246,275, tests/test_hermes_harness.py:43,105,219, tests/test_review_agent.py:222); FakeMeasurer three (test_attempt.py:2256, test_measure_and_decide.py:53, test_followup.py:2374); FakeGitHub two with disjoint method sets (test_attempt.py:735 3 methods, test_followup.py:58 7 methods); a _git(cwd, *args) helper four times (test_attempt.py:625, test_followup.py:47, test_steward.py:195, test_sweep_git_locks.py:21) and _repo twice. tests/conftest.py holds one fixture. Add tests/fakes.py and a git_repo fixture. M, low, mechanical.
Real subprocesses where a fake would do. 18 test files spawn subprocesses; most drive real git (the code under test is git plumbing) and are fine. tests/test_tick_resident.py runs the real bash chain with shimmed sbatch/uv (4.6s each, three tests) and is the only end-to-end lane; worth keeping but marking as such. S, low, (decision) whether to introduce an integration marker.
Test files track the module sizes. test_attempt.py 5099 lines will split along the same seams as attempt.py; do them together. L, low, mechanical after the module split.
6. Tick performance
list_runs re-scans every run record up to 13 times per tick. runstate.py:246-266 walks runs/* and JSON-parses each state.json. Per tick it is called from service_in_review (tick.py:610), sweep (1163), cancel_ended_launches (1224), service_research_log (1304), _sweep_implementing (1468), service_syncs (2003), service_self_initiated (2528), service_steward (2697), housekeeping.shed_candidates (housekeeping.py:45) and four board passes (climbboard.py:177,881,906,1016). With N runs that is up to 13N file reads; a fleet of 50 runs (the 2026-09-03 outage size, housekeeping.py:113) is 650 parses. Load one snapshot at tick start and pass it; the three sites that deliberately re-read a single record (tick.py:849,1525,1626) stay as they are. M, med, mechanical.
Each in-review PR costs five GitHub calls per tick, one of them a duplicate. close_if_done fetches the PR (followup.py:302), then service_in_review fetches the same PR again (tick.py:632), then has_new_comments lists issue comments, reviews and review comments (followup.py:334-341). Return the PR from close_if_done to drop one call per record. S, low, mechanical.
The open-issue list is fetched up to five times per tick. Alarm (tick.py:531, up to 10 pages), research log (tick.py:1400 per publishable record, 1419 on cache miss), intake (intake.py:72), steward (steward.py:243,326). One snapshot per tick. M, low, mechanical.
The board reads every data file every tick. service_climb_board does get_file per benchmark for curves (climbboard.py:1181) and data (:1278), then get_file_content per view (:1343) and branch_head (:1264); service_status reads and may write STATUS_PATH (:1079). A local hash of the last published content would skip the reads when nothing changed. M, low, mechanical.
No conditional requests. github.py has no ETag / If-None-Match handling (grep). Adding it to the GET path (github.py:196-215) would make the repeated list calls above free against the rate limit even before the snapshots land. M, med, (decision).
Filesystem reads per tick are otherwise bounded. Heartbeat and check_disk run twice (tick.py:1770-1800), tombstones and pendings are glob+parse per target (tick.py:2195,2248), and the ledger issue cache is one small file (tick.py:1278). Nothing else re-lists per record.
7. Comment and docs hygiene
Dated provenance comments in src: 16 lines; PR-number breadcrumbs: 55. Dates: attempt.py:552,1333,1480, tick.py:741,1086,1709, followup.py:1628, github.py:935, housekeeping.py:5,113, limits.py:24, steward.py:121, syscall.py:1086, contract.py:107, disk.py:7. "(#NNN)" and "terra #170 r5" style: followup.py 21, tick.py 20, the rest scattered. Trim to intent on touch, never a mass rewrite. S, low, mechanical (on touch).
CHANGELOG [Unreleased] has 56 sub-headings. CHANGELOG.md is 1810 lines with a single ## [Unreleased] (line 7) and 19 ### Added, 20 ### Changed, 12 ### Fixed, 4 ### Removed, 1 ### Removed (enablement scaffolding) for 219 bullets. RELEASING.md:11-14 says dev pre-releases leave Unreleased in place, so the single section is by design; the sub-headings should be merged into one of each. Do it with exact-anchor edits, not a spanning regex. M, low, mechanical.
Roadmap items that shipped are still unchecked. docs/roadmap.md:80 session watcher (src/outerloop/watcher.py, docs/design/session-watcher.md), :101 resident tick (scripts/tick_resident.sh, cli.py RESIDENT_JOB_NAME), :291 eval seed cache (src/outerloop/evalcache.py, PR #330 merged), :283 progress webpage (partly: climbboard.py and web/index.html). S, low, mechanical after the maintainer confirms each is "done" rather than "partly".
Legacy-name wording disagrees. docs/install.md:249 says the old names are accepted "for one release"; paths.py:15 and cli.py:35 say "the release after 0.1". Pick one. S, low, mechanical.
Design note status lines are stale. docs/design/consolidation.md:3 still reads "Status: proposal (2026-08-12)" after several of its stages landed. S, low, (decision) on what to mark done.
Undocumented knobs and init flags (section 1) belong in docs/install.md or docs/compute.md. S, low, mechanical.
8. Small correctness smells
The contract is fetched from a hardcoded main. tick.py:1884-1888 calls _contract_text(github, target, "main") and reports "contract file missing on main", while service_eval_cache (tick.py:1986) and github.py:789 use github.default_branch(target). A target whose default branch is not main loses its launch lanes every tick. S, low, mechanical.
A scope gate fails open on a normalisation error. orchestrator.py:573-578: paths that raise inside _fold(normalize_path(path)) are skipped with except Exception: continue, so a path the normaliser cannot handle is never compared against the shared-scope list. Log and treat as a hit, or narrow the except. S, med, (decision).
Silent except Exception returns. attempt.py:224 (rev-parse HEAD failure returns ""), tick.py:891 (_last_worked_ts: a corrupt work marker silently resets tick coalescing), init.py:359 (network error indistinguishable from "not an org"). ruff BLE001 counts 101 blind excepts in src overall; these three have no log line. S, low, mechanical (add a log.debug/warning).
Seven subprocess.run calls without check. cli.py:307,331,431, compute.py:71, dispatch.py:210, github.py:1277, harness.py:947 (ruff PLW1510). Most inspect returncode themselves; make it explicit with check=False so the intent is visible. S, low, mechanical.
Unbounded reads. GitHub API bodies are read whole (github.py:200,213, appauth.py:93, init.py:142,204,247,358) and _read_no_follow (harness.py:337-343) reads an entire transcript file into memory. Bounded in practice by the API; note only. S, low, (decision) whether to cap.
Mixed print and logging in job code. print() at 70 sites is fine for init.py (42) and the CLIs, but attempt.py (3), followup.py (1) and steward.py (1) mix it with logging inside Slurm jobs. S, low, mechanical.
Suggested PR groupings
- Toolchain bump (S-M, low):
uv lock --upgrade; ruff-pre-commit rev to match; gitleaks 8.30.1 in ci.yml and pre-commit with a sha256 check; actions/checkout v7, setup-uv v10, upload-artifact v7, download-artifact v8; ubuntu-24.04 everywhere; fix the pyproject.toml:20-22 comment. Verify by running the review workflows once.
- Tick read amplification (M, med): one
list_runs snapshot per tick passed to the eight services and the board; return the PR from close_if_done and drop tick.py:632; one list_open_issues snapshot shared by alarm, research log, intake and steward; default_branch instead of "main" at tick.py:1888.
- Test hygiene (M, low): patch
time.sleep in the 15s sync test; replace fixed sleeps with bounded polls in test_sweep_git_locks.py, test_watcher.py, test_local_compute.py, test_harness.py, test_syscall_cli.py; add tests/fakes.py (FakePopen, FakeMeasurer, git_repo fixture); move FakeHarness and RecordingDispatcher out of src.
- Dead code and private imports (S, low): delete
containment_available, STEWARD_LABEL, read_pending, shed_candidates; make _target_clone_url (to github.py), _fence/_cap (to style.py), _emit/_pull_request, _stage_launch_job_ids, _metric_from_output public in their owners; check=False on the seven subprocess.run calls; log lines on the three silent excepts.
- Lint tightening (S, low): add RET, PIE, PERF, N, TC to
[tool.ruff.lint] select and fix the ~67 hits; disallow_untyped_defs for src (9 fixes).
- Docs and CHANGELOG (S-M, low): consolidate the 56 Unreleased sub-headings into five; check off roadmap 80, 101, 291 (and 283 if confirmed); document the 12 knobs and the two
init flags; align the legacy-names wording between install.md and paths.py.
- Shared argparse groups (M, low):
add_placement_args, add_identity_args, add_session_args used by attempt, followup, steward, tick, init and cli; shrinks attempt.main and removes eleven duplicated flag definitions.
- Cycle-break leaf modules (M, med, after a design nod):
github.clone_url, seal.py (snapshot/seal helpers both attempt and followup import), wakespec.py (the tick.py:987-1143 file protocol), so the 91 lazy imports across attempt/tick/followup become top-level and the later big splits (attempt, tick, _respond) can proceed as mechanical moves.
Held for a maintainer decision, not PRs yet: dropping the AUTORESEARCH_/.autoresearch shims after 0.1.0; codex 0.153.x and hermes v2026.9.7 re-verification; single owner for seal-and-measure and for the .env reader; ETag support in GitHubClient; the orchestrator.py:577 scope-gate except.
Read-only maintainer scan of
mainat 0b3400e (after #330), run 2026-09-07 by a subagent; no code was changed. Items marked "(decision)" wait for the maintainer; the rest are candidates for agent PRs in the groupings at the end. The two.envreaders in section 2 and thetick_deploy.shline numbers predate #331.Healthy: ruff, ruff format, mypy and
uv lock --checkare all clean; 1322 tests pass in 60s under xdist; no bareexcept:; every installer verifies a sha256 or commit pin; the tick reads GitHub only, never writes, on its steady-state path.Most worth doing (1): cut the tick's read amplification:
list_runsre-reads every run record up to 13 times per tick, each in-review PR is fetched twice plus three comment lists, and the open-issue list is fetched up to five times by four services.Most worth doing (2): the dependency and CI surface is stale in a mechanical way: actions/checkout v4 (latest v7), setup-uv v6 (v10), upload/download-artifact v4 (v7/v8), gitleaks 8.24.3 (8.30.1, pinned twice without a checksum), ruff 0.16.1 (0.16.6) with the pre-commit rev to match.
Most worth doing (3): the three big modules (attempt.py 3725, tick.py 3394, followup.py 2179 lines) hold 21 functions over 150 lines and 91 in-function imports that exist only to break the attempt/tick/followup import cycle; a small set of leaf modules (clone URL, seal, wake spec) would let them split.
Decisions needed from the maintainer: when to drop the AUTORESEARCH_/.autoresearch shims (all say "release after 0.1"), whether codex 0.130.0 to 0.153.x and hermes v2026.8.13 to v2026.9.7 get re-verified, and who owns seal-and-measure (attempt vs followup) and the operator .env reader (cli.py vs tick_deploy.sh).
Measurement basis:
wc -l, an AST pass oversrc/outerloop,uv pip list --outdated,uv run ruff check --select ALL --statistics,uv run mypy --strict src,uv run pytest --durations=25, GitHub releases API for upstream pins. All paths are repo-relative. Effort S/M/L, risk low/med/high; "(decision)" marks items that need the maintainer's call before an agent PR.1. Dead or unused pathways
Two top-level symbols have no references anywhere.
containment_available(src/outerloop/image.py:193) andSTEWARD_LABEL(src/outerloop/intake.py:34) are never read by src, tests, scripts or docs (AST scan of 904 top-level defs). S, low, mechanical.Seven src symbols are referenced only from tests.
compute.gpus_in_gres(compute.py:634),dispatch.result_summary(dispatch.py:598),dispatch.parse_result_json(dispatch.py:614),housekeeping.shed_candidates(housekeeping.py:41, not called byshed_ended_workspaces),tick.read_pending(tick.py:2229, onlylist_pendingsis used), plus two test doubles that live in src:harness.FakeHarness(harness.py:1446) andtick.RecordingDispatcher(tick.py:150). Delete the first five or wire them in; move the two doubles totests/. S, low, mechanical for the doubles; (decision) forgpus_in_gres/result_summary/parse_result_json, which look like half-finished features.Legacy rename shims: eleven sites, one removal date. All say "dropped in the release after 0.1" and the version is 0.1.0.dev2, so none can go yet; listing them so the removal PR is one sweep:
src/outerloop/__init__.py:8-18_bridge_legacy_envcopiesAUTORESEARCH_*toOUTERLOOP_*at import.src/outerloop/cli.py:108-120legacy spelling insideenv_file_values;cli.py:193AUTORESEARCH_HOME.src/outerloop/cli.py:32LEGACY_RESIDENT_JOB_NAME = "autoresearch-resident"(squeue lookups at 312, 416);cli.py:35LEGACY_LOCAL_ROOT_NAME = ".autoresearch"(298).src/outerloop/paths.py:16CONFIG_DIR_NAMES = ("outerloop", "autoresearch").scripts/tick_chain.sbatch:41-46env bridge loop;scripts/tick_deploy.sh:92legacy config dir and:113twin lookup.src/outerloop/syscall.py:55CHANNEL_DIR_NAMES = (".outerloop", ".autoresearch")with hardening at 571-608 and 1098 that exists to guard the legacy name.src/outerloop/contract.py:30CONTRACT_NAMES = (".outerloop.yaml", ".autoresearch.yaml").tests/test_env_bridge.py(11 refs),tests/test_contract_names.py, and 9 test files that still write.autoresearch.yamlfixtures (tests/test_attempt.py:637,1141,2295,...).The contract-name and channel-dir shims guard files committed in target repos; the others guard operator config only, so they could be dropped on different schedules. M, med, (decision) on timing; the removal itself is mechanical.
Twelve
OUTERLOOP_*knobs are read by src and named in no doc.OUTERLOOP_CADENCE_MIN,_CLAUDE_BIN,_CODEX_BIN,_HARNESS_KEY_FILE,_MAX_JOB_MINUTES,_MIN_TICK_MINUTES,_PANEL_CODEX_KEY_FILE,_PANEL_HERMES_KEY_FILE,_PANEL_KEY_FILE,_RESIDENT_MINUTES,_SETUP_URL,_STEWARD_KEY_FILE(grep ofdocs/ README.md CONTRIBUTING.md examples/). ConverselyOUTERLOOP_TICK_TIMEOUTandOUTERLOOP_RESIDENT_{CADENCE,MARGIN,RETRY}_Sexist only inscripts/tick_resident.sh:15-21. S, low, mechanical (docs only).CLI flags with no test or doc coverage.
--app-name,--org(init.py),--claude-bin(steward.py,followup.py),--deadline-margin-s,--work-order-b64(steward.py),--run-id(followup.py),--issue(attempt.py,steward.py). The steward/followup ones are built by the tick's argv assembly, so they are live but untested; the twoinitflags are user-facing and undocumented. S, low, mechanical.Markers declared but unused.
slow,llm,slurmare declared inpyproject.tomland deselected intests/conftest.py:40, but zero tests carry them;serialhas one. Either keep as intended tiers or drop the config. S, low, (decision).2. Duplicated logic that should have one owner
The operator .env reader is implemented twice.
src/outerloop/cli.py:82-121(env_file_values) andscripts/tick_deploy.sh:91-130both implement: refuse unless owned and not group/world-writable, last assignment wins, strip one quote pair and a trailing CR, presence-based off-switch, legacy twin loses to the canonical key.tests/test_start.py:88-92keeps the key lists identical but not the parsing rules. The shell copy exists because deploy must run even whenuv syncfailed (tick_deploy.sh:43-77). M, med, (decision): accept the duplication with a shared conformance test, or have the shell call a tiny stdlib-only Python that needs no venv.Seal-and-measure has two owners.
followup.py:1503 _commit_sealed_tree,:1521 _followup_measure,:1547 _seal_and_measure,:1595 _park_remeasure,:1673 _resume_measure(275 lines) re-implement the seal/snapshot/measure/park sequence thatattempt.py:1298 _push_line_snapshot/:1317 _seal_and_pushandorchestrator.py:795 measure_and_decide(252 lines) already perform, importingwith_seed,LINE_MEMORY_PATHS,_target_clone_urllazily from attempt (followup.py:1563-1569). L, med, (decision) on the target module (aseal.pyleaf both import).Private helpers are imported across modules.
_target_clone_urlfrom attempt infollowup.py:524,1711andtick.py:1999(belongs ingithub.py);_fence/_capfrom brief inintake.py:16,followup.py:622,steward.py:383,syscall.py:46,panel.py:21(belongs instyle.py);_emit/_pull_requestfrom review_agent inverify_agent.py:23,review_summarize_cli.py:27,verify_post_cli.py:23,review_post_cli.py:30;_stage_launch_job_idsfrom attempt intick.py:1221;_metric_from_outputfrom orchestrator indispatch.py:46. Rename to public in the owning module. S, low, mechanical.Argparse groups are copied across five entry points.
--target,--run-root,--pat-file,--max-turns,--partition,--account,--image,--gpu-partition,--gpu-account,--claude-bin,--session-minutesare each defined in three to five ofattempt.py,followup.py,steward.py,tick.py,init.py,cli.py;attempt.mainalone is 390 lines (attempt.py:3332). Sharedadd_placement_args/add_identity_args/add_session_argshelpers. M, low, mechanical.The same GitHub list is fetched by four services.
github.list_open_issues(target)is called by the alarm finder (tick.py:531), the research log twice (tick.py:1400,1419), intake (intake.py:72) and steward twice (steward.py:243,326), each with its own filtering loop. One per-tick issue snapshot passed down. M, low, mechanical (see section 6).The backend version pins live in four places each. codex
0.130.0:scripts/install_codex.sh:18,.github/workflows/advisory-review-summarize.yml:115,review-agent.yml:76,advisory-review-agent.yml:191. hermesv2026.8.13plus its commit sha:scripts/install_hermes.sh:19-20,advisory-review-summarize.yml:93,review-agent.yml:71,advisory-review-agent.yml:158. A single pins file both read would make a bump a one-line change. S, low, mechanical.3. Oversized modules and functions
Ten largest files (
wc -l, src total 28,223):attempt.py3725,tick.py3394,followup.py2179,orchestrator.py2011,github.py1494,harness.py1460,climbboard.py1353,syscall.py1190,steward.py852,syscall_cli.py758. Tests mirror it:test_attempt.py5099,test_tick.py4523,test_followup.py3113.Five longest functions (AST; 748 functions total, 37 over 100 lines, 21 over 150):
followup._respond817 lines (followup.py:415),orchestrator.attempt_once712 (orchestrator.py:1140),attempt.resume_run701 (attempt.py:1604),attempt.live_attempt687 (attempt.py:2563),climbboard.render_html526 (climbboard.py:323). Next:attempt.main390,steward.live_steward336,attempt._wake_author_sleep290,tick.service_in_review289,followup._resume_measure275.attempt.py split seams. 54 top-level defs and no section markers. Natural pieces: author sleep/wake (
:752-1042), line snapshot and seal (:1298-1330),resume_run(:1604-2305),live_attempt(:2563-3250), argparsemain(:3332-3722). L, med, (decision) on the package layout (attempt/{wake,seal,resume,cli}.py).tick.py split seams. Eight
service_*functions, wake-spec files (:987-1143), pending/tombstone markers (:2165-2282), env parsing_*_from_env(:3037-3260),main(:3262). L, med, (decision).The import cycle drives 188 in-function imports (ruff PLC0415). 39 in
tick.py, 27 infollowup.py, 25 inattempt.py. attempt imports tick (attempt.py:587,2651); tick imports attempt and followup (tick.py:600,1221,1999,2312,2333); followup imports attempt eight times (followup.py:524,1249,1326,1563,1568,1711,2056,2129). Moving the shared leaves (clone URL, seal, wake spec,MAX_CLAIM_CHARS,LINE_MEMORY_PATHS) into small modules removes most of the cycle before any big split. M, med, mechanical once the leaf modules are named.climbboard.render_htmlis an HTML template in Python. 205 of its 526 lines are markup, CSS and JS (climbboard.py:323-849). Move the template to package data and render withstring.Template. M, low, mechanical.4. Dependency and tooling upgrades
Python dependencies (uv pip list --outdated). ruff 0.16.1 to 0.16.6, mypy 2.3.0 to 2.3.1, pydantic 2.13.4 to 2.13.5 (pydantic-core 2.46.4 to 2.48.0), pre-commit 4.6.1 to 4.6.2, plus transitive (filelock, platformdirs, virtualenv, pygments).
uv lock --upgradethen bump.pre-commit-config.yamlruff-pre-commitrev: v0.16.1in the same PR so hooks and CI agree. S, low, mechanical.GitHub Actions are three major versions behind.
actions/checkout@v4(14 uses; latest v7.0.1),astral-sh/setup-uv@v6(9; v10.0.1),actions/upload-artifact@v4(4; v7.0.1),actions/download-artifact@v4(5; v8.0.1).eWaterCycle/setup-apptainer@v2andpypa/gh-action-pypi-publish@release/v1are current. checkout v5+ requires node24 runners and download-artifact v5+ changed default extraction paths, so run the review workflows once after bumping. M, med, mechanical with a CI verification.gitleaks is pinned twice at 8.24.3 without a checksum.
.github/workflows/ci.yml:41curls the release tarball straight intotar;.pre-commit-config.yamlpinsrev: v8.24.3. Latest is v8.30.1. Bump both and add a sha256 check the wayscripts/install_codex.sh:22-49does. S, low, mechanical.codex pinned at 0.130.0; upstream is rust-v0.153.4 (2026-09-04).
scripts/install_codex.sh:16-18explains the hold: 0.149.x needs the code-mode-host helper and bubblewrap on the author path. Any bump is a harness re-verification (tests/test_codex_harness.py) and a live canary. M, high, (decision).hermes-agent pinned at v2026.8.13; upstream is v2026.9.7.
scripts/install_hermes.sh:19-20andHERMES_REFin three workflows; the panel lens is bench-only today. M, med, (decision).Runner images are mixed. 11 jobs use
ubuntu-latest, one usesubuntu-24.04(build-image.yml:24). Pin all toubuntu-24.04for reproducible review runs. S, low, mechanical.ruff: four rule families cost nothing to enable. On src today: RET 0, PIE 0, PERF 3, N 7, ARG 16, TC/TCH 41 (auto-fixable typing-only imports), PTH 46. Larger: C90 48 (complexity), BLE 101 (blind except), TRY 288, PL 435, D 1192. Recommend adding RET, PIE, PERF, N, TC now; track PLC0415 (188) as the cycle-break metric. S, low, mechanical.
mypy can be tightened cheaply.
--disallow-untyped-defs srcreports 9 errors in 5 files;--strict srcreports 59 in 18. Enabledisallow_untyped_defsfor src. S, low, mechanical.Only Python 3.12 is exercised.
requires-python >=3.12,.python-version3.12, no matrix inci.yml. CI is deliberately one job for billing (ci.yml:1-3), so a 3.13 run would be a second job. S, low, (decision).Stale planning comment in pyproject.
pyproject.toml:20-22still says "Planned: github: PyGithub or gh-CLI wrappers" whilegithub.pyis 1494 lines of urllib. S, low, mechanical.5. Test-suite health
Suite: 1322 passed in 59.7s (
-n auto), 0 skipped tiers in use. Slowest 25 range 2.7s to 15.0s.One test spends 15s in a real sleep.
tests/test_syscall.py::test_sync_cli_waits_for_the_done_marker(15.01s serial and parallel) callscmd_sync, which polls with a hardtime.sleep(15)(src/outerloop/syscall_cli.py:508); the neighbouring test patchestime.sleepbut this one does not. Patch it or make the poll interval a parameter. Saves a quarter of the slowest worker's wall time. S, low, mechanical.The 5-7s followup tests are xdist contention, not test cost.
test_a_capped_out_or_degraded_reread_leaves_the_findings_to_a_humanandtest_followup_row_update_respects_the_cross_seed_floortake 6.9s/5.7s under-n autobut 2.5s each serially. No action beyond the fixture consolidation below.Fixed sleeps around real subprocesses.
tests/test_sweep_git_locks.py:89,116,165,186,208(time.sleep(0.3)after a bashPopen),tests/test_harness.py:443,tests/test_syscall_cli.py:473,tests/test_watcher.py:198(0.05s),tests/test_local_compute.py:238("let the SIGKILL land"). Replace with a bounded poll on the condition. S, low, mechanical.Fakes are defined per file.
FakePopenten times (tests/test_codex_harness.py:43,131,166,193,246,275,tests/test_hermes_harness.py:43,105,219,tests/test_review_agent.py:222);FakeMeasurerthree (test_attempt.py:2256,test_measure_and_decide.py:53,test_followup.py:2374);FakeGitHubtwo with disjoint method sets (test_attempt.py:7353 methods,test_followup.py:587 methods); a_git(cwd, *args)helper four times (test_attempt.py:625,test_followup.py:47,test_steward.py:195,test_sweep_git_locks.py:21) and_repotwice.tests/conftest.pyholds one fixture. Addtests/fakes.pyand agit_repofixture. M, low, mechanical.Real subprocesses where a fake would do. 18 test files spawn subprocesses; most drive real git (the code under test is git plumbing) and are fine.
tests/test_tick_resident.pyruns the real bash chain with shimmedsbatch/uv(4.6s each, three tests) and is the only end-to-end lane; worth keeping but marking as such. S, low, (decision) whether to introduce anintegrationmarker.Test files track the module sizes.
test_attempt.py5099 lines will split along the same seams asattempt.py; do them together. L, low, mechanical after the module split.6. Tick performance
list_runsre-scans every run record up to 13 times per tick.runstate.py:246-266walksruns/*and JSON-parses eachstate.json. Per tick it is called fromservice_in_review(tick.py:610),sweep(1163),cancel_ended_launches(1224),service_research_log(1304),_sweep_implementing(1468),service_syncs(2003),service_self_initiated(2528),service_steward(2697),housekeeping.shed_candidates(housekeeping.py:45) and four board passes (climbboard.py:177,881,906,1016). With N runs that is up to 13N file reads; a fleet of 50 runs (the 2026-09-03 outage size,housekeeping.py:113) is 650 parses. Load one snapshot at tick start and pass it; the three sites that deliberately re-read a single record (tick.py:849,1525,1626) stay as they are. M, med, mechanical.Each in-review PR costs five GitHub calls per tick, one of them a duplicate.
close_if_donefetches the PR (followup.py:302), thenservice_in_reviewfetches the same PR again (tick.py:632), thenhas_new_commentslists issue comments, reviews and review comments (followup.py:334-341). Return the PR fromclose_if_doneto drop one call per record. S, low, mechanical.The open-issue list is fetched up to five times per tick. Alarm (
tick.py:531, up to 10 pages), research log (tick.py:1400per publishable record,1419on cache miss), intake (intake.py:72), steward (steward.py:243,326). One snapshot per tick. M, low, mechanical.The board reads every data file every tick.
service_climb_boarddoesget_fileper benchmark for curves (climbboard.py:1181) and data (:1278), thenget_file_contentper view (:1343) andbranch_head(:1264);service_statusreads and may writeSTATUS_PATH(:1079). A local hash of the last published content would skip the reads when nothing changed. M, low, mechanical.No conditional requests.
github.pyhas no ETag /If-None-Matchhandling (grep). Adding it to the GET path (github.py:196-215) would make the repeated list calls above free against the rate limit even before the snapshots land. M, med, (decision).Filesystem reads per tick are otherwise bounded. Heartbeat and
check_diskrun twice (tick.py:1770-1800), tombstones and pendings are glob+parse per target (tick.py:2195,2248), and the ledger issue cache is one small file (tick.py:1278). Nothing else re-lists per record.7. Comment and docs hygiene
Dated provenance comments in src: 16 lines; PR-number breadcrumbs: 55. Dates:
attempt.py:552,1333,1480,tick.py:741,1086,1709,followup.py:1628,github.py:935,housekeeping.py:5,113,limits.py:24,steward.py:121,syscall.py:1086,contract.py:107,disk.py:7. "(#NNN)" and "terra #170 r5" style:followup.py21,tick.py20, the rest scattered. Trim to intent on touch, never a mass rewrite. S, low, mechanical (on touch).CHANGELOG [Unreleased] has 56 sub-headings.
CHANGELOG.mdis 1810 lines with a single## [Unreleased](line 7) and 19### Added, 20### Changed, 12### Fixed, 4### Removed, 1### Removed (enablement scaffolding)for 219 bullets.RELEASING.md:11-14says dev pre-releases leave Unreleased in place, so the single section is by design; the sub-headings should be merged into one of each. Do it with exact-anchor edits, not a spanning regex. M, low, mechanical.Roadmap items that shipped are still unchecked.
docs/roadmap.md:80session watcher (src/outerloop/watcher.py,docs/design/session-watcher.md),:101resident tick (scripts/tick_resident.sh,cli.pyRESIDENT_JOB_NAME),:291eval seed cache (src/outerloop/evalcache.py, PR #330 merged),:283progress webpage (partly:climbboard.pyandweb/index.html). S, low, mechanical after the maintainer confirms each is "done" rather than "partly".Legacy-name wording disagrees.
docs/install.md:249says the old names are accepted "for one release";paths.py:15andcli.py:35say "the release after 0.1". Pick one. S, low, mechanical.Design note status lines are stale.
docs/design/consolidation.md:3still reads "Status: proposal (2026-08-12)" after several of its stages landed. S, low, (decision) on what to mark done.Undocumented knobs and init flags (section 1) belong in
docs/install.mdordocs/compute.md. S, low, mechanical.8. Small correctness smells
The contract is fetched from a hardcoded
main.tick.py:1884-1888calls_contract_text(github, target, "main")and reports "contract file missing on main", whileservice_eval_cache(tick.py:1986) andgithub.py:789usegithub.default_branch(target). A target whose default branch is notmainloses its launch lanes every tick. S, low, mechanical.A scope gate fails open on a normalisation error.
orchestrator.py:573-578: paths that raise inside_fold(normalize_path(path))are skipped withexcept Exception: continue, so a path the normaliser cannot handle is never compared against the shared-scope list. Log and treat as a hit, or narrow the except. S, med, (decision).Silent
except Exceptionreturns.attempt.py:224(rev-parse HEADfailure returns ""),tick.py:891(_last_worked_ts: a corrupt work marker silently resets tick coalescing),init.py:359(network error indistinguishable from "not an org"). ruff BLE001 counts 101 blind excepts in src overall; these three have no log line. S, low, mechanical (add alog.debug/warning).Seven
subprocess.runcalls withoutcheck.cli.py:307,331,431,compute.py:71,dispatch.py:210,github.py:1277,harness.py:947(ruff PLW1510). Most inspectreturncodethemselves; make it explicit withcheck=Falseso the intent is visible. S, low, mechanical.Unbounded reads. GitHub API bodies are read whole (
github.py:200,213,appauth.py:93,init.py:142,204,247,358) and_read_no_follow(harness.py:337-343) reads an entire transcript file into memory. Bounded in practice by the API; note only. S, low, (decision) whether to cap.Mixed print and logging in job code.
print()at 70 sites is fine forinit.py(42) and the CLIs, butattempt.py(3),followup.py(1) andsteward.py(1) mix it withlogginginside Slurm jobs. S, low, mechanical.Suggested PR groupings
uv lock --upgrade; ruff-pre-commit rev to match; gitleaks 8.30.1 inci.ymland pre-commit with a sha256 check; actions/checkout v7, setup-uv v10, upload-artifact v7, download-artifact v8;ubuntu-24.04everywhere; fix thepyproject.toml:20-22comment. Verify by running the review workflows once.list_runssnapshot per tick passed to the eight services and the board; return the PR fromclose_if_doneand droptick.py:632; onelist_open_issuessnapshot shared by alarm, research log, intake and steward;default_branchinstead of"main"attick.py:1888.time.sleepin the 15s sync test; replace fixed sleeps with bounded polls intest_sweep_git_locks.py,test_watcher.py,test_local_compute.py,test_harness.py,test_syscall_cli.py; addtests/fakes.py(FakePopen, FakeMeasurer,git_repofixture); moveFakeHarnessandRecordingDispatcherout of src.containment_available,STEWARD_LABEL,read_pending,shed_candidates; make_target_clone_url(togithub.py),_fence/_cap(tostyle.py),_emit/_pull_request,_stage_launch_job_ids,_metric_from_outputpublic in their owners;check=Falseon the sevensubprocess.runcalls; log lines on the three silent excepts.[tool.ruff.lint] selectand fix the ~67 hits;disallow_untyped_defsfor src (9 fixes).initflags; align the legacy-names wording betweeninstall.mdandpaths.py.add_placement_args,add_identity_args,add_session_argsused by attempt, followup, steward, tick, init and cli; shrinksattempt.mainand removes eleven duplicated flag definitions.github.clone_url,seal.py(snapshot/seal helpers both attempt and followup import),wakespec.py(thetick.py:987-1143file protocol), so the 91 lazy imports across attempt/tick/followup become top-level and the later big splits (attempt, tick,_respond) can proceed as mechanical moves.Held for a maintainer decision, not PRs yet: dropping the AUTORESEARCH_/.autoresearch shims after 0.1.0; codex 0.153.x and hermes v2026.9.7 re-verification; single owner for seal-and-measure and for the .env reader; ETag support in
GitHubClient; theorchestrator.py:577scope-gate except.