Skip to content

maint(session): four-organ remote block, and uv repair in the hook #360

Closed

Description

@Jammy2211

Overview

The 2026-08-27 mobile-workflow pass shipped two things half-rolled-out, because the session that shipped them could attach only PyAutoMind and PyAutoBrain. Heart and Hands still carry hand-written remote-session text that is a pass behind (it names No module named pytest as the trigger — a symptom the canonical text records as having stopped appearing when the image moved to 3.12). And repair_uv_tools — the fix for uv tool envs that resolve into the session venv and die with ModuleNotFoundError naming themselves — sits in scripts/session_bootstrap.sh, which a single-repo session never calls; the hook is what that session runs.

Both halves landed in be0121ad and 33c81de6 (2026-08-27 18:14 UTC). Nothing has touched scripts/, policy/ or tests/ since, and there are no open PRs on the four organs.

Plan

  • Add the repos_sync:remote marker pair to PyAutoHeart/AGENTS.md and PyAutoHands/AGENTS.md, replacing their hand-written "Remote sessions" sections, so all four organs take the block from the generated source.
  • Move repair_uv_tools out of the bootstrap and into policy/session_start_hook.sh, beside retool_uv_tools which creates the condition — so the repair reaches a single-repo session, which runs the hook and never the bootstrap.
  • Harden the moved function for the hook's set -euo pipefail (the bootstrap runs set -u only, and two of its command substitutions would abort the hook as written).
  • Regenerate the four organs' hook copies, keep the bootstrap's seam and its --check leg working, and add a hook-level version of the uv-repair test.
  • File a separate Mind prompt for the 30-repo hook-propagation backlog this task deliberately leaves (see "Scope" below).

Scope: the prompt's --check clause cannot be met, and why

The prompt says the hook "is generated into all four organs". It is generated into 34 repos — write_session_hooks iterates the whole manifest, not just organs. repos_sync.py --check is already red before this task touches anything:

check session-start hooks (generated): 34 mismatch(es)
  ✗ 'PyAutoMemory' … 'PyAutoFit' … 'autolens_workspace' …   (30 stale copies)
  ✗ 'euclid_assistant': no .claude/hooks/session-start.sh
  ✗ 'admin_jammy': no .claude/hooks/session-start.sh

The four organs are clean; every other repo is a hook-pass behind, because the 2026-08-26 regeneration wave (PyAutoHeart 701b578, PyAutoHands d78ea54) also only saw four repos. No gate catches this: firewall_gate.yml checks out exactly Mind + Brain + Heart + Hands and skips absent repos, so the 30 stale copies are invisible to CI and visible only to a full local-workspace --check.

So this task regenerates the four organs — the set CI actually gates — and the acceptance bar becomes --check --root <four-organ worktree root> is clean. The 30-repo propagation is filed separately rather than folded in, since it would mean PRs against every library, workspace, HowTo and assistant repo.

Detailed implementation plan

Affected Repositories

  • PyAutoMind (primary) — canonical hook, bootstrap, policy, tests
  • PyAutoBrain — regenerated hook copy only
  • PyAutoHeart — AGENTS.md markers + regenerated hook copy
  • PyAutoHands — AGENTS.md markers + regenerated hook copy

PyAutoBrain is not listed in the prompt but must be claimed: it carries a generated hook copy that goes stale the moment the canonical hook changes, and firewall_gate.yml checks it.

Branch Survey

Repository Current Branch Dirty?
./PyAutoMind main clean
./PyAutoBrain main clean
./PyAutoHeart main clean
./PyAutoHands main clean

All four in sync with origin/main; worktree_check_conflict exits 0, no claims in active.md.

Suggested branch: feature/organ-remote-block-and-uv-hook-repair

Implementation Steps

1. Heart and Hands opt into the generated block

  1. PyAutoHeart/AGENTS.md:49-70 — delete the hand-written ## Remote sessions: knock on the door first section, insert <!-- repos_sync:remote:begin --> / <!-- repos_sync:remote:end --> in its place, immediately above the existing repos_sync:history block. Mirrors PyAutoMind/AGENTS.md:104 and PyAutoBrain/AGENTS.md:163.
  2. PyAutoHands/AGENTS.md:41-68 — same.
  3. repos_sync.py --write fills both from policy/remote_sessions.md.

Deliberate loss: the canonical text carries no per-repo halves by design (repos_sync.py:128 module comment; test_the_canonical_text_carries_no_per_repo_numbers pins it). Heart's "641 tests / 7.6s / 2.7s", Hands' "406 tests / 27s / 13s" and Hands' .claude/session-python.txt bullet all go. Nothing is lost operationally — the hook reads that file at run time.

2. Move repair_uv_tools into the hook

  1. Move the function and its comment block from scripts/session_bootstrap.sh:120-146 into policy/session_start_hook.sh as leg 3b, directly after retool_uv_tools (:327).
  2. say → log.
  3. set -e hardening (required). The hook runs set -euo pipefail; the bootstrap only set -u. Two assignments abort the whole hook as written — base="$("$VENV/bin/python" -c … 2>/dev/null)" and the post-repair prefix="$("$link" -c … 2>/dev/null)". Both get an || base="" / || prefix="" tail.
  4. Call it from the main flow (:477) directly after retool_uv_tools, before point_pytest_at_venv.
  5. Add a --repair-uv-tools argv seam beside the existing PYAUTO_SESSION_DEFINE_ONLY seam (:455).

Placement note for the moved comment: the bug reaches a tool env by two paths, not one. retool_uv_tools rebuilds 3.11 tools and hands them the hijacked /usr/local/bin/python3; point_system_default separately breaks every pre-existing tool env that already symlinked there and that retool skips (is_py312 … && continue). Running after retool_uv_tools covers both, because point_system_default runs earlier.

3. Bootstrap keeps its call, forwarding to the hook

  1. session_bootstrap.sh:158-163 — the --repair-uv-tools seam becomes CLAUDE_CODE_REMOTE=true exec "$HOOK" --repair-uv-tools (after the existing [ -x "$HOOK" ] guard). Kept as a subprocess so the hook's set -euo pipefail never leaks into the bootstrap, which is contractually "never a gate".
  2. session_bootstrap.sh:296 — the trailing call becomes the same subprocess invocation, non-fatal on failure. Still worth keeping: it covers the case where the hook's python leg exited early.
  3. The --check leg is untouched.

4. Tests

  1. tests/test_session_bootstrap.py — the existing _repair() helper still drives the bootstrap seam, which now proves the forward works end-to-end. test_a_tool_env_pointed_at_the_session_venv_is_repaired and test_a_healthy_tool_env_is_left_alone must stay green unchanged (they assert on "repointed widget", which log() still emits).
  2. Add the hook-level twin using the existing _call_hook_function("repair_uv_tools", …) helper (:287) with PYAUTO_UV_TOOLS_DIR — no new seam needed for it.
  3. Add a test that the hook's main flow actually calls the repair. That is the bug: a single-repo session runs the hook, and nothing else.

5. Regenerate, scoped

--write from the full local workspace would write all 34 repos. Run it from the task worktree root instead, where only the four claimed repos exist — absent repos are skipped by design:

python3 PyAutoMind/scripts/repos_sync.py --write --root ~/Code/PyAutoLabs-wt/organ-remote-block-and-uv-hook-repair
python3 PyAutoMind/scripts/repos_sync.py --check --root ~/Code/PyAutoLabs-wt/organ-remote-block-and-uv-hook-repair

6. File the backlog prompt

New Mind draft under maintenance/organs/: the 30 stale hook copies + euclid_assistant and admin_jammy which carry none, and the fact that no gate sees them.

Key Files

  • PyAutoMind/policy/session_start_hook.sh — canonical hook; gains repair_uv_tools + the argv seam
  • PyAutoMind/scripts/session_bootstrap.sh — loses the function, keeps both call sites as hook invocations
  • PyAutoMind/policy/remote_sessions.md — canonical block, unchanged
  • PyAutoMind/scripts/repos_sync.py — unchanged (keeps firewall_gate.yml from triggering; it is path-filtered to this file)
  • PyAutoMind/tests/test_session_bootstrap.py — hook-level uv-repair coverage
  • PyAutoHeart/AGENTS.md, PyAutoHands/AGENTS.md — marker pairs replacing hand-written text
  • {PyAutoMind,PyAutoBrain,PyAutoHeart,PyAutoHands}/.claude/hooks/session-start.sh — regenerated

Work Classification

Library — organ code and generated copies. No workspace follow-up.

Worktree root

~/Code/PyAutoLabs-wt/organ-remote-block-and-uv-hook-repair/

Testing

  • python3 -m pytest -q in PyAutoMind (hook + bootstrap + repos_sync suites)
  • repos_sync.py --check --root <worktree root> clean on every leg
  • Heart and Hands suites, since their AGENTS.md changes

Original Prompt

Click to expand starting prompt
# Two organs are missing this session's fixes, and the hook still ships the uv bug

Type: maintenance
Target: organs
Repos:
- PyAutoMind
- PyAutoHeart
- PyAutoHands
Difficulty: small
Autonomy: supervised
Priority: normal
Status: formalised
Filed: 2026-08-27

The 2026-08-27 mobile-workflow pass shipped two things that are, by
construction, only half-rolled-out. Both need a session with **all four organs
attached** — this one could not attach PyAutoHeart or PyAutoHands (`add_repo`
was refused), which is the same "a drift check is only as strong as the number
of repos your session can see" the last three passes each paid for once.

## 1. Opt Heart and Hands into the generated remote-session block

`PyAutoMind/policy/remote_sessions.md` is now the single source, written into
each repo's `AGENTS.md` between `<!-- repos_sync:remote:begin/end -->` by
`repos_sync.py --write`, with a drift check. Mind and Brain carry the markers;
Heart and Hands still carry their own hand-written text, which is already a pass
behind (it says "bootstrap if pytest misbehaves", a trigger that no longer
fires).

Add the markers to both, run `--write`, and confirm `--check` reports the block
for four repos rather than two. No gate order: `firewall_gate.yml` is
path-filtered to `scripts/repos_sync.py`, which this does not touch.

## 2. Promote the uv-tool repair from the bootstrap into the hook

`scripts/session_bootstrap.sh` gained `repair_uv_tools`: uv creates each tool
env's `bin/python` as a symlink to `/usr/local/bin/python3`, the hook replaces
that path with a wrapper that `exec`s the session venv, and the exec replaces
argv — so every tool env resolves `sys.prefix` to the venv and mypy, flake8,
black, poetry and pyright all die with `ModuleNotFoundError` naming themselves.
Measured 2026-08-27; `--check` called them all `3.12 OK` until the same commit
taught it to run each tool.

The fix belongs in `policy/session_start_hook.sh`, beside `retool_uv_tools`
which creates the condition — the hook is what a **single-repo** session runs,
and that session never calls the bootstrap. It was left in `scripts/` because
the hook is generated into all four organs and two of them could not be
attached: changing it here would have made `firewall_gate.yml` red on repos this
session could not regenerate.

So: move the function into the hook, regenerate all four copies, keep the
bootstrap's call (it is idempotent, and the bootstrap runs the hook anyway), and
keep the `--check` leg where it is.

## Done when

- `repos_sync.py --check` is clean with all four organs checked out, and the
  remote-session block leg names four repos.
- A fresh remote session that runs only the SessionStart hook — no bootstrap
  call — has a working `mypy`, `flake8` and `black`.
- The existing tests still pass, plus a hook-level version of
  `test_a_tool_env_pointed_at_the_session_venv_is_repaired`.

Activity

  1. Jammy2211 commented on Aug 27, 2026

    @Jammy2211
    CollaboratorAuthor

    Plan correction — step 5's --write scoping does not work as written.

    The detailed plan says to run repos_sync.py --write --root <task worktree root> so only the four claimed repos are written. Verified against the worktree worktree_create actually builds, that is wrong and would be actively harmful:

    drwxr-xr-x  PyAutoBrain          <- real worktree
    drwxr-xr-x  PyAutoHands          <- real worktree
    drwxr-xr-x  PyAutoHeart          <- real worktree
    drwxr-xr-x  PyAutoMind           <- real worktree
    lrwxrwxrwx  PyAutoFit         -> /home/jammy/Code/PyAutoLabs/PyAutoFit
    lrwxrwxrwx  autolens_workspace -> /home/jammy/Code/PyAutoLabs/autolens_workspace
    …30 more symlinks back to the canonical checkouts
    

    worktree_create symlinks every unclaimed entry back to the canonical checkout, and Path.is_dir() follows symlinks — so --write --root <worktree> would resolve those 30 names to the live main workspace and write regenerated hooks straight into it, on main, outside any branch. That is the opposite of scoping, and it would dirty repos other sessions are working in.

    Replacement for step 5: build a scratch root containing symlinks to only the four worktree repos, and point --root at that. Absent repos are skipped by design, so the other 30 are never reached, and the writes land in the worktree through the four symlinks.

    WT=~/Code/PyAutoLabs-wt/organ-remote-block-and-uv-hook-repair
    SCOPE=$(mktemp -d)
    for r in PyAutoMind PyAutoBrain PyAutoHeart PyAutoHands; do ln -s "$WT/$r" "$SCOPE/$r"; done
    python3 "$WT/PyAutoMind/scripts/repos_sync.py" --write --root "$SCOPE"
    python3 "$WT/PyAutoMind/scripts/repos_sync.py" --check --root "$SCOPE"

    The acceptance bar in "Scope" is unchanged — it just has to be measured through $SCOPE, not through the worktree root.

  2. Jammy2211 commented on Aug 27, 2026

    @Jammy2211
    CollaboratorAuthor

    Library PRs created — shipped under an explicit Heart-RED override.

    Repo PR Contents
    PyAutoMind #363 canonical hook (leg 3b), bootstrap rewiring, 4 new tests, regenerated copy
    PyAutoBrain PyAutoLabs/PyAutoBrain#308 regenerated hook copy
    PyAutoHeart PyAutoLabs/PyAutoHeart#190 AGENTS.md markers + regenerated hook
    PyAutoHands PyAutoLabs/PyAutoHands#270 AGENTS.md markers + regenerated hook

    Merge order is Mind first — the three copies must never be ahead of the canonical they are generated from.

    Suites: PyAutoMind 271, PyAutoBrain 584, PyAutoHeart 656, PyAutoHands 430 — all green.

    The Heart gate, and the override

    pyauto-heart readiness was RED at ship time. Verbatim:

    RELEASE READINESS  ✗ RED  score 40
      ✗ release validation FAILED (stage integrate)
      ! workspace validation not passing (0 failed, 1 timeout, cloud#32902243623:
          autolens_test scripts/multi_dataset/shared_preloads.py)
      ! manifest drift: session-start hooks (generated) — 34 mismatch(es) vs PyAutoMind/repos.yaml
      ! PyAutoFit: open PR 8d old
    

    The single RED reason is release validation FAILED (stage integrate) — PyAutoLabs/PyAutoHeart run 33073386315, a workflow_dispatch "Release Integrate" from 2026-08-27 12:45 UTC that failed on workspace script runs (autofit/plot, autolens_test/imaging, autolens_test/multi_dataset), not on anything in this change. ship_library's corrective-PR exception covers only a fix scoped to the RED reason, which this is not — so this was shipped and merged on an explicit human override, recorded here rather than left implicit.

    Worth noting: the third YELLOW is this task's own subject. Heart already sees the 34-mismatch hook drift that CI is structurally blind to (firewall_gate.yml checks out four organs and skips absent repos). This change does not reduce that count — the four organs stay clean, the other 30 stay stale — which is what the follow-up prompt is for.

  3. Jammy2211 commented on Aug 27, 2026

    @Jammy2211
    CollaboratorAuthor

    Shipped. All four PRs merged, in canonical-first order.

    Repo PR Merge commit reachable from origin/main
    PyAutoMind #363 1dce71d8 ✓
    PyAutoBrain PyAutoLabs/PyAutoBrain#308 d9e1ad62 ✓
    PyAutoHeart PyAutoLabs/PyAutoHeart#190 0ab4f0bd ✓
    PyAutoHands PyAutoLabs/PyAutoHands#270 b151636c ✓

    Proved with git merge-base --is-ancestor against unshallowed clones, per repo — not from the PR state alone.

    CI: every run and every matrix leg green. Mind privacy pass (drift is if: github.event_name != 'pull_request', skipped by design); Brain 3.12 + 3.13; Heart 3.12 + 3.13; Hands 3.12 + 3.13 + 3.14. firewall_gate.yml correctly did not fire — it is path-filtered to scripts/repos_sync.py, which this task deliberately left untouched.

    What landed

    1. All four organs now take the remote-session block from policy/remote_sessions.md, up from two. Heart and Hands lost 23 and 28 hand-written lines each, including a trigger (No module named pytest) that the canonical text records as having stopped firing when the image moved to 3.12.
    2. repair_uv_tools is leg 3b of the hook, so a single-repo session — which runs the hook and never the bootstrap — no longer ends with mypy, flake8, black, poetry and pyright all dead from the breakage leg 2 caused on the way past.

    Three things found along the way

    • The hook is generated into 34 repos, not four. write_session_hooks walks the whole manifest. Thirty copies are a pass behind and two carry none; firewall_gate.yml checks out four organs and skips absent repos, so no gate sees it. Heart does (manifest drift: … 34 mismatch(es)). Filed as draft/maintenance/organs/session_hook_reaches_only_four_of_thirty_four_repos.md.
    • The set -e gap was real. Moving a function from a set -u script into a set -euo pipefail one made two command substitutions, and a failing ln -sfn, into whole-session-start aborts. All three are now non-fatal, matching every other leg's contract.
    • --write --root <task worktree> is not a scoping mechanism. worktree_create symlinks unclaimed repos back to the canonical checkouts and Path.is_dir() follows symlinks, so it would have regenerated hooks into the live main workspace on main. Corrected in the plan comment above; the regeneration ran through a scratch root holding only the four claimed repos, and all 34 canonical checkouts were walked afterwards to confirm nothing leaked.

    Shipped under an explicit human override of Heart RED (release validation FAILED (stage integrate)), unrelated to this change and recorded in the previous comment.

  4. added a commit that references this issue on Aug 27, 2026
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