From 8a3924ea8de190ee5f2e3f978af0f91da8a63acd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 18:23:09 +0000 Subject: [PATCH 1/8] prompt: split staleness-detection leg 3 into its own PyAutoBrain prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit draft_staleness_detection_signals.md is DELIVERED for legs 1 and 2 (the lifecycle.py gate keys and the intake reconcile re-rank). Leg 3 — an upstream-repo read — is the only route to the two findings that leave no Mind-local evidence, and its own delivery note says so. One prompt is one task is one PR, so it is split out rather than re-opening a delivered prompt; the parent now points at it. Filed by hand after the Intake Agent mis-classified the raw text as bug/PyAutoFit — it latched onto the counter-example prose rather than the work. Corrected to feature/pyautobrain; the Feature Agent parses the result as feature/pyautobrain, medium, high, direct, library workflow. Baselines verified at split time and recorded in the prompt: reconcile flags 29 of 136 scanned (7 high); no --repo flag exists in _intake.py's argparse; and nothing under PyAutoBrain/agents/ performs any network access, which is why leg 3's opt-in flag needs an explicit policy line. Also verified the trap the acceptance criterion turns on: all five identifiers named by test_mode_bypass_ordered_assertion_ties.md are present on PyAutoFit main, yet that prompt is confirmed NOT shipped — so the mode must report needs-review with evidence, never a shipped verdict. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- .../reconcile_upstream_repo_mode.md | 144 ++++++++++++++++++ .../draft_staleness_detection_signals.md | 5 + 2 files changed, 149 insertions(+) create mode 100644 draft/feature/pyautobrain/reconcile_upstream_repo_mode.md diff --git a/draft/feature/pyautobrain/reconcile_upstream_repo_mode.md b/draft/feature/pyautobrain/reconcile_upstream_repo_mode.md new file mode 100644 index 00000000..b12849c1 --- /dev/null +++ b/draft/feature/pyautobrain/reconcile_upstream_repo_mode.md @@ -0,0 +1,144 @@ +# `intake reconcile --repo ` — read the target repo, the only route to the findings the Mind cannot see + +Type: feature +Target: PyAutoBrain +Repos: +- PyAutoBrain +- PyAutoMind +Difficulty: medium +Autonomy: supervised +Priority: high +Status: planned + +Leg 3 of `draft/feature/pyautomind/draft_staleness_detection_signals.md`, split +out because that prompt's legs 1 and 2 are **delivered** and one prompt is one +task is one PR. Read the parent for the labelled set and the measurement; this +file is the remaining leg only. Do not re-open the parent. + +## Why this leg exists, and why it cannot be skipped + +The parent's § Hard limit is the argument. Of the five confirmed findings from +the 2026-08-09 `draft/` sweep, **two left no Mind-local evidence at all**: + +- `rectangular_adapt_constant_split_guard` — its evidence sat inside a sibling + *prompt*, not a completion record, so nothing that reads `complete/` could + reach it. +- `latent_samples_none_on_resumed_fit` — PyAutoFit#1418 fixed it the same day it + was filed and **no completion record was written**. There was nothing in the + Mind to cross-reference. (A record exists now — `complete/2026/07/ + latent-samples-none-on-resumed-fit.md` — but only because a human did the + upstream read by hand. That is precisely the work this leg mechanises, and the + next such fix will land with no record again.) + +The parent proved that no amount of re-ranking Mind-local signals reaches these: +every attempt cost precision without gaining truth. The upstream read is +load-bearing. This leg is that read. + +Current state to build on: `intake reconcile` flags **29 suspects of 135 +scanned** (7 `high`, 22 `medium`) — the post-re-rank numbers holding. The +ranking is not the problem any more; the missing evidence source is. + +## Scope + +Add an opt-in upstream mode to +`@PyAutoBrain/agents/conductors/intake/_intake.py`: + +``` +pyauto-brain intake reconcile --repo [prefix] +``` + +It resolves the target to a GitHub slug, makes a cached shallow clone of that +repo's `main`, extracts the backticked identifiers each prompt names, greps the +clone, and ranks on hits — reporting `file:line` evidence a human can read. + +### Target resolution + +`repos.yaml` in @PyAutoMind carries the `github:` slug for every repo, and +`@PyAutoBrain/config/policy.yaml` already carries `repo_aliases` +(`pyautoarray -> autoarray`, and the sizing faculty's `PyAutoFit -> autofit` +package form). Reuse both — do not add a third mapping. + +**Multi-repo pseudo-targets must be refused, not guessed.** `workspaces`, +`health_fixes`, `priors`, `graphical_ep` are not repos, and they are among the +*largest* buckets in `draft/` (`workspaces` alone spans 22 prompts across four +work-types). `--repo workspaces` must exit non-zero naming the real candidate +repos. Silently picking one would produce confident nonsense on the biggest +part of the backlog. + +### Clone mechanics + +Plain `--depth 1` with `GIT_LFS_SKIP_SMUDGE=1`, cached per-repo and re-fetched +on later runs. Record the resolved sha in the output so a verdict is +reproducible and re-checkable. + +**Correction to the parent prompt:** it suggests `--depth 1 --filter=blob:none`. +Treeless is wrong here — grepping source refetches every blob on demand, so it +is a false economy. Use a plain shallow clone. + +### The signal + +Extract backticked `snake_case` / `CamelCase` identifiers from the prompt and +grep the clone for them. `_IDENT_RE` in `_intake.py` already does exactly this +extraction for the Mind-local leg; reuse it rather than writing a second regex. + +The parent proposes scoping extraction to the prompt's `## Scope` / +`## Acceptance` sections, to catch "identifiers the prompt says do not exist +yet". That rule needs a stated fallback: **the trap prompt below has neither +heading** — its only heading is a dated correction block — so a strictly +section-scoped rule no-ops on exactly the prompts that matter most. + +## The trap — verified, and the acceptance criterion turns on it + +`draft/bug/autofit/test_mode_bypass_ordered_assertion_ties.md` names five +identifiers: `FitException`, `check_assertions`, `ignore_assertions`, +`instance_for_arguments`, `instance_from_vector`. **All five are on PyAutoFit +`main`.** The prompt is confirmed **NOT** shipped: `main` catches +`exc.FitException` in the TEST_MODE bypass, which looks exactly like the +requested fix, but the catch wraps only the likelihood call while +`model.instance_from_vector` — where `check_assertions` actually raises on an +ordering tie — sits on the line *before* the `try`. + +A naive identifier-presence matcher scores this 5 of 5 and calls it shipped. It +would be wrong, and it is the single mis-grade this tool must never make. + +So: **the mode reports `needs-review` with evidence, never a `shipped` +verdict.** It ranks for a human and retires nothing — which is already +reconcile's stated contract (`retiring a prompt stays human`). Keep it. + +## Architectural constraint — this is the Brain's first network access + +Nothing under `@PyAutoBrain/agents/` currently uses `urllib`, `requests`, `gh` +or `git clone`. Every conductor and faculty is stdlib-only and offline. This +leg breaks that invariant, so: + +- the flag is **strictly opt-in**; the default `reconcile` path stays byte-for- + byte offline and read-only; +- the upstream read sits behind an **injectable seam** (a source-reader + callable), so `@PyAutoBrain/tests/test_intake_reconcile_ranking.py` stays + hermetic — every fixture there is a fictional Mind tree in `tmp_path`, and + that property is worth more than integration coverage; +- one opt-in network integration test, gated on an env var, covers the real + clone path; +- `@PyAutoBrain/AUTONOMY.md` gets a line stating the network surface and that it + is opt-in. + +## Acceptance + +- `--repo ` resolves via `repos.yaml` + `policy.yaml` `repo_aliases`, + and **refuses** `workspaces` / `health_fixes` / `priors` / `graphical_ep` + non-zero, naming the real repos. +- Run against PyAutoFit, `draft/bug/autofit/test_mode_bypass_ordered_assertion_ties.md` + is **NOT** reported as shipped, despite all five of its identifiers being + present upstream. Pinned by a test. +- No prompt is moved or retired by the tool. Pinned by a test (the parent + already asserts this for the Mind-local legs). +- The default `reconcile` path performs **no** network access. Pinned by a test. +- The existing hermetic tests still pass unchanged. +- Output records the resolved upstream sha. + + diff --git a/draft/feature/pyautomind/draft_staleness_detection_signals.md b/draft/feature/pyautomind/draft_staleness_detection_signals.md index 15f04c1f..3c558814 100644 --- a/draft/feature/pyautomind/draft_staleness_detection_signals.md +++ b/draft/feature/pyautomind/draft_staleness_detection_signals.md @@ -180,6 +180,11 @@ is already reconcile's stated contract. Keep it. > > Leg 3 remains open and is now better motivated: it is the *only* route to the > last two findings. +> +> **Leg 3 was split out on 2026-08-10** to +> `draft/feature/pyautobrain/reconcile_upstream_repo_mode.md` — one prompt is +> one task is one PR, and legs 1-2 here are delivered. Track leg 3 there; do +> not re-open this prompt for it. - Re-running the ranker against PyAutoMind `f25e154e` (the pre-sweep tree, which is the labelled set this prompt establishes) puts every **Mind-reachable** From 85b6f826ff9b35fcb4d86aa7271b1bb87acae9a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 18:30:19 +0000 Subject: [PATCH 2/8] prompt: file issue for reconcile-upstream-repo-mode (#223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes leg 3 of the staleness work into the dev workflow. PyAutoBrain#223 carries the two-level plan; the prompt advances draft/ -> active/ and is registered in active.md as library-dev. Conflict check clean at registration (worktree_check_conflict exit 0, no other task claims PyAutoBrain or PyAutoMind). The repos: list is left empty per the schema — no branch exists yet, and any 2-space `- Repo` bullet would read as a live worktree claim. Committed by hand rather than via prompt_sync_push: that helper hardcodes `git push origin main`, which is wrong for a branch-scoped session. Mind changes for this task go to claude/automind-task-planning-wxq004. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- active.md | 14 ++++++++++++++ .../reconcile_upstream_repo_mode.md | 0 2 files changed, 14 insertions(+) rename {draft/feature/pyautobrain => active}/reconcile_upstream_repo_mode.md (100%) diff --git a/active.md b/active.md index 111bfa6a..e146c31e 100644 --- a/active.md +++ b/active.md @@ -45,3 +45,17 @@ - classification: both (library first, then workspaces) - repos: - PyAutoGalaxy: feature/profile-validation-resample-recovery + +## reconcile-upstream-repo-mode +- issue: https://github.com/PyAutoLabs/PyAutoBrain/issues/223 +- session: Claude Code cloud session (no local worktree — see note below) +- status: library-dev +- worktree: ~/Code/PyAutoLabs-wt/reconcile-upstream-repo-mode +- prompt: active/reconcile_upstream_repo_mode.md +- branch: feature/reconcile-upstream-repo-mode (not yet created) +- classification: library (PyAutoBrain primary; PyAutoMind docs only) +- split-from: draft/feature/pyautomind/draft_staleness_detection_signals.md — legs 1-2 DELIVERED 2026-08-09, this is leg 3 +- conflict-check: `worktree_check_conflict reconcile-upstream-repo-mode PyAutoBrain PyAutoMind` exit 0 at registration time; no other task claims either repo +- cloud-session-note: registered from a cloud session, where the `~/Code/PyAutoLabs-wt/` worktree model does not exist — the checkouts are /workspace/pyautobrain and /home/user/PyAutoMind. The `worktree:` path above is the laptop path `/start_library` would create; it does NOT exist yet. Mind changes for this task go to branch `claude/automind-task-planning-wxq004`, not to main. +- baselines-at-planning: reconcile flags 29 of 136 scanned (7 high); no `--repo` in `_intake.py` argparse; no network access anywhere under PyAutoBrain/agents/ +- repos: diff --git a/draft/feature/pyautobrain/reconcile_upstream_repo_mode.md b/active/reconcile_upstream_repo_mode.md similarity index 100% rename from draft/feature/pyautobrain/reconcile_upstream_repo_mode.md rename to active/reconcile_upstream_repo_mode.md From 8126dbce3c3b1802c4e7faf19695b34405bae859 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 19:37:45 +0000 Subject: [PATCH 3/8] prompt: register reconcile-upstream-repo-mode branch claim (#223) Records the PyAutoBrain claim per start_library step 4's web-github variant (repos bullet, no worktree: field) now that feature/reconcile-upstream-repo-mode is pushed at 875330f. Corrects the conflict-check line: the run recorded at start_dev time was VACUOUS. worktree_check_conflict resolves active.md under $PYAUTO_MAIN, which defaults to $HOME/Code/PyAutoLabs and does not exist in a cloud session, so it read no file and returned 0 = "no conflict". Re-run with PYAUTO_MAIN=/workspace it confirms the same conclusion for real: the only other live claim is profile-validation-resample-recovery on PyAutoGalaxy, no overlap. The guard failing OPEN is logged as a follow-up. A cloud or CI session running the documented command gets a green light that means nothing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- active.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/active.md b/active.md index e146c31e..b77b2e71 100644 --- a/active.md +++ b/active.md @@ -50,12 +50,15 @@ - issue: https://github.com/PyAutoLabs/PyAutoBrain/issues/223 - session: Claude Code cloud session (no local worktree — see note below) - status: library-dev -- worktree: ~/Code/PyAutoLabs-wt/reconcile-upstream-repo-mode - prompt: active/reconcile_upstream_repo_mode.md -- branch: feature/reconcile-upstream-repo-mode (not yet created) +- branch: feature/reconcile-upstream-repo-mode (pushed, 875330f) - classification: library (PyAutoBrain primary; PyAutoMind docs only) - split-from: draft/feature/pyautomind/draft_staleness_detection_signals.md — legs 1-2 DELIVERED 2026-08-09, this is leg 3 -- conflict-check: `worktree_check_conflict reconcile-upstream-repo-mode PyAutoBrain PyAutoMind` exit 0 at registration time; no other task claims either repo -- cloud-session-note: registered from a cloud session, where the `~/Code/PyAutoLabs-wt/` worktree model does not exist — the checkouts are /workspace/pyautobrain and /home/user/PyAutoMind. The `worktree:` path above is the laptop path `/start_library` would create; it does NOT exist yet. Mind changes for this task go to branch `claude/automind-task-planning-wxq004`, not to main. +- conflict-check: CLEAN, but re-verified — the first run was VACUOUS. `worktree_check_conflict` resolves `active.md` under `$PYAUTO_MAIN` (default `$HOME/Code/PyAutoLabs`), which does not exist in a cloud session, so it read no file and returned 0 = "no conflict". Re-run with `PYAUTO_MAIN=/workspace` it works and confirms the real answer: the only other live claim is profile-validation-resample-recovery on PyAutoGalaxy, no overlap. With this entry's `repos:` bullet in place, a competing task on PyAutoBrain now correctly exits 1 and this same task resuming exits 0. +- GUARD-BUG (worth its own prompt): `worktree_check_conflict` FAILS OPEN when `$PYAUTO_MAIN` does not resolve — it returns 0 rather than erroring. Any cloud/CI session running the documented command gets a green light that means nothing, and two sessions could claim the same repo. Same root cause class as the `prompt_sync_push` hardcoded-`main` issue noted below. +- cloud-session-note: registered from a cloud session, where the `~/Code/PyAutoLabs-wt/` worktree model does not exist — the checkouts are /workspace/pyautobrain and /home/user/PyAutoMind. Per `start_library` step 4 (web-github / ci-only), the repos are claimed below with NO `worktree:` field. Mind changes for this task go to branch `claude/automind-task-planning-wxq004`, not to main. - baselines-at-planning: reconcile flags 29 of 136 scanned (7 high); no `--repo` in `_intake.py` argparse; no network access anywhere under PyAutoBrain/agents/ +- implemented: 2026-08-10 on `feature/reconcile-upstream-repo-mode` (875330f), pushed, NOT yet a PR. 8 new hermetic tests, 330 passed. Verified end-to-end against PyAutoFit fbe9f45d: `test_mode_bypass_ordered_assertion_ties.md` lands in `needs-review` with overlap_score 0.0 despite all five of its identifiers being present upstream — the acceptance criterion, met. +- known-preexisting: `tests/test_skill_install.py` has 2 failures on clean main in this environment (the installer takes the web-github path, so the asserted "SKIP intake (Codex skill" line is never printed). NOT caused by this task; verified by stashing. - repos: + - PyAutoBrain: feature/reconcile-upstream-repo-mode From 6ee7298e8a554958daf566657fb76aa6d14f5a58 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 19:54:14 +0000 Subject: [PATCH 4/8] =?UTF-8?q?prompt:=20reconcile-upstream-repo-mode=20sh?= =?UTF-8?q?ipped=20to=20PR=20(#223=20=E2=86=92=20PyAutoBrain#224)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the open PR and the ship-gate outcome. The documented fallback gate returned RED (Heart is unreachable from a cloud session, so pytest -x is the gate) on two tests/test_skill_install.py failures. A human acknowledged the reason and authorized the PR. CI then vindicated that call: both pytest legs (3.12, 3.13) are SUCCESS on 875330f, so those failures were purely local to this container — install.sh takes its web-github branch when $HOME/Code/PyAutoLabs is absent and never performs the work the test asserts on. They were also proven pre-existing at merge-base a50efc3 with the diff absent. The prompt stays in active/ until merge, when the ship skills advance it to complete/2026/08/. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- active.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/active.md b/active.md index b77b2e71..3d16f6ed 100644 --- a/active.md +++ b/active.md @@ -58,7 +58,9 @@ - GUARD-BUG (worth its own prompt): `worktree_check_conflict` FAILS OPEN when `$PYAUTO_MAIN` does not resolve — it returns 0 rather than erroring. Any cloud/CI session running the documented command gets a green light that means nothing, and two sessions could claim the same repo. Same root cause class as the `prompt_sync_push` hardcoded-`main` issue noted below. - cloud-session-note: registered from a cloud session, where the `~/Code/PyAutoLabs-wt/` worktree model does not exist — the checkouts are /workspace/pyautobrain and /home/user/PyAutoMind. Per `start_library` step 4 (web-github / ci-only), the repos are claimed below with NO `worktree:` field. Mind changes for this task go to branch `claude/automind-task-planning-wxq004`, not to main. - baselines-at-planning: reconcile flags 29 of 136 scanned (7 high); no `--repo` in `_intake.py` argparse; no network access anywhere under PyAutoBrain/agents/ -- implemented: 2026-08-10 on `feature/reconcile-upstream-repo-mode` (875330f), pushed, NOT yet a PR. 8 new hermetic tests, 330 passed. Verified end-to-end against PyAutoFit fbe9f45d: `test_mode_bypass_ordered_assertion_ties.md` lands in `needs-review` with overlap_score 0.0 despite all five of its identifiers being present upstream — the acceptance criterion, met. -- known-preexisting: `tests/test_skill_install.py` has 2 failures on clean main in this environment (the installer takes the web-github path, so the asserted "SKIP intake (Codex skill" line is never printed). NOT caused by this task; verified by stashing. +- status-detail: PR OPEN, awaiting review/merge. On merge the ship skills advance `active/reconcile_upstream_repo_mode.md` to `complete/2026/08/`. +- pr: https://github.com/PyAutoLabs/PyAutoBrain/pull/224 (label `pending-release`, `Closes #223`, 467+/12- across 6 files) +- implemented: 2026-08-10 on `feature/reconcile-upstream-repo-mode` (875330f). 8 new hermetic tests, 330 passed locally. Verified end-to-end against PyAutoFit fbe9f45d: `test_mode_bypass_ordered_assertion_ties.md` lands in `needs-review` with overlap_score 0.0 despite all five of its identifiers being present upstream — the acceptance criterion, met. +- ship-gate: the documented fallback gate (`pytest -x`, Heart unreachable here) returned RED on `tests/test_skill_install.py` (`assert "SKIP intake (Codex skill" in result.stdout`, 2 failures). Human acknowledged and authorized the PR. **CI then vindicated that call**: both `pytest (3.12)` and `pytest (3.13)` legs SUCCESS on 875330f, so the failures were purely local to the cloud container — `bin/install.sh` takes its `web-github / ci-only` branch (`:282`) when `$HOME/Code/PyAutoLabs` is absent, and never does the work the test asserts on. Also proven pre-existing at merge-base a50efc3 with the diff absent. - repos: - PyAutoBrain: feature/reconcile-upstream-repo-mode From 7354a32d71b218b7c3aaec3a6051012642c4047f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 20:09:42 +0000 Subject: [PATCH 5/8] fix: prompt_sync helpers push the current branch, not a hardcoded main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PyAutoMind half of PyAutoBrain#225. Both helpers ended in `git push origin main` regardless of what was checked out. create_issue step 6 and start_dev step 7 both instruct an agent to call them, so a branch-scoped session — a cloud session with a designated branch, or any PR-based flow — that followed the documented steps verbatim pushed Mind straight to main, bypassing review. In this session it was caught only because the helper was inspected before use; the documented path would have violated the branch requirement. Pushing HEAD leaves the single-branch laptop flow byte-identical (there the current branch IS main) while making a branch-scoped checkout do the right thing. A detached HEAD is refused rather than guessed at. prompt_sync_push is restructured to drop its subshell: with the old `( ... ) && push` shape the "no-op if nothing is staged" early return would have fallen through to the push, so a caller with no changes would have pushed anyway. The early return now precedes the push. Tests drive the real script against throwaway repos with real bare remotes, because only that shows WHICH ref actually moved; grepping the source would pass on a script that still pushed the wrong branch by another route. Verified they fail against the pre-fix script — 3 of 5 do, including the detached-HEAD case, where the old script pushed `main -> main`. Also files the prompt for the whole three-defect task. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- ...ev_workflow_helpers_assume_laptop_paths.md | 125 ++++++++++++++++ scripts/prompt_sync.sh | 39 ++++- tests/test_prompt_sync_branch.py | 136 ++++++++++++++++++ 3 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md create mode 100644 tests/test_prompt_sync_branch.py diff --git a/draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md b/draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md new file mode 100644 index 00000000..b1a480b8 --- /dev/null +++ b/draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md @@ -0,0 +1,125 @@ +# The dev-workflow helpers assume a laptop checkout and misbehave without it — one of them fails open + +Type: bug +Target: PyAutoBrain +Repos: +- PyAutoBrain +- PyAutoMind +Difficulty: medium +Autonomy: supervised +Priority: high +Status: planned + +Three defects, one root cause: the dev-workflow helper scripts resolve paths +under `$HOME/Code/PyAutoLabs`, which does not exist in a cloud, web or CI +session. All three were hit in a single session on 2026-08-10 while shipping +PyAutoBrain#224, and each one silently did the wrong thing rather than +complaining. + +`Priority: high` because #2 is a **safety** defect, not an inconvenience: a +guard that returns "all clear" when it cannot read its input is worse than no +guard, because the workflow documents it and skills act on its answer. + +## 1. `prompt_sync_push` hardcodes `git push origin main` + +`@PyAutoMind/scripts/prompt_sync.sh` — the helper every Mind-writing skill is +told to call: + +```bash +prompt_sync_push() { + ... + git commit -m "$subject" && \ + git push origin main ) +} +``` + +`create_issue` step 6 and `start_dev` step 7 both instruct the agent to call it. +A branch-scoped session — a cloud session with a designated branch, or any +PR-based flow — that follows the documented steps verbatim pushes Mind straight +to `main`, bypassing review entirely. In the 2026-08-10 session this was caught +only because the branch requirement was read carefully and the helper inspected +before use; the documented path would have violated it. + +Fix: push the current branch (`git push -u origin HEAD`), or refuse with a clear +error when the checkout is not on `main`. Do not leave this to operator +vigilance — that is the same reasoning that motivated the `pre_build` staging +fix (PyAutoHands#233). + +## 2. `worktree_check_conflict` FAILS OPEN — the important one + +`@PyAutoBrain/bin/worktree.sh` — `worktree_list_claimed` reads `active.md` under +`$PYAUTO_MAIN` (default `$HOME/Code/PyAutoLabs`): + +```bash +if [[ ! -f "$active" ]]; then + return 0 +fi +``` + +No file → returns 0 → `worktree_check_conflict` reports **no conflict**. So in +any environment where the roots are not at the default path, the guard cannot +distinguish "nothing claims this repo" from "I could not read the registry", and +answers the former. + +Reproduced 2026-08-10: `worktree_check_conflict reconcile-upstream-repo-mode +PyAutoBrain PyAutoMind` returned 0 and was recorded as a clean conflict check. +Re-run with `PYAUTO_MAIN=/workspace` it actually worked and returned the same +verdict — but the first answer was worth nothing, and if a conflict HAD existed +the session would have started conflicting work with a green light. + +This is the guard that serialises two agents wanting the same repo. Two sessions +claiming one repo is exactly the class of collision the worktree flow exists to +prevent. + +Fix: fail closed. If the registry cannot be resolved, return non-zero with a +message naming the path it tried, so the caller stops rather than proceeds. A +`--allow-missing-registry` escape hatch is acceptable only if it must be passed +explicitly. + +## 3. `test_skill_install.py` fails locally but passes in CI + +`@PyAutoBrain/tests/test_skill_install.py` — two tests assert on installer output +that only appears on the local-dev path: + +```python +assert "SKIP intake (Codex skill" in result.stdout +``` + +`@PyAutoBrain/bin/install.sh` already knows the difference — it prints +`Environment: local-dev (PyAuto repos detected)` or `Environment: web-github / +ci-only (clone roots on demand)` — and on the web-github path it skips the +skill-symlink work these tests assert on. So they fail in any cloud session and +pass in real CI. + +Consequence, observed: PyAutoHeart is unreachable from a cloud session, so +`ship_library` falls back to `pytest -x` as the ship gate and treats any failure +as RED. These two failures made that gate spuriously RED on PyAutoBrain#224 and +cost a human acknowledgement to override. CI on that PR then went green on both +legs, confirming the diff was never implicated. + +Fix: skip (not fail) when the installer reports the web-github path. The tests +are asserting local-dev behaviour and should say so. + +## The unifying point + +`install.sh` already detects and prints its execution environment, and +`WORKFLOW.md` already names the environments (`local-dev`, `web-github`, +`ci-only`). The helpers should consult that same notion and either adapt or fail +closed. None of the three should silently do the wrong thing. + +## Acceptance + +- `prompt_sync_push` on a non-`main` branch pushes that branch, or refuses; + it never pushes to `main` from a branch-scoped checkout. Covered by a test. +- `worktree_check_conflict` returns **non-zero** when it cannot resolve + `active.md`, naming the path it tried. Covered by a test that points + `$PYAUTO_MAIN` at an empty dir. The "registry present, no claim" case still + returns 0, so the existing behaviour is unchanged where it worked. +- `test_skill_install.py` passes on both paths — skipping, with a reason, when + the installer reports web-github; still asserting fully on local-dev. +- `pytest tests/` is green in a cloud session, so the `ship_library` fallback + gate stops producing false REDs. + + diff --git a/scripts/prompt_sync.sh b/scripts/prompt_sync.sh index 49081433..21a977f4 100755 --- a/scripts/prompt_sync.sh +++ b/scripts/prompt_sync.sh @@ -45,6 +45,28 @@ _prompt_sync_require_repo() { fi } +# Push the checkout's CURRENT branch — never a hardcoded `main`. +# +# Both helpers below used to end in `git push origin main` regardless of what +# was checked out. `create_issue` step 6 and `start_dev` step 7 both instruct an +# agent to call them, so a branch-scoped session (a cloud session with a +# designated branch, or any PR-based flow) that followed the documented steps +# verbatim pushed Mind straight to `main`, bypassing review entirely. +# +# Pushing HEAD leaves the single-branch laptop flow byte-identical — there the +# current branch IS `main` — while making a branch-scoped checkout do the right +# thing instead of the dangerous thing. +_prompt_sync_push_current() { + local branch + branch=$(git -C "$PROMPT_REPO" rev-parse --abbrev-ref HEAD 2>/dev/null) + if [ -z "$branch" ] || [ "$branch" = "HEAD" ]; then + echo "prompt_sync: refusing to push from a detached HEAD in $PROMPT_REPO" >&2 + echo " check out a branch first." >&2 + return 1 + fi + git -C "$PROMPT_REPO" push -u origin "$branch" +} + # Commit and push any new untracked .md files at the repo root or under # category dirs as one "sync new task ideas" commit. Each new file is listed # individually in the commit body so the history shows which prompts arrived. @@ -69,8 +91,8 @@ prompt_sync_new_prompts() { body=$(echo "$untracked" | sed 's/^/- /') ( cd "$PROMPT_REPO" && git add -- $untracked && \ - git commit -m "$(printf 'prompt: sync new task ideas\n\n%s\n' "$body")" && \ - git push origin main ) + git commit -m "$(printf 'prompt: sync new task ideas\n\n%s\n' "$body")" ) && \ + _prompt_sync_push_current } # Stage any modifications under the repo, commit with the given subject, @@ -79,9 +101,12 @@ prompt_sync_new_prompts() { prompt_sync_push() { _prompt_sync_require_repo || return 1 local subject="${1:-prompt: sync PyAutoMind}" - ( cd "$PROMPT_REPO" && \ - git add -A && \ - if git diff --cached --quiet; then return 0; fi && \ - git commit -m "$subject" && \ - git push origin main ) + git -C "$PROMPT_REPO" add -A || return 1 + # Still a no-op when nothing is staged — the early return must come BEFORE the + # push, or a caller with no changes would push anyway. + if git -C "$PROMPT_REPO" diff --cached --quiet; then + return 0 + fi + git -C "$PROMPT_REPO" commit -m "$subject" || return 1 + _prompt_sync_push_current } diff --git a/tests/test_prompt_sync_branch.py b/tests/test_prompt_sync_branch.py new file mode 100644 index 00000000..a2813514 --- /dev/null +++ b/tests/test_prompt_sync_branch.py @@ -0,0 +1,136 @@ +"""`prompt_sync.sh` must push the branch it is ON, never a hardcoded `main`. + +Both helpers used to end in `git push origin main` regardless of what was +checked out. `create_issue` step 6 and `start_dev` step 7 both instruct an agent +to call them, so a branch-scoped session — a cloud session with a designated +branch, or any PR-based flow — that followed the documented steps verbatim +pushed Mind straight to `main`, bypassing review entirely. + +These tests drive the real script against throwaway git repos with real bare +remotes, because that is the only way to observe *which ref actually moved*. A +test that merely greps the source for the string would pass on a script that +still pushed the wrong branch by another route. + +Fictional fixtures only, per `test_spawn_privacy.py` — `tests/**` is KEEP-copied +verbatim into the public template, so nothing here names a real task or prompt. +""" + +import subprocess +from pathlib import Path + +SYNC = Path(__file__).resolve().parents[1] / "scripts" / "prompt_sync.sh" + + +def _git(repo, *args): + return subprocess.run(["git", "-C", str(repo), *args], + capture_output=True, text=True, check=True).stdout.strip() + + +def _repo_with_remote(tmp_path): + """A checkout on `main` with one commit, wired to a real bare remote.""" + remote = tmp_path / "remote.git" + subprocess.run(["git", "init", "--bare", "-b", "main", str(remote)], + capture_output=True, check=True) + repo = tmp_path / "mind" + subprocess.run(["git", "init", "-b", "main", str(repo)], + capture_output=True, check=True) + _git(repo, "config", "user.email", "test@example.invalid") + _git(repo, "config", "user.name", "Test") + (repo / "seed.md").write_text("# seed\n") + _git(repo, "add", "-A") + _git(repo, "commit", "-m", "seed") + _git(repo, "remote", "add", "origin", str(remote)) + _git(repo, "push", "-u", "origin", "main") + return repo, remote + + +def _run(repo, snippet): + """Source the real script against `repo` and run `snippet`.""" + return subprocess.run( + ["bash", "-c", f'set -e; export PROMPT_REPO="{repo}"; ' + f'source "{SYNC}"; {snippet}'], + capture_output=True, text=True) + + +def _remote_branches(remote): + out = subprocess.run(["git", "-C", str(remote), "for-each-ref", + "--format=%(refname:short)", "refs/heads/"], + capture_output=True, text=True, check=True).stdout + return set(out.split()) + + +def test_push_from_a_feature_branch_does_not_touch_main(tmp_path): + """THE defect. On a branch-scoped checkout the helper must move that + branch's ref and leave `main` exactly where it was.""" + repo, remote = _repo_with_remote(tmp_path) + main_before = _git(remote, "rev-parse", "main") + + _git(repo, "checkout", "-b", "claude/some-task") + (repo / "active.md").write_text("# Active\n\n## a-task\n") + + res = _run(repo, 'prompt_sync_push "prompt: a subject"') + assert res.returncode == 0, res.stderr + + assert "claude/some-task" in _remote_branches(remote) + assert _git(remote, "rev-parse", "main") == main_before, ( + "main moved — the helper pushed the wrong branch") + + +def test_push_on_main_still_pushes_main(tmp_path): + """The single-branch laptop flow must be unchanged: there the current + branch IS main, so the fix is a no-op for it.""" + repo, remote = _repo_with_remote(tmp_path) + main_before = _git(remote, "rev-parse", "main") + + (repo / "active.md").write_text("# Active\n") + res = _run(repo, 'prompt_sync_push "prompt: a subject"') + assert res.returncode == 0, res.stderr + + assert _git(remote, "rev-parse", "main") != main_before + assert _remote_branches(remote) == {"main"} + + +def test_push_is_still_a_no_op_when_nothing_is_staged(tmp_path): + """The documented contract. Guards a regression the branch fix can easily + introduce: if the early return moves after the push, a caller with no + changes pushes anyway.""" + repo, remote = _repo_with_remote(tmp_path) + _git(repo, "checkout", "-b", "claude/some-task") + + res = _run(repo, 'prompt_sync_push "prompt: nothing to do"') + assert res.returncode == 0, res.stderr + + assert _remote_branches(remote) == {"main"}, ( + "a no-op call created a remote branch") + + +def test_new_prompts_sweep_also_pushes_the_current_branch(tmp_path): + """The sibling helper carried the same hardcoded push.""" + repo, remote = _repo_with_remote(tmp_path) + main_before = _git(remote, "rev-parse", "main") + + _git(repo, "checkout", "-b", "claude/some-task") + draft = repo / "draft" / "feature" / "widget" + draft.mkdir(parents=True) + (draft / "a_new_idea.md").write_text("# A new idea\n") + + res = _run(repo, "prompt_sync_new_prompts") + assert res.returncode == 0, res.stderr + + assert "claude/some-task" in _remote_branches(remote) + assert _git(remote, "rev-parse", "main") == main_before + + +def test_detached_head_is_refused(tmp_path): + """`push -u origin HEAD` from a detached HEAD is meaningless — say so + rather than guessing a branch.""" + repo, _ = _repo_with_remote(tmp_path) + (repo / "active.md").write_text("# Active\n") + _git(repo, "add", "-A") + _git(repo, "commit", "-m", "second") + _git(repo, "checkout", "--detach", "HEAD") + (repo / "active.md").write_text("# Active\n\nchanged\n") + + res = _run(repo, 'prompt_sync_push "prompt: from detached"') + assert res.returncode != 0 + assert "detached HEAD" in res.stderr From f938c98b5b2486593927ef5b03da4c1ad5481a40 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 20:11:05 +0000 Subject: [PATCH 6/8] =?UTF-8?q?prompt:=20register=20dev-workflow-helpers-l?= =?UTF-8?q?aptop-paths=20(#225=20=E2=86=92=20PRs=20226/178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both PRs open. Records the three defects, the fixes, and a correction to the initial diagnosis of the installer-test failure: it was not the installer's web-github branch, it was PYAUTO_ROOT defaulting to the repo's grandparent, so a clone at lowercase pyautobrain never resolves PyAutoBrain/skills. The issue body was corrected before implementation. PyAutoBrain pytest is now 331 passed with nothing ignored — green in a cloud session for the first time, which retires the false-RED ship gate that cost a human acknowledgement on #224. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- active.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/active.md b/active.md index 3d16f6ed..79aa5f1e 100644 --- a/active.md +++ b/active.md @@ -46,6 +46,22 @@ - repos: - PyAutoGalaxy: feature/profile-validation-resample-recovery +## dev-workflow-helpers-laptop-paths +- issue: https://github.com/PyAutoLabs/PyAutoBrain/issues/225 +- prs: PyAutoBrain#226 (guard + installer tests) · PyAutoMind#178 (prompt_sync) +- session: Claude Code cloud session +- status: library-dev — both PRs OPEN, issue stays open until both land +- prompt: draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md +- branch: PyAutoBrain `feature/dev-workflow-helpers-laptop-paths` (off main, files disjoint from #224 so ordering is free); PyAutoMind `claude/automind-task-planning-wxq004` +- classification: bug (PyAutoBrain primary; PyAutoMind scripts/prompt_sync.sh) +- found: 2026-08-10, three defects hit in sequence while shipping PyAutoBrain#224 — all one root cause, the helpers resolving paths under `$HOME/Code/PyAutoLabs`, which no cloud/web/CI session has. +- fixed: (1) `prompt_sync_push`/`prompt_sync_new_prompts` pushed a hardcoded `main` — now push HEAD, detached HEAD refused. (2) `worktree_check_conflict` FAILED OPEN — now exits 3 `CANNOT VERIFY`, with an explicit `--allow-missing-registry` hatch. (3) `test_skill_install.py` depended on the checkout being NAMED PyAutoBrain under `PYAUTO_ROOT` (default `bin/../..`) — now pins a fixture root. +- diagnosis-correction: the first read of (3) blamed the installer's `web-github` branch. Wrong — the real cause is `PYAUTO_ROOT` defaulting to the repo's grandparent, so a clone at lowercase `pyautobrain` never resolves `PyAutoBrain/skills` and `intake` is never installed. The environment line is a symptom of the same path resolution, not the cause. PyAutoBrain#225's body was corrected before implementation. +- test-evidence: both fixes verified to FAIL against their pre-fix code (Mind 3 of 5 new tests fail pre-fix, including the old script pushing `main -> main` from a detached HEAD; Brain guard exercised across all five paths). PyAutoBrain `pytest tests/` now 331 passed with NOTHING ignored — green in a cloud session for the first time, which retires the false-RED ship gate. +- note: `test_missing_active_md_yields_no_claims` asserted the fail-open behaviour, so it was rewritten to the corrected contract rather than worked around — it was pinning the defect. +- repos: + - PyAutoBrain: feature/dev-workflow-helpers-laptop-paths + ## reconcile-upstream-repo-mode - issue: https://github.com/PyAutoLabs/PyAutoBrain/issues/223 - session: Claude Code cloud session (no local worktree — see note below) From d6130479ed46d749b62d2c7e0611df9d4d98cd82 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 20:12:04 +0000 Subject: [PATCH 7/8] prompt: advance dev-workflow-helpers prompt to active/ (#225) lifecycle.py check flagged DRIFT: the prompt sat in draft/ while active.md listed it as an issued task. This is create_issue step 5, which was skipped when going prompt -> issue -> implementation directly. Caught late because the check ran in an `&& tail -2` pipeline, so the shell saw tail's exit status rather than the check's. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- active.md | 2 +- .../dev_workflow_helpers_assume_laptop_paths.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {draft/bug/pyautobrain => active}/dev_workflow_helpers_assume_laptop_paths.md (100%) diff --git a/active.md b/active.md index 79aa5f1e..4597e0e4 100644 --- a/active.md +++ b/active.md @@ -51,7 +51,7 @@ - prs: PyAutoBrain#226 (guard + installer tests) · PyAutoMind#178 (prompt_sync) - session: Claude Code cloud session - status: library-dev — both PRs OPEN, issue stays open until both land -- prompt: draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md +- prompt: active/dev_workflow_helpers_assume_laptop_paths.md - branch: PyAutoBrain `feature/dev-workflow-helpers-laptop-paths` (off main, files disjoint from #224 so ordering is free); PyAutoMind `claude/automind-task-planning-wxq004` - classification: bug (PyAutoBrain primary; PyAutoMind scripts/prompt_sync.sh) - found: 2026-08-10, three defects hit in sequence while shipping PyAutoBrain#224 — all one root cause, the helpers resolving paths under `$HOME/Code/PyAutoLabs`, which no cloud/web/CI session has. diff --git a/draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md b/active/dev_workflow_helpers_assume_laptop_paths.md similarity index 100% rename from draft/bug/pyautobrain/dev_workflow_helpers_assume_laptop_paths.md rename to active/dev_workflow_helpers_assume_laptop_paths.md From 3119d6d2394a796d525d14734f270c72619dec0b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 20:20:15 +0000 Subject: [PATCH 8/8] fix: correct the prompt_sync diagnosis and cover the failure it actually hits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found auditing this branch against origin/main. The write-up said the old helper "pushes Mind straight to main". Measuring it against real bare remotes shows that is only the half that needs local main to be AHEAD. `git push origin main` pushes the local main REF, never the commit just made on a feature branch. So when local main equals the remote — the normal case for a session that branched from it — the push is a no-op, exits 0, and the committed work never leaves the machine while the caller is told the sync succeeded. In an ephemeral cloud container that is silent loss, and it is the case that fires most often. Publishing unreviewed main commits is the other half. Two tests added for the two halves, since they are separate failures: test_committed_work_actually_reaches_the_remote (the work is ON the remote) and test_a_branch_push_never_advances_main (an unpushed local main commit is not published as a side effect). Both fail against origin/main's script; 5 of the 7 now do. 134 passed. The fix itself is unchanged and already correct — pushing HEAD addresses both halves. This corrects the description and the coverage, not the code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PY39MRi1oPbxhhr6tDToQU --- ...ev_workflow_helpers_assume_laptop_paths.md | 28 ++++++++--- tests/test_prompt_sync_branch.py | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/active/dev_workflow_helpers_assume_laptop_paths.md b/active/dev_workflow_helpers_assume_laptop_paths.md index b1a480b8..9e32483f 100644 --- a/active/dev_workflow_helpers_assume_laptop_paths.md +++ b/active/dev_workflow_helpers_assume_laptop_paths.md @@ -34,11 +34,23 @@ prompt_sync_push() { ``` `create_issue` step 6 and `start_dev` step 7 both instruct the agent to call it. -A branch-scoped session — a cloud session with a designated branch, or any -PR-based flow — that follows the documented steps verbatim pushes Mind straight -to `main`, bypassing review entirely. In the 2026-08-10 session this was caught -only because the branch requirement was read carefully and the helper inspected -before use; the documented path would have violated it. + +**Corrected 2026-08-10 after measuring the old script against real bare +remotes.** The first write-up of this defect said it "pushes Mind straight to +`main`". That is only half of it, and not the half that fires most often. +`git push origin main` pushes the local `main` *ref* — never the commit just +made on a feature branch. So the old behaviour splits by the state of local +`main`: + +| local `main` vs remote | what the old script did | +|---|---| +| **equal** (the normal case — session branched from it) | push is a **no-op**, exits **0**, and the committed work **never leaves the machine**. The caller is told the sync succeeded. | +| **ahead** (unpushed commits on `main`) | publishes those commits to remote `main` — unreviewed work bypassing review — **and still** leaves the branch work local. | + +The first row is the common one and the quieter danger: in an ephemeral cloud +container, "committed, reported synced, never pushed" is silent loss. The second +row is the review-bypass. Both are the same root cause and both are fixed by +pushing `HEAD`. Fix: push the current branch (`git push -u origin HEAD`), or refuse with a clear error when the checkout is not on `main`. Do not leave this to operator @@ -109,8 +121,10 @@ closed. None of the three should silently do the wrong thing. ## Acceptance -- `prompt_sync_push` on a non-`main` branch pushes that branch, or refuses; - it never pushes to `main` from a branch-scoped checkout. Covered by a test. +- `prompt_sync_push` on a non-`main` branch pushes **that branch** — so the + committed work actually reaches the remote — and never advances `main` as a + side effect. Both halves covered by their own test, since they are separate + failures of the same bug. - `worktree_check_conflict` returns **non-zero** when it cannot resolve `active.md`, naming the path it tried. Covered by a test that points `$PYAUTO_MAIN` at an empty dir. The "registry present, no claim" case still diff --git a/tests/test_prompt_sync_branch.py b/tests/test_prompt_sync_branch.py index a2813514..44d3e74c 100644 --- a/tests/test_prompt_sync_branch.py +++ b/tests/test_prompt_sync_branch.py @@ -76,6 +76,54 @@ def test_push_from_a_feature_branch_does_not_touch_main(tmp_path): "main moved — the helper pushed the wrong branch") +def test_committed_work_actually_reaches_the_remote(tmp_path): + """The failure the old script hit MOST often, and the quietest one. + + `git push origin main` from a feature branch pushes the local `main` ref — + not the commit just made. When local `main` already matches the remote (the + normal case for a session that branched from it) the push is a no-op, exits + 0, and the committed work never leaves the machine. The caller is told the + sync succeeded. In an ephemeral cloud container that is silent loss. + + Leaking to `main` is the other half of the same bug and needs local `main` + to be ahead — see `test_a_branch_push_never_advances_main` below. This test + is the common case: the work must be ON the remote afterwards. + """ + repo, remote = _repo_with_remote(tmp_path) + _git(repo, "checkout", "-b", "claude/some-task") + (repo / "active.md").write_text("# Active\n\n## a-task\n") + + res = _run(repo, 'prompt_sync_push "prompt: a subject"') + assert res.returncode == 0, res.stderr + + local_head = _git(repo, "rev-parse", "HEAD") + assert _git(remote, "rev-parse", "claude/some-task") == local_head, ( + "the commit never reached the remote") + + +def test_a_branch_push_never_advances_main(tmp_path): + """The other half: an unpushed commit sitting on local `main` must not be + published as a side effect of syncing a feature branch. + + The old script pushed it — unreviewed work onto `main`, while the branch + work it was actually asked to sync stayed local. + """ + repo, remote = _repo_with_remote(tmp_path) + (repo / "secret.md").write_text("unreviewed local work\n") + _git(repo, "add", "-A") + _git(repo, "commit", "-m", "local main work") + main_before = _git(remote, "rev-parse", "main") + + _git(repo, "checkout", "-b", "claude/some-task") + (repo / "active.md").write_text("# Active\n") + res = _run(repo, 'prompt_sync_push "prompt: a subject"') + assert res.returncode == 0, res.stderr + + assert _git(remote, "rev-parse", "main") == main_before, ( + "an unpushed local main commit was published as a side effect") + assert "claude/some-task" in _remote_branches(remote) + + def test_push_on_main_still_pushes_main(tmp_path): """The single-branch laptop flow must be unchanged: there the current branch IS main, so the fix is a no-op for it."""