Repository navigation
Description
Overview
Phase 2 of the workspace regroup. Changes no directory layout. It fixes the last two places that still derive the workspace root by "my parent directory", now that phase 1a (#391) has shipped the marker-based resolver.
The headline item is the session-start hook: it exports PYAUTO_ROOT into every remote/web session, and under any regroup it would export a family directory — poisoning every downstream consumer, writing a bogus .claude/ tree into that directory, and unshallowing one family instead of every repo. Every guard passes, so it fails silently.
Plan
- Give the canonical session-start hook the phase-1a marker walk, and fix its one-level fan-out — fixing root discovery alone is not sufficient.
- Fix the 7 tracked Python files that hardcode an absolute workspace path, one of which is a live cross-repo reference.
- Let the existing propagation workflow push the regenerated hook to every manifest repo.
Detailed implementation plan
Affected Repositories
- PyAutoMind (primary) — the canonical hook; propagates itself
- PyAutoReduce, PyAutoArray — hardcoded paths
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoMind | main | clean |
| ./PyAutoReduce | main | clean |
| ./PyAutoArray | main | clean |
worktree_check_conflict workspace-resolver-fanout PyAutoMind PyAutoReduce PyAutoArray → exit 0.
Suggested branch: feature/workspace-resolver-fanout
1. The session-start hook — one PR, not 37
PyAutoMind/policy/session_start_hook.sh:72 is WORKSPACE_ROOT="$(dirname "$REPO_DIR")". Verified 2026-09-18: exactly 37 copies, all byte-identical (single md5 dbcdbbde…), all carrying that line.
This is one PR. PyAutoMind/.github/workflows/session_hook_propagate.yml fires on a push to main touching policy/session_start_hook.sh and bot-pushes the regenerated copy to every manifest repo. Its own header states the reasoning: "WHY A BOT PUSH, NOT ~30 PULL REQUESTS … Thirty PRs per hook edit is a review queue nobody would read."
Correct severity (an early survey overstated this): the hook exits at :47 unless CLAUDE_CODE_REMOTE=true, so it never runs in a local CLI session. It is a remote/web-session defect — real, but not the dominant local risk.
Fixing root discovery is not sufficient. The one-level fan-out must move too:
:426— theensure_full_cloneunshallow loop (would unshallow one family, not 39 repos):470—install_workspace_settings, which would write a.claude/tree into a family directory; both its[ -d "$root/.git" ]and[ -w "$root" ]guards pass, so it succeeds and creates clutter at a wrong root:564— thePYAUTO_ROOTexport, which poisons every downstream consumer
Adopt the phase-1a rule exactly — walk up to .pyauto-root, sibling-organ probe as fallback — and do not invent a second resolution rule. bin/_pyauto_root.sh already exports PYAUTO_ROOT_REASON and PYAUTO_ROOT_MARKER.
Coverage exists: PyAutoMind/tests/test_session_bootstrap.py, tests/test_session_hook_sync.py.
2. The 7 hardcoded absolute paths
Only one is a genuine cross-repo reference, and the only one a regroup actually breaks:
PyAutoReduce/scripts/reduce_cosmos_web_ring.py:34→/home/jammy/Code/PyAutoLabs/autolens_assistant/dataset/imaging/cosmos_web_ring/wavebands
Also PyAutoReduce/prototypes/: starred_vs_epsf_m92.py:30, starred_vs_epsf_omegacen.py:26, starred_vs_epsf_comparison.py:31, starred_epsf_spike.py:38 (all Path.home()/"Code/PyAutoLabs/PyAutoReduce/scripts/output"), and PyAutoArray/files/ghost_peak_experiment.py:39, pca_rotation_experiment.py:28 (~/Code/PyAutoLabs-wt/ in docstrings).
This corrects an earlier sweep that reported zero such files; an independent Codex review found them.
Testing / Witness
- With a nested workspace fixture, the regenerated hook resolves the true root, not the family directory, and fans out over every manifest repo.
- After propagation,
git grep -c 'WORKSPACE_ROOT="$(dirname "$REPO_DIR")"'returns 0 across all repos. - No tracked Python file in PyAutoReduce or PyAutoArray matches
Code/PyAutoLabs. test_session_bootstrap.pyandtest_session_hook_sync.pystay green;repos_sync.py --checkstays exit 0.
Key Files
PyAutoMind/policy/session_start_hook.sh(canonical; 37 generated copies)PyAutoMind/.github/workflows/session_hook_propagate.yml(the propagation path)PyAutoReduce/scripts/,PyAutoReduce/prototypes/,PyAutoArray/files/
Explicitly not in this phase
The 12 smoke shims were split out into draft/maintenance/pyautobrain/workspace_smoke_shim_bootstrap.md. They are not copies — 12 distinct checksums in 5 shape-classes — and have no propagation mechanism, so patching them twelve times would re-affirm the structure that made them drift. That task carries the prior question of whether 12 divergent copies of one bootstrap should exist at all.
Also deferred to phase 3: the physical move, the repos.yaml path: field, the ~12 root enumerators, worktree.sh, and smoke_install.sh's flat pip install ./PyAutoFit … chain (which PyAutoHeart runs locally, not only in CI).
Context
Phase 1a shipped 2026-09-18 — PyAutoBrain#392, PyAutoHeart#232, PyAutoHands#283, PyAutoMind#414 — putting the marker-based resolver on main. This task consumes it.
Carried limitation: step 3 of the resolver is still deliberately wrong for a nested workspace with no marker, to protect the remote single-repo case. So the marker is the only thing protecting a regrouped tree, and phase 3's move must create it in the same commit.
Original Prompt
Click to expand starting prompt
# Workspace resolver fan-out: the hook, the smoke shims and the hardcoded paths
Type: maintenance
Target: PyAutoBrain
Repos:
- PyAutoMind
- PyAutoReduce
- PyAutoArray
Difficulty: large
Autonomy: supervised
Priority: high
Status: formalised
Consequence: judge
Witness: with a nested workspace fixture the regenerated session-start hook resolves the TRUE root rather than the family directory and fans out over every manifest repo rather than one family; `git grep -c 'WORKSPACE_ROOT="$(dirname "$REPO_DIR")"'` returns 0 across all repos after propagation; and no tracked Python file in PyAutoReduce or PyAutoArray matches `Code/PyAutoLabs`.
Review-minutes: 25
Unattended: needs-slicing
Unblocked: phase 1a SHIPPED 2026-09-18 (PyAutoBrain#392, PyAutoHeart#232, PyAutoHands#283, PyAutoMind#414) — record complete/2026/09/workspace-location-contracts.md. The resolver this consumes is on main: `_pyauto_root` resolves by `.pyauto-root` marker with the sibling-organ probe demoted to a fallback, and exports PYAUTO_ROOT_REASON / PYAUTO_ROOT_MARKER for shell callers. NOTE the limitation this phase inherits: step 3 is still wrong for a NESTED workspace with no marker (pinned by a test, deliberately, to protect the remote single-repo case), so the marker is the only thing protecting a regrouped tree.
Phase 2 of the PyAutoLabs workspace regroup. Changes NO directory layout. Mechanical fan-out that
CONSUMES the single resolver phase 1a introduces, so it cannot land before it.
## Scope
1. **The session-start hook — 1 source, 37 generated copies.**
`PyAutoMind/policy/session_start_hook.sh:72` is `WORKSPACE_ROOT="$(dirname "$REPO_DIR")"`. Verified
2026-09-18: exactly 37 copies exist, all byte-identical (single md5 dbcdbbde9ef3b44f54cbb1dca94fbf95),
and all 37 carry that line. Give it the phase-1a marker walk and regenerate via
`repos_sync.py --write`.
Correct severity, which an early survey overstated: the hook exits at :47 unless
CLAUDE_CODE_REMOTE=true, so it never runs in a local CLI session. It is a remote/web-session defect,
not a local one — real, but not the dominant local risk.
Fixing root discovery is NOT sufficient on its own: the one-level fan-out must move too —
:426 (the ensure_full_clone unshallow loop), :470 (install_workspace_settings, which would write a
bogus .claude/ root INTO a family directory, every guard passing), and :564 (the PYAUTO_ROOT export,
which poisons every downstream consumer). Coverage exists in PyAutoMind/tests/test_session_bootstrap.py
and tests/test_session_hook_sync.py.
2. Renumbered — the smoke shims moved OUT of this task. They are
`draft/maintenance/pyautobrain/workspace_smoke_shim_bootstrap.md`: 12 repos, no propagation
mechanism, and the 12 files are NOT copies (12 distinct checksums, 5 shape-classes). That task
carries the prior question of whether 12 divergent copies of one bootstrap should exist at all.
3. **The 7 tracked Python files that hardcode an absolute workspace path.**
Only one is a genuine cross-repo reference and the only one a regroup actually breaks:
`PyAutoReduce/scripts/reduce_cosmos_web_ring.py:34` ->
`/home/jammy/Code/PyAutoLabs/autolens_assistant/dataset/imaging/cosmos_web_ring/wavebands`.
Also `PyAutoReduce/prototypes/` starred_vs_epsf_m92.py:30, starred_vs_epsf_omegacen.py:26,
starred_vs_epsf_comparison.py:31, starred_epsf_spike.py:38 (all
`Path.home()/"Code/PyAutoLabs/PyAutoReduce/scripts/output"`), and
`PyAutoArray/files/ghost_peak_experiment.py:39`, `pca_rotation_experiment.py:28`
(`~/Code/PyAutoLabs-wt/` inside docstrings).
This corrects an earlier sweep that reported ZERO such files; an independent Codex review found them.
## Not in this phase
The physical move, the repos.yaml `path:` field, the ~12 root enumerators, worktree.sh and the
IDE/PYTHONPATH/symlink migration are phase 3. Also deferred there: the `smoke_install.sh` flat
`pip install ./PyAutoFit ./PyAutoArray ...` chain, which PyAutoHeart runs LOCALLY via
`heart/smoke.py:313` with `cwd=organism_root` (`smoke.py:229` builds `organism_root / repo` the same
way) — it is a shared CI+local script whose two callers would need different layouts, which is exactly
the ordering hazard the Codex review named. Do not introduce a universal `root / manifest.path` rule
before that is resolved.
Activity
Jammy2211 commentedon Sep 18, 2026
Shipped
All legs green before merge — Mind firewall+privacy, Reduce 3.12/3.13, Array 3.12/3.13/nojax. Both skipped legs verified structural, not accidental: Mind's
driftis gatedgithub.event_name != 'pull_request', and Reduce'snojaxisif: inputs.package != 'autoreduce'in the sharedlib-tests.yml:529. Heart freeze: not frozen — which mattered here, since PyAutoReduce and PyAutoArray arecategory: libraryand genuinely gated by it. Every branch proven merged bygit merge-base --is-ancestor.session_hook_propagate.ymlfired on the merge, as designed, to sync the new hook into every manifest repo.The regression the implementation caught — in my own brief
I instructed "source the resolver when it is reachable". That is unsafe: the resolver anchors on the Brain checkout, and in a task bundle
PyAutoBrainis a symlink into the canonical workspace. Measured before a guard was added:A hook running inside a bundle would have exported the canonical workspace to the whole session, written a
.claude/root into it, and unshallowed its repos. The hook now refuses a resolver answer that does not contain its own checkout — the guardscripts/session_bootstrap.shalready carried for the same trap, generalised to an ancestor test.That is the fifth instance in this epic of the same shape: locating is not writing, and not grading.
The gate this PR exposed, and fixed
The first push went red on exactly one leg, structurally: the sibling organs are checked out at their mains, so their hook copies are stale by construction until propagation runs after merge. Nothing a PR can do makes that leg pass.
Latent, not new —
policy/session_start_hook.shentered the gate's paths filter on 2026-08-29 (#369) and the generated-hooks leg enteredrepos_sync.pyon 2026-09-03, while the last canonical hook edit was 2026-08-27. This was the first hook PR to run under both, and every future one would have been red forever.Fixed with
--skip CHECKbeside--only(both validated against the full registry before narrowing, so a typo can never silently disable a gate), used only onpull_request, only when the PR's real diff touches a canonical hook, with a loud banner and — critically — a compensating assertion that PyAutoMind's own copies match canonical.Proven not to be a weakening: with the leg skipped, desyncing that copy by one line leaves the drift check at
exit 0and still fails the run through the assertion. That demonstration also caught a bug in the fix — a-kselecting one of two hook tests — now pinned by a test that extracts the-kfrom the workflow and asserts it matches both. Verified in live CI:2 passed, 47 deselected.Design decision worth recording
PyAutoReducedeliberately does not import PyAutoBrain's resolver. It is a released PyPI distribution: apip install pyautoreduceuser has no Brain checkout, no marker and no workspace, so the import would be dead code on every machine but this one — the exact failure class that resolver's docstring exists to kill. It would also invert the dependency direction. It uses$AUTOREDUCE_DEMO_ROOT→ a bounded sibling probe → an unverified guess, plus a--demo-rootflag; resolution demonstrated across six layouts including the phase-3 one.The
_CANDfallbacks replaced a guess at where the canonical checkout is with the fact git already holds: a worktree's.gitis a file containinggitdir: <canonical>/….Filed, not fixed here
PyAutoArray/files/{ghost_peak,pca_rotation}_experiment.pyimportautoarray.inversion.mesh.interpolator.rectangular_spline, which no longer exists — they cannot run. Confirmed independently by the PyAuto API gate. Filed as a decision (repair or retire).scripts/session_bootstrap.shcarries the same one-level defect and its root guard rejects a correct nested root. It is a phase-3 root-enumerator item; until that lands, the multi-repo bootstrap door still misses nested checkouts even though the hook no longer does.Next
Phase 2c —
draft/maintenance/pyautobrain/workspace_smoke_shim_bootstrap.md— the 12 smoke shims, split out of this task because they are not copies (12 checksums, 5 shape-classes) and have no propagation mechanism. Phase 3 (the physical regroup) still waits on the in-flight tasks clearing the release gate, and must create the.pyauto-rootmarker in the same commit as the move.