feat: opt-in lists + notebook leg, so workspaces can delegate run_smoke.py - #261
Merged
Merged
Conversation
…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
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
5 tasks
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
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Both PyAutoHands legs of #260 — collapsing the ten vendored
.github/scripts/run_smoke.pycopies onto this runner.The obstacle to delegation was never behaviour, it was discovery model.
run_python.py/run.pywere opt-out only (recursive discovery minusno_run.yaml), while theworkspaceandworkspace_testvariants are opt-inallowlists (
smoke_tests.txt,smoke_notebooks.txt). That mismatch is why sevenrepos 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-inallowlist.
build_util.regenerate_notebook(nb_path, scripts_dir)— new; stale-notebookrecovery.
execute_scripts_in_folder(..., files=None)andexecute_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 ofNone. Split internally into_run_notebook_once+ a wrapper owning the tempcopy and the retry.
run_python.py --list;run.py --list --no-write-back --retry-from.See full details below.
Test Plan
pytest tests/ -q→ 388 passed, 5 skipped, 0 failedtests/test_script_list.py,tests/test_notebook_delegation.py)makes the
execute_notebooksplit saferepos_sync.py --check --only "tenant firewall (organ code)") → OKunit tests: allowlist order preserved;
no_runwinning 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 (
1with a failure,0all-green,1on 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-inallowlist to
List[Path], in the list's own order. Blanks and#commentsdropped, duplicates collapsed to first occurrence. Raises
FileNotFoundErrorif the list file is missing.
build_util.regenerate_notebook(nb_path, scripts_dir)— regenerates onenotebook from its source
.pyinto a temp dir viapy_to_notebook. RaisesFileNotFoundErrorwhen 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 committednotebooks untouched.
run.py --retry-from DIR— one regenerate-and-retry from the scripts dir.Changed
execute_notebookreturns a status string rather thanNone. Nothing in-treeconsumed the old
None.execute_scripts_in_folder/execute_notebooks_in_folderacceptfiles=.Omitted → unchanged recursive discovery.
Deliberate invariants (each pinned by a test)
find_scripts_in_folder'ssimulator-first sort. A hand-maintained list's sequence is the author's
statement of what must run before what.
no_run.yamlwins over the allowlist, and is checked before existence —so an excluded-and-deleted entry is
SKIPPED, notFAILED. Letting a listoverride an explicit exclusion would resurrect a script someone deliberately
turned off.
runner's contract is to continue through failures; a stale entry must not cost
coverage of every entry after it.
the vacuously-green-gate failure mode.
doubles the slowest entry's cost.
contributes exactly one result and a temp path never leaks into the report.
Two findings that changed the plan
run_notebook.pywrites executed outputs back in place. Right forgeneration, 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 thekernel 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_RCdid not need promoting. That guard exists because theworkspace copy shells out to a bare
jupyter, so an absent toolchain raisedFileNotFoundErrorout ofmain()and aborted with no summary line.execute_notebookinvokessys.executable run_notebook.py, which alwaysexists — the failure mode is structurally absent here.
Docs
docs/internals.mdwas 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_pythondocument the new flags.Generated by the PyAutoLabs agent workflow.
Generated by Claude Code