Skip to content

feat: opt-in lists + notebook leg, so workspaces can delegate run_smoke.py - #261

Merged
Jammy2211 merged 2 commits into
mainfrom
claude/smoke-copy-drift-ci-docs-ozntvv
Aug 24, 2026
Merged

feat: opt-in lists + notebook leg, so workspaces can delegate run_smoke.py#261
Jammy2211 merged 2 commits into
mainfrom
claude/smoke-copy-drift-ci-docs-ozntvv

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Both PyAutoHands legs of #260 — collapsing the ten vendored
.github/scripts/run_smoke.py copies onto this runner.

The obstacle to delegation was never behaviour, it was discovery model.
run_python.py / run.py were opt-out only (recursive discovery minus
no_run.yaml), while the workspace and workspace_test variants are opt-in
allowlists (smoke_tests.txt, smoke_notebooks.txt). That mismatch is why seven
repos still vendor a copy of the loop, and why each of the last three fixes —
env resolution (#185), per-script timeouts (#226/#227), the jupyter guard — had
to be swept across N repos by hand. The HowTo tier needed none of those sweeps
because it holds no logic.

This PR gives Hands everything those seven repos need to become HowTo-shaped
delegators. No workspace changes here — those are one-file PRs that follow,
gated on this merging.

Measured 2026-08-24: ten copies, three variants, and no live drift inside any
variant. That is the receipt for three manual sweeps, not evidence the
copy-per-repo design is safe.

API Changes

Additive only; every existing call site behaves exactly as before.

  • build_util.files_from_list(directory, list_path) — new; resolves an opt-in
    allowlist.
  • build_util.regenerate_notebook(nb_path, scripts_dir) — new; stale-notebook
    recovery.
  • execute_scripts_in_folder(..., files=None) and
    execute_notebooks_in_folder(..., files=None, write_back=True, retry_from_scripts=None)
    — new keyword args, defaults preserve today's behaviour.
  • execute_notebook(..., write_back=True, retry_from_scripts=None, report_as=None)
    — new keyword args; now returns "passed"/"failed"/"timeout" instead of
    None. Split internally into _run_notebook_once + a wrapper owning the temp
    copy and the retry.
  • CLI: run_python.py --list; run.py --list --no-write-back --retry-from.

See full details below.

Test Plan

  • pytest tests/ -q388 passed, 5 skipped, 0 failed
  • 16 new tests (tests/test_script_list.py, tests/test_notebook_delegation.py)
  • The 38 existing notebook + timeout tests pass untouched, which is what
    makes the execute_notebook split safe
  • Tenant-firewall gate (repos_sync.py --check --only "tenant firewall (organ code)") → OK
  • Both CLI legs exercised end-to-end against fixture workspaces, not only via
    unit tests: allowlist order preserved; no_run winning over the list;
    a stale notebook recovering via regenerate-and-retry and being recorded
    once under its original path; a missing entry failing without stopping
    the run; the committed notebook byte-identical afterwards; exit codes
    propagating (1 with a failure, 0 all-green, 1 on a missing list file)

Readiness gate: PyAutoHeart is not checked out in the authoring session, so the
documented fallback applied — the repo's own suite as the gate, green.

Full API Changes (for automation & release notes)

Added

  • build_util.files_from_list(directory, list_path) — resolves an opt-in
    allowlist to List[Path], in the list's own order. Blanks and # comments
    dropped, duplicates collapsed to first occurrence. Raises FileNotFoundError
    if the list file is missing.
  • build_util.regenerate_notebook(nb_path, scripts_dir) — regenerates one
    notebook from its source .py into a temp dir via py_to_notebook. Raises
    FileNotFoundError when there is no source script.
  • run_python.py --list FILE, run.py --list FILE — opt-in coverage.
  • run.py --no-write-back — execute a throwaway copy, leaving committed
    notebooks untouched.
  • run.py --retry-from DIR — one regenerate-and-retry from the scripts dir.

Changed

  • execute_notebook returns a status string rather than None. Nothing in-tree
    consumed the old None.
  • execute_scripts_in_folder / execute_notebooks_in_folder accept files=.
    Omitted → unchanged recursive discovery.

Deliberate invariants (each pinned by a test)

  1. Allowlist order is the file's, not find_scripts_in_folder's
    simulator-first sort. A hand-maintained list's sequence is the author's
    statement of what must run before what.
  2. no_run.yaml wins over the allowlist, and is checked before existence —
    so an excluded-and-deleted entry is SKIPPED, not FAILED. Letting a list
    override an explicit exclusion would resurrect a script someone deliberately
    turned off.
  3. A listed-but-missing entry is one FAIL and the run continues. The
    runner's contract is to continue through failures; a stale entry must not cost
    coverage of every entry after it.
  4. A missing list file is a hard error. Exiting 0 having tested nothing is
    the vacuously-green-gate failure mode.
  5. A TIMEOUT is never retried — a second full cap buys the same answer and
    doubles the slowest entry's cost.
  6. A clean skip-guard exit is already a PASS and never reaches the retry.
  7. The retry's verdict replaces the first attempt's, so one notebook
    contributes exactly one result and a temp path never leaks into the report.

Two findings that changed the plan

  • run_notebook.py writes executed outputs back in place. Right for
    generation, where the outputs are the product; wrong for a PR gate, which
    must not dirty the tree it is testing. Hence --no-write-back. Since the
    kernel cwd is already pinned to the repo root, this supersedes the
    workspace copy's staged-copy-at-root trick rather than porting it.
  • JUPYTER_MISSING_RC did not need promoting. That guard exists because the
    workspace copy shells out to a bare jupyter, so an absent toolchain raised
    FileNotFoundError out of main() and aborted with no summary line.
    execute_notebook invokes sys.executable run_notebook.py, which always
    exists — the failure mode is structurally absent here.

Docs

docs/internals.md was stale on its own inventory: it claimed "nine copies,
five distinct revisions — they have drifted" and omitted HowToFit. Replaced with
the measured three-variant table, the promotion history, and the direction of
travel. bin/autohands help run / help run_python document the new flags.

Generated by the PyAutoLabs agent workflow.


Generated by Claude Code

claude added 2 commits August 24, 2026 00:46
…delegate

Phase 1 of collapsing the 10 vendored `.github/scripts/run_smoke.py` copies
onto this runner (#260).

The obstacle to delegation was never behaviour — it was discovery model.
`run_python.py` was opt-out only (recursive discovery minus no_run.yaml),
while the `workspace` and `workspace_test` variants are opt-in allowlists
(smoke_tests.txt). That mismatch is why seven repos still vendor a copy of
the loop, and why each of the last three fixes — env resolution (#185),
per-script timeouts (#226/#227), the jupyter guard — had to be swept across
N repos by hand. The HowTo tier needed none of those sweeps because it holds
no logic.

`--list FILE` runs exactly the listed entries, in the list's own order.
Four behaviours are deliberate and pinned by tests:

- Order is the file's, NOT find_scripts_in_folder's simulator-first sort. An
  allowlist is hand-maintained, so its sequence is the author's statement of
  what must run before what.
- no_run.yaml still applies and WINS over the list. An explicit exclusion is
  the more specific intent; letting an allowlist override it would resurrect
  a script someone deliberately turned off. Checked before existence, so an
  excluded-and-deleted entry is SKIPPED, not FAILED.
- A listed-but-missing entry is one FAIL and the run continues — the runner's
  contract is to continue through failures, and a stale entry must not cost
  coverage of every entry after it.
- A missing list FILE is a hard error, not an empty run. Exiting 0 having
  tested nothing is the vacuously-green-gate failure mode.

Also corrects docs/internals.md, which was stale on its own inventory: it
claimed "nine copies, five distinct revisions — they have drifted" and omitted
HowToFit. Measured 2026-08-24: ten copies, three variants, and no live drift
inside any variant — the workspace copies are byte-identical, the four
workspace_test copies differ in two docstring lines, the HowTo copies in
PROJECT alone.

Suite: 362 passed, 8 skipped. The 14 failures in this sandbox are pre-existing
and unrelated (ipynb-py-convert and Pillow unavailable); baseline on a clean
tree is the same 14 with 354 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpSFum81Jeq9KZ9wdKtaeZ
…from

Phase 2 of collapsing the vendored run_smoke.py copies onto this runner
(#260). Phase 1 gave run_python.py opt-in script lists; this gives run.py the
matching notebook leg, so the 356-line `workspace` variant can collapse too.

Of the three notebook behaviours that variant held and build_util did not, two
needed promoting and one dissolved on inspection:

- --no-write-back (promoted). run_notebook.py writes executed outputs back in
  place. That is right for generation, where the outputs ARE the product, and
  wrong for a PR gate, which must not dirty the tree it is testing. The flag
  executes a throwaway copy. Because the kernel cwd is already pinned to the
  repo root, this supersedes the workspace copy's staged-copy-at-root trick
  rather than porting it.

- --retry-from (promoted). One regenerate-from-source retry recovers a STALE
  notebook whose script moved on but whose committed .ipynb was never
  refreshed. Deliberately narrow: a TIMEOUT is never retried (a second full cap
  buys the same answer and doubles the slowest entry's cost); a clean
  skip-guard exit is already a PASS and never reaches it; and the retry's
  verdict REPLACES the first attempt's, so one notebook contributes exactly one
  result and a temp path never leaks into the report.

- JUPYTER_MISSING_RC (not needed). That guard exists because the workspace copy
  shelled out to a bare `jupyter` binary, so an absent toolchain raised
  FileNotFoundError out of main() and aborted the run with no summary line.
  execute_notebook invokes `sys.executable run_notebook.py`, which always
  exists, so a missing toolchain is an ordinary non-zero exit — one FAIL, run
  continues. The failure mode is structurally absent here.

execute_notebook is split into _run_notebook_once (runs and classifies,
returning passed/failed/timeout) and a thin wrapper owning the temp copy and
the retry. Behaviour on the existing paths is unchanged — the 38 notebook and
timeout tests pass untouched.

execute_notebooks_in_folder gains the same `files` allowlist as its script
counterpart, with the same two invariants: no_run wins over the list, checked
before existence so an excluded-and-deleted entry is SKIPPED not FAILED; and a
listed-but-missing entry is one FAIL with the run continuing.

Suite: 388 passed, 5 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpSFum81Jeq9KZ9wdKtaeZ
@Jammy2211
Jammy2211 merged commit c0e2e53 into main Aug 24, 2026
3 checks passed
Jammy2211 pushed a commit that referenced this pull request Aug 24, 2026
#261 shipped the opposite rule ("no_run wins over the list"). Measured against
the real workspaces before writing a single delegator, that rule would have
SILENTLY DROPPED 13 SCRIPTS from smoke coverage:

  autogalaxy_workspace_test  9
  autolens_workspace_test    2
  autofit_workspace          1
  autolens_workspace         1

all of which run in smoke today. The vendored runners read only
smoke_tests.txt and have never opened no_run.yaml, so honouring it would have
been a coverage regression disguised as a refactor — exactly what the task's
"no repo loses behaviour it has today" criterion forbids.

The rule was wrong because it conflated two policies for two different runs.
no_run.yaml governs the release mega-run and notebook generation; an allowlist
governs the PR smoke gate. A script legitimately appears in both — excluded
from the full build, required in smoke. So with --list the allowlist is the
policy and no_run.yaml is not consulted; without one, discovery is filtered by
no_run.yaml exactly as before.

Second fix, same cause: a missing config/build/no_run.yaml is no longer fatal
under --list. autocti_workspace_test has none, and the autohands-level fallback
path does not exist either, so both run_python.py and run.py crashed with
FileNotFoundError before running anything — once at load and again in
parse_no_run_reasons under --report-dir. Discovery still requires the file (a
run with no exclusion policy is not a safe default) and now says why.

Tests: the two that pinned the old rule are replaced by ones pinning the new,
each carrying the measured 13-script rationale, plus a test that discovery is
untouched. Suite 389 passed, 5 skipped. Verified end-to-end that a script named
by BOTH the list and no_run.yaml now runs, and that the autocti shape (no
no_run.yaml at all) completes and reports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpSFum81Jeq9KZ9wdKtaeZ
Jammy2211 pushed a commit that referenced this pull request Aug 24, 2026
…lename

#261 looked the source script up as `scripts_dir / nb_path.name`, which drops
the subdirectory. Every workspace notebook lives in one, so
`notebooks/imaging/model_fit.ipynb` searched for `scripts/model_fit.py` instead
of `scripts/imaging/model_fit.py` and never found it. The stale-notebook
recovery was therefore dead on arrival for real workspaces: FileNotFoundError,
caught, original FAIL left standing. No false PASS, but a feature that silently
did nothing.

Worse than not finding it, the bare filename can find the WRONG one — two
`model_fit.ipynb` under different topic folders map to two different scripts,
and a same-named script at the scripts root would be picked up for all of them.

execute_notebook gains `notebook_rel` and execute_notebooks_in_folder computes
it with `file.relative_to(Path.cwd() / directory)`, so the folder runner always
passes it. A direct caller that omits it keeps the old bare-filename fallback,
which is correct for a notebook at the root.

Caught while preparing the phase-2 workspace delegators, not by the existing
tests: those used a FLAT fixture (notebook and script both at the root), which
is exactly the one layout where the bug is invisible. The two new tests use a
nested notebook plus a decoy script at the scripts root, so resolving by
filename picks the decoy and fails loudly. Both were negative-tested against the
old resolution and fail there.

Suite 391 passed, 5 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpSFum81Jeq9KZ9wdKtaeZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants