Skip to content

maintenance: resolve the session-start hook and the last hardcoded workspace paths #393

Closed

Description

@Jammy2211

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 — the ensure_full_clone unshallow 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 — the PYAUTO_ROOT export, 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.py and test_session_hook_sync.py stay green; repos_sync.py --check stays 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

  1. Jammy2211 commented on Sep 18, 2026

    @Jammy2211
    ContributorAuthor

    Shipped

    PR Merged
    PyAutoLabs/PyAutoReduce#75 21:21:51Z
    PyAutoLabs/PyAutoArray#561 21:21:56Z
    PyAutoLabs/PyAutoMind#415 21:22:00Z

    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 drift is gated github.event_name != 'pull_request', and Reduce's nojax is if: inputs.package != 'autoreduce' in the shared lib-tests.yml:529. Heart freeze: not frozen — which mattered here, since PyAutoReduce and PyAutoArray are category: library and genuinely gated by it. Every branch proven merged by git merge-base --is-ancestor.

    session_hook_propagate.yml fired 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 PyAutoBrain is a symlink into the canonical workspace. Measured before a guard was added:

    REAL bundle worktree: root=/home/jammy/Code/PyAutoLabs   reason=.pyauto-root marker
    

    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 guard scripts/session_bootstrap.sh already 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.sh entered the gate's paths filter on 2026-08-29 (#369) and the generated-hooks leg entered repos_sync.py on 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 CHECK beside --only (both validated against the full registry before narrowing, so a typo can never silently disable a gate), used only on pull_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 0 and still fails the run through the assertion. That demonstration also caught a bug in the fix — a -k selecting one of two hook tests — now pinned by a test that extracts the -k from the workflow and asserts it matches both. Verified in live CI: 2 passed, 47 deselected.

    Design decision worth recording

    PyAutoReduce deliberately does not import PyAutoBrain's resolver. It is a released PyPI distribution: a pip install pyautoreduce user 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-root flag; resolution demonstrated across six layouts including the phase-3 one.

    The _CAND fallbacks replaced a guess at where the canonical checkout is with the fact git already holds: a worktree's .git is a file containing gitdir: <canonical>/….

    Filed, not fixed here

    • PyAutoArray/files/{ghost_peak,pca_rotation}_experiment.py import autoarray.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.sh carries 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-root marker in the same commit as the move.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions