Skip to content

fix: run notebooks with the kernel cwd pinned to the workspace root - #205

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/guard-followups
Jul 28, 2026
Merged

fix: run notebooks with the kernel cwd pinned to the workspace root#205
Jammy2211 merged 1 commit into
mainfrom
feature/guard-followups

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Notebooks execute with the kernel's working directory pinned to the workspace root, so root-relative auto-simulate guards resolve.

Closes #204.

The bug

jupyter nbconvert --execute starts the kernel in the notebook's own directory, not the directory nbconvert was launched from. The workspaces document the opposite (autolens_workspace/AGENTS.md: "Scripts are run from the repository root so relative paths to dataset/ and output/ resolve correctly"), and the auto-simulate guards depend on it:

if al.util.dataset.should_simulate(str(dataset_path)):
    subprocess.run([sys.executable, "scripts/imaging/simulator.py"], check=True)

That path resolves from a script run at the root and cannot resolve from a notebook run in notebooks/<topic>/ — exit 2, "can't open file". It is the single largest contributor to the red run_notebooks shards. The .py scripts were always fine, which is why run_scripts mostly passed.

Why this shape

Both options in the original issue turned out to be unimplementable as written, so I checked each before building:

  • "Force the kernel's working directory" is not a cwd= kwarg. The launcher's cwd is already the root; nbconvert overrides the kernel's cwd regardless and exposes no CLI flag for it. The only knob is resources['metadata']['path'], which nbclient turns into the kernel cwd (nbclient/client.py:535) — Python API only.
  • "Resolve off __file__" cannot work: __file__ is not defined in a notebook kernel (verified). It exists in the .py scripts, which already work, and is missing in exactly the broken context.

So execute_notebook now subprocesses a new autohands/run_notebook.py that sets the resource path. The subprocess boundary is kept deliberately — process isolation, BUILD_SCRIPT_TIMEOUT and the per-notebook environment are all unchanged. Only the kernel's cwd moves. No tutorial scripts are touched.

The load-bearing constraint

build_util.is_clean_skip_exit parses the run's combined stdout/stderr for a CellExecutionError marker whose last line is SystemExit: 0, to tell an intentional optional-dependency skip from a genuine failure. run_notebook.py therefore leaves CellExecutionError uncaught, so Python's own traceback reproduces that exact shape. A handler that reformatted the message would silently break skip detection — noted in the module docstring.

Verified against the classifier:

[OK] skip.ipynb  exit=1  is_clean_skip_exit=True  (want True)
[OK] fail.ipynb  exit=1  is_clean_skip_exit=False (want False)
[OK] err.ipynb   exit=1  is_clean_skip_exit=False (want False)

Verification

End-to-end on a real workspace notebook (autogalaxy_workspace/notebooks/guides/plot/examples/mat_plot.ipynb), whose guard shells out to a root-relative simulator:

OLD PATH: jupyter nbconvert --execute   -> exit=1  CalledProcessError
NEW PATH: run_notebook.py               -> exit=0  PASS

Two regression tests added (tests/test_run_notebook_cwd.py) covering the kernel cwd and the root-relative-subprocess case. Full suite green: 236 passed.

🤖 Generated with Claude Code

`jupyter nbconvert --execute` starts the kernel in the notebook's own
directory, not the directory nbconvert was launched from. The workspaces
document the opposite ("scripts are run from the repository root so relative
paths to dataset/ and output/ resolve") and their auto-simulate guards shell
out to root-relative simulator paths, so every auto-simulating notebook failed
with exit 2 ("can't open file") — the largest contributor to the red
run_notebooks shards.

nbconvert exposes no CLI flag for the kernel's working directory; the only
knob is resources['metadata']['path'], which nbclient turns into the kernel
cwd, and that is reachable only from the Python API. So execute_notebook now
subprocesses a new autohands/run_notebook.py which sets it. The subprocess
boundary is kept deliberately: process isolation, BUILD_SCRIPT_TIMEOUT and the
per-notebook environment are all unchanged — only the kernel's cwd moves.

run_notebook.py leaves CellExecutionError uncaught on purpose. Python's own
traceback then reproduces the exact shape build_util.is_clean_skip_exit parses
(a CellExecutionError marker whose last line is `SystemExit: 0`) to tell an
intentional optional-dependency skip from a real failure. Verified:
sys.exit(0) -> True, sys.exit(1) -> False, ValueError -> False.

Two regression tests lock in the kernel cwd and the root-relative-subprocess
case. Full suite green (236).

Closes #204

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Jammy2211
Jammy2211 merged commit ef0f5a3 into main Jul 28, 2026
34 checks passed
@Jammy2211
Jammy2211 deleted the feature/guard-followups branch July 28, 2026 13:08
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.

fix: notebooks execute from their own directory, breaking every auto-simulate subprocess

1 participant