Repository navigation
resume and status hold each leg to the branch bootstrap puts it on: the leg row's own branch:, else the root's tracking_branch (#235) - #238
Conversation
…he leg row's own branch:, else the root's tracking_branch (#235) root_legs_ok compared every leg with the ROOT's tracking_branch, but the root's bootstrap.py places a leg on str(leg.get("branch") or branch). So a leg that declares its own branch: develop was let through while bootstrap moved it (exit 0), and refused while it sat on develop. status passed the root's branch to read_leg and had the same blind spot. A shared leg_branch_of reads a leg row's own branch: the way legs_of reads the rows. resume refuses a leg off that branch (naming the leg, where it is, where it belongs and whether that is its own branch: or the root's tracking_branch); status gives each leg's row, and so read_leg, that branch. A leg with no branch: of its own is held to the root's branch exactly as before. The comment above root_legs_ok now says what bootstrap does with a leg's own branch:, including openRepoShape v1.1.0's fast-forward and checkout of a branch behind the pin (opensoft/openRepoShape#615). Fixes #235 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 2 days and 12 hours by commenting |
Reviewer's GuideThe PR fixes branch selection for resume and status by matching each leg to the branch bootstrap will use—its row-level branch when declared, otherwise the root's tracking_branch—through a shared resolver, clearer diagnostics, and regression tests covering both declared and fallback cases. File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
brettheap
left a comment
There was a problem hiding this comment.
Independent review of #238 at fefeaca: LAND after fixes
The rule is the right one and the behaviour is correct for every manifest the scaffold writes. I found no blocking defect. Finding 1 should be fixed before this lands. The rest can go in the same push or be filed.
Run at fefeaca, submodule at 190133d (v1.1.0), with every LANES_* and TMUX* variable unset and a private HOME, TMPDIR, basetemp and pycache, under the pytest lock:
- The 5 new tests pass on the head and all 5 fail on
f595e44'spark/resume/status. On the old scripts (b) exits 0 over[spec] spec: on develop at <sha> (branch tip == pin), and (d1) reports 0 findings. (c) fails on the old scripts only through the new parenthetical, as the PR says. tests/test_repo_hygiene.pypasses (189). The three resolver blocks have the same sha256.shellcheckis clean on all three files.- No bash 3.2 was available. I ran bash 5.2 and read the new code for 3.2: no
${x,,},mapfile,declare -A,local -nor multi-lineawk -v. The#inside${branch%%[[:space:]]#*}is not at a word start, so 3.2's$()parsing is not affected. - A full run of the two touched modules was cut at about 84% by my own timeout, with no failure shown. I did not complete it. CI's
testsandtests-no-submodulewere still pending when I looked, and CI is the record.
Findings
1. should-fix: nothing tests leg_branch_of's parsing, which the PR body and code comment claim.
All 5 tests use branch: develop on the line after path: spec. The comment and PR body claim four more things: branch: on either side of path:, an empty value, null/~, and a trailing # comment cut off. None of them is exercised.
I proved it with a mutant, applied identically to all three copies so byte-identity still holds. It dropped the comment cut, dropped the null/~ case, and reset branch when path: is read. That makes branch: develop # c return develop # c, branch: null return null, and branch-before-path return nothing. Result: the 5 new tests, test_repo_hygiene.py (189) and the 72 existing leg and refusal tests in test_park_resume_commands.py all still pass.
There is also no mixed case, one leg with its own branch: and one without. It works. I ran root_legs_ok over a two-leg root with tracking_branch: trunk and a branch: develop spec leg: both legs where they belong passed, and a code leg on develop was refused as "not its tracking branch 'trunk'". It is simply unguarded.
Fix: add a table-driven test that sources the shared block (sed -n '/BEGIN shared estate resolver/,/END shared estate resolver/p' resume) into bash and asserts leg_branch_of over:
| row | expected |
|---|---|
branch: develop after path: |
develop |
branch: develop before path: |
develop |
branch: "develop" |
develop |
branch: develop # own |
develop |
empty branch: |
nothing |
branch: null, branch: ~ |
nothing |
branch: # none |
nothing |
another row's branch:, and the assembly row's |
nothing |
two rows whose paths share a prefix (spec, spec-extra) |
the exact row only |
Then add one two-leg resume case.
2. should-fix: status and resume look a leg up under different path spellings.
status finds the leg's row by its .gitmodules path (declared_legs, then leg_branch_of "$root" "$leg"). resume uses the path: text from project.yaml (legs_of). The two are compared by exact string. validate-manifest accepts path: "./spec" and spec/ (only absolute paths, .. and . are refused), and status's own comment above physical_path says "./spec against spec is the same leg and two strings".
Repro: branch: develop, path: "./spec", .gitmodules path spec, leg on main.
status:leg main <leg>thenat its pin, which is origin/main's tip as of the last fetch; clean. This is the blind spot the PR sets out to close.resume(root_legs_ok):REFUSED R: leg ./spec is on 'main', not its own branch 'develop' (...), rc=1.
Fix, in the shared block so all three copies move together: normalise both sides in leg_branch_of before comparing (p="${path#./}"; p="${p%/}", the same for want). Or document the limitation next to leg_branch_of.
3. should-fix: the operator-facing text still states the old rule, and two comments now say something untrue.
resume --help(resume:99-102): "a root one of whose LEGS is off its tracking branch".AGENTS.md:79-82, driving rule 1: "would walk the leg back onto its tracking branch".status:703-704still saysestate_reposis "THE SAME ENUMERATIONpark's not-carried report makes". It no longer is: the branch column for a leg now differs.park:584-585still says "A LEG'S branch is the ROOT'stracking_branch:— that is the one valuescripts/bootstrap.pyplaces every leg on".
Fix: reword the first two to "off the branch bootstrap places it on (its row's branch:, else the root's tracking_branch)". Reword the two comments, with a pointer to leg_branch_of. The PR body says park's report_repos is "left alone" but names no issue. File one and link it. It is a data-loss-adjacent report: for a leg on its own branch, park counts unpushed commits against the root's branch, not the leg's.
4. note: for a few scalars the shell and the loader disagree, so the comment's claim is not exactly true.
The comment says "what the loader would call no branch is none here too". I checked repo_shape.parse_yaml and bootstrap.py's str(leg.get("branch") or branch):
| YAML | loader gives | shell gives |
|---|---|---|
"null", '~' |
the branch null, ~ |
none |
false, 0 |
falsy, so the root's branch | the branch false, 0 |
true |
True |
true |
No real branch is named that, so this is cosmetic. Soften the comment to "null, Null, NULL, ~ unquoted, empty, or a comment".
5. note: a row that does not open with - role: does not reset branch.
This is the same limitation legs_of has for path:, but branch: is new. Probe: - role: spec / path: spec, then - path: other / branch: leak, with want=spec, returns leak.
The scaffold template always opens a row with - role:, so this is not reachable from a generated manifest. Hardening, if wanted: add a ' - '*) arm that clears role, path and branch before the more specific arms. That is one line in the shared block, and it changes nothing for role:-first rows.
Checked and not findings
Each is what I looked for in the checklist.
resume's refusal. It comes beforepull_ff_onlyandmake bootstrap(refresh_root). (b) asserts exit 1, the exact refusal line, HEAD name and sha unchanged, and that the verb did not run. Then it runs the realbootstrap.pyby hand to show it would have moved the leg. Detached at the pin still passes. The detached-off-pin, dirty and root-branch checks are unchanged in the diff.statusconsumers. The row header, the fetch line (fetch_estate_first,fetch_remote),read_fork,ask_github_fork,read_tracking,read_branches,no_origin_branch_findingand the "at its pin ... origin/'s tip" line all take the row's branch, so they are right for a leg with its ownbranch:. For a leg without one, or a non-leg.gitmodulesentry,leg_branch_ofreturns nothing and the column is the root's, as before.holder,rootandmissingrows are untouched.- A pin ahead of
origin/<own branch>. With a declared branchdevelopbehind the pin,statusnow sayspin <sha> is not on origin/develop: 1 commit(s) unpushed in the leg. That is correct for v1.1.0's fast-forward, which leaves localdevelopahead oforigin/develop. - Hygiene and body. The diff has no host-absolute path. The one
~/is the existingworkspace.yamlfixture line. The body has "Fixes #235" only, and "Bug Fixes:" is Sourcery's heading with no issue number after it. The openRepoShape references areopensoft/openRepoShape#603and#615. - The v1.1.0 bootstrap behaviour the new
resumecomment describes. I readfast_forward_to_pinandmove_back_to_pinin the mountedbootstrap.py, and the comment matches them.
🤖 Generated with Claude Code
…ind a leg by either spelling of its path, and fix the stale text (#235) Review 5561239715 of #238: 1. leg_branch_of's parsing claims (branch: on either side of path:, an empty value, null and ~, a trailing # comment, a row with one branch: beside one without, prefix-sharing paths) are now a table of 22 cases run against the real function cut out of resume, plus a two-leg resume case. 2. status looked a leg up by .gitmodules' path and resume by the manifest's text, so a row spelling its path "./spec" or "spec/" was a leg on its tracking branch to status. leg_branch_of, in the shared block, drops a leading ./ and a trailing / from both sides before it compares them. 3. resume --help, AGENTS.md rule 1 and the comments in status and park no longer say a leg is held to the root's tracking branch; park's report_repos keeps that assumption, filed as #242. Also: a row that does not open with `- role:` clears the row before it, and the comment says where the loader and this reader differ. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Applied the independent review 5561239715 at 9be1a44: findings 1-3 (a 22-case table and a two-leg case for |
|
brettheap
left a comment
There was a problem hiding this comment.
Delta re-check of #238 at 9be1a44: LAND
Findings 1, 2, 3 and 5 of review 5561239715 are fixed, and finding 4's comment is softened. I found nothing new. Landing still needs CI's tests and tests-no-submodule on 9be1a44 green; both were pending when I looked.
Finding 1, parsing tests.
shared_functionreadsresumeas it is at test time, so it cannot hold stale code. It usesstr.index, so a renamed or missing function fails the test with aValueErrorinstead of passing silently.- It cuts at the first
\n}\n. Bothtrim_unquoteandleg_branch_ofend at column 0, and there is no inner column-0 brace. - The other two copies are covered by the existing byte-identity test.
- I re-applied my three mutants identically to
park,resumeandstatus. Each now fails:- comment cut dropped: 3 failures (
a-trailing-comment-is-cut,a-trailing-comment-after-quotes-is-cut,only-a-comment-is-no-branch) null/Null/~case dropped: 3 failuresbranchreset at thepath:line: 1 failure (before-path)
- comment cut dropped: 3 failures (
- I added three more mutants. Dropping the
./and trailing/normalisation gives 5 failures. Dropping the new row-clear arm gives 1 failure. Makingleg_branch_ofignore which path was asked gives 5 failures, includingtest_each_leg_of_one_root_is_held_to_its_own_branch. test_each_leg_of_one_root_is_held_to_its_own_branchfails onf5c3bcd's scripts. It passes with spec ondevelopand code onmain. It then refuses code ondevelopby name, leaves spec alone, and leaves both HEADs and branches unchanged.
Finding 2, ./spec and spec/.
- The new
test_a_leg_spelled_dot_slash_or_with_a_slash_is_one_leg_to_status_and_resumeis my probe as a test.statusprintsleg develop <leg>and the "on branch main, not develop" finding.resumerefuses with exit 1, and HEAD is unchanged. It fails on the pre-PR scripts. - The normalisation strips one leading
./and one trailing/from both sides. - Probes show it cannot join two different legs:
../spec,sub/spec,.//spec,spec//and./sub/../specdo not equalspec.../specstill matches itself.
- A path of
.is unchanged. The assembly row never emits, andresumenever asks for.. - The shared block has the same sha256 in all three scripts, and
tests/test_repo_hygiene.pypasses.
Finding 3, stale text. resume --help, AGENTS.md rule 1, the status comment above estate_repos and the park comment in report_repos now describe the real rule and point at #242. A grep for the old wording finds no remaining copy.
Merge 7ae0f46 (main f5c3bcd).
- It touches only
lane-start,skills/handoff/SKILL.md,tests/test_lane_helpers.shandtests/test_skill_shell_blocks.py. git diff f5c3bcd 7ae0f46is byte-for-byte the previous PR diff (git diff f595e44 fefeaca; sha256398a799b…for both onceindexlines are dropped). So it adds nothing topark,resume,statusor the test files.- The writer's own commit touches only AGENTS.md,
park,resume,statusandtests/test_park_resume_commands.py.
#242 is accurate. Against f5c3bcd's park: report_repos is at lines 526-548, the comment at 536-537, unpushed_on_tracking at 559 and not_parked_report at 579. The unpushed line prints the branch. The suggested fix works with the new normalisation. The test it asks for would fail without the change.
Runs at 9be1a44, submodule at 190133d, with every LANES_* and TMUX* variable unset, private dirs, under the pytest lock:
- The 30 new and earlier branch tests pass.
tests/test_repo_hygiene.pypasses (189).tests/test_park_resume_commands.pypasses in full (164 tests).tests/test_status_command.pypasses in full (202 tests). This completes the full-module run I cut short last time.shellcheckis clean on all three scripts.- No bash 3.2 was available. The new constructs (
${want#./},${want%/}and one morecasearm) are plain parameter expansion and glob patterns, as the old code uses.
🤖 Generated with Claude Code
…AGENTS.md line net zero The independent review of 016a07c: - M1: README said the settings file is 600 "on every run". It is 600 on every run that writes it or finds an entry of ours in it. A deferred run with nothing of ours to add or take out leaves the file, and its mode, as they were. The install paragraph no longer claims 600 for the person's own hooks. - S1: the `wip init` paragraph gives the exception where it gives the exit codes: a hook reconcile it cannot make is printed, and the exit stays 0. - N8: ruling 9's condition, "and `~/projects/xFactory` exists". - N3: "the launcher's probe, plus an execute bit", where it said "the launcher's own rule". - S2: AGENTS.md's line is reworded for a `--install` that can now take our guard entry out ("Shared hooks stay installed while `~/projects/xFactory/lanes-edit.sh` answers `guard`; until then they are deferred and our guard entry taken out"). It is back to main's 321 lines, so #238's two lines still fit under the cap of 323. README stays at main's 485 lines. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y/lanes-edit.sh answers guard, and take our guard entry back out until then, so a fresh install never leaves a prompt hook calling a missing command (#237) (#247) * Give the hook tests a link_xfactory fixture before the #237 gate lands `link_xfactory(home)` makes `~/projects/xFactory/lanes-edit.sh` the way `link-estates` leaves it: a symlink to the bin directory's `lanes-edit.sh`. The eleven tests in tests/test_install_skill_and_hook.py that assert the SessionStart entry and the UserPromptSubmit name guard are placed, and test_installing_twice_changes_nothing's thirty-one `unchanged`, link it first, so they keep asserting the merged pair once `--install` merges it only for a target that answers `guard`. `ALWAYS` (29) is named beside `ARTIFACTS` (31) for the count the gate makes conditional. tests/test_install_payload_source.py's test_r12_an_adopted_install_keeps_modes_receipts_targets_and_what_is_not_ours makes the same link to its own bin directory's `lanes-edit.sh`, since it asserts the guard entry is merged (the lead's ruling 7). The fixture changes nothing on the current code, which ignores the link. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge the #237 hook pair only once ~/projects/xFactory/lanes-edit.sh answers `guard`, in --install and in wip init A fresh `--install` merged the SessionStart entry and Amendment 12's UserPromptSubmit name guard before anything had placed the path both call, `~/projects/xFactory/lanes-edit.sh`, which `link-estates` links and only `openRepoTools wip init` runs, after the install (opensoft/openBenches#107). The guard then failed every prompt with exit 127 instead of refusing with 2. Approach (A), the claude-profile launcher's own rule, in both writers: - `--install` (`plan_hook_merge`, `place_hook_pair`) stages both outcomes before anything is placed and picks one after the bin loop. The pair is merged only when that path exists, is executable and answers `guard`, by the launcher's awk probe (claude-profile:404-411), plus `-x`. - When it does not answer, nothing is added, and our EXACT guard entry is removed from a file that carries it, filtered on each entry's nested array (workBenches#89, #90). The SessionStart entry, every foreign hook and every other key stay; the file is written back at mode 600. - Every deferral and every removal is printed with its remedy. - `wip init` reconciles the pair in step 10, after `link-estates`, and on step 1's "nothing to do" path, where it first re-runs `link-estates` if the path does not answer and ~/projects/xFactory exists. `--dry-run` prints "would merge". A refusal there is a note and the exit stays 0. THE CONTRACT CHANGE: `--install` places 29 artifacts always, plus the two hook entries once that path answers `guard` (it was 31 unconditionally). Unchanged: the hook strings and their exact-string identity, the rival refusal (still refuses while the pair is deferred), the receipt (no hook rows), the launcher contract, and the guard's real exit 2 (no `|| true`). tests/test_install_hook_gate.py holds it: fresh, interrupted, dangling-link migration, a target with no `guard` verb or no execute bit, the bin-loop ordering, rivals while deferred, `wip init` after `link-estates`, on its nothing-to-do path and after a cut, `--dry-run`, a repeat install, and the installed guard's real exit 2 before and after the workspace exists, and `--install` with no `jq` still ending in the `jq` refusal whether or not the target answers (006's FR-018). Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the #237 hook gate in README's install paragraph, its wip init note and AGENTS.md README: the install paragraph says both entries wait for the path they call, what a deferred run prints and removes, and the remedy; the count line reads "Twenty-nine artifacts always, plus the two hook entries once the path they call answers `guard`"; the `wip init` note and its idempotence paragraph say it completes the pair after `link-estates`. AGENTS.md's "Shared hooks stay installed" line carries the same count. No line is added to README (485, cap 486); AGENTS.md gains one (322, cap 323). Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a guard entry #237 cannot take apart before anything is placed, and pin wip init's exit-0 reconcile The independent review of 016a07c, M2: a guard entry in a shape the strip filter does not take apart (a `UserPromptSubmit`, or an entry's `hooks`, that is not an array), against a target that does not answer, was refused only at placement, after the 29 artifacts were placed. The refusal is now decided in `plan_hook_merge`, with "NOTHING was installed", like every other file the merge cannot handle. The one target the bin loop can still repair, a link to the bin directory's own `lanes-edit.sh`, is let through, and the run merges once that loop has put the far end back. `hook_pair_defer` keeps the same refusal as a backstop. S1: `wip init` keeps exit 0 when the hook reconcile is refused, on step 10's path and on step 1's, under one rule. `--help` now says so where it gives the exit codes: "a hook reconcile it cannot make is printed, and the exit stays 0". It also gives ruling 9's condition, "and ~/projects/xFactory exists". NITs taken: - N2: an executable that cannot be read is named "is not readable", and a link that dangles or loops "does not resolve". - N6: with no ~/projects/xFactory, the remedy first says that directory has to exist. Tests: - the odd shape now asserts nothing is placed; - a new case: the odd shape beside a link the bin loop repairs is merged; - a new parameter: a target that is not readable; - S1: wip init's refused reconcile on both paths; - N7: step 1 does not relink when the target answers or ~/projects/xFactory is absent; - M1: a deferred run leaves a file of the person's own at its 644. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Correct #237's README mode sentence and wip init exits, and make its AGENTS.md line net zero The independent review of 016a07c: - M1: README said the settings file is 600 "on every run". It is 600 on every run that writes it or finds an entry of ours in it. A deferred run with nothing of ours to add or take out leaves the file, and its mode, as they were. The install paragraph no longer claims 600 for the person's own hooks. - S1: the `wip init` paragraph gives the exception where it gives the exit codes: a hook reconcile it cannot make is printed, and the exit stays 0. - N8: ruling 9's condition, "and `~/projects/xFactory` exists". - N3: "the launcher's probe, plus an execute bit", where it said "the launcher's own rule". - S2: AGENTS.md's line is reworded for a `--install` that can now take our guard entry out ("Shared hooks stay installed while `~/projects/xFactory/lanes-edit.sh` answers `guard`; until then they are deferred and our guard entry taken out"). It is back to main's 321 lines, so #238's two lines still fit under the cap of 323. README stays at main's 485 lines. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brett Heap <service@opensoft.dev> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… this PR on today's main No conflict: of the three commits since 2cb14a9, #238 and #240 touch only park, resume, status and their tests, and #213 touches lanes-edit.sh (managed_seam_refuse, managed_local_hint, managed_local_remedy, lane_trees_list and the managed-projection and lane-trees verbs), docs/README-lanes.md and tests/test_lane_helpers.sh (its managed-seam cases) only away from this PR's functions and hunks, and all three shared files merged as exactly main's version plus this PR's own change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch its own row declares, else the root's tracking_branch (#242) (#249) * park's not-carried report counts a leg's unpushed commits on the branch its own row declares, else the root's tracking_branch (#242) report_repos gave every leg's row the ROOT's tracking_branch, but the root's bootstrap.py places a leg on str(leg.get("branch") or branch). A leg whose row in project.yaml declares its own branch: keeps its unpushed work on THAT branch, so not_parked_report asked about the wrong one, found nothing ahead and ended without naming the leg: a silent miss in the report that exists to say what park did not carry. Both loops of report_repos (the standalone root and a family member's) now take each leg's branch from leg_branch_of, the shared helper #238 gave status and resume, and fall back to the root's branch on an empty answer, as status's estate_repos does. The false comment says the leg row's own branch:, else the root's tracking_branch, and points at #235 and #238. The shared resolver block is untouched. Four tests beside the not-carried report's: a standalone root's spec leg on branch: develop, the same inside a family member's clone, a leg with no branch: unchanged, and one root with a leg of each kind. The first, second and fourth fail on main's park and pass with this change. Fixes #242 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Apply the independent review of #249: survive an unreadable member manifest, pin the family loop's fallback, and fix status's comment about park (#242) Review 5563423499 of #249: 1. status's estate_repos header said park's report gives every leg the root's branch. It now gives every leg the same column as status does. 2. report_repos runs under set -e in a process substitution, and leg_branch_of fails when a project.yaml that exists cannot be read, so the bare assignment ended the subshell and dropped that leg's row and every row after it, with no word from park. Both loops now say || true and fall back to the root's branch; a test makes a family member's project.yaml mode 000 (skipped as root, which mode 000 does not bind) and fails without the guard. 3. The family loop's fallback to the member's tracking_branch was reached by no test: the member of test (b) now has a code leg declaring no branch:, and the report must name it on main. That fails with the family printf using "$legbranch" with no fallback. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brett Heap <service@opensoft.dev> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>



Fixes #235
The defect
resume'sroot_legs_okis F3 of the review on #83: it refuses a root whose leg sits off the branch the root's ownmake bootstrapwould move it onto. It took that branch from the ROOT (tracking_branch_of "$root") for every leg. Buttemplates/assembly-root/scripts/bootstrap.pyplaces a leg onstr(leg.get("branch") or branch): thebranch:the leg's own row inproject.yamldeclares, and the root'stracking_branchonly when the row declares none. So for a leg with its ownbranch:the guard reasoned about the wrong branch, in both directions:mainwhose row saysbranch: developwas moved ontodevelopby bootstrap andresumeexited 0;developwas refused as "not its tracking branch 'main'".statushad the same blind spot: it passed the root's branch toread_leg.The gap is older than v1.1.0 (it happens with the bootstrap at
7f84ca4too), and opensoft/openRepoShape#603 → opensoft/openRepoShape#615 widens it: a declared branch BEHIND the pin is now fast-forwarded and checked out, from that branch, from a HEAD detached at the pin, or from another branch.The design
leg_branch_of <root> <leg path>, new, in the shared estate resolver (so all three copies stay byte-identical;parkcarries it unused, likelegs_of). It reads a leg row's ownbranch:the waylegs_ofreads the same rows (role:beforepath:, the assembly row skipped), except thatbranch:may sit on either side ofpath:, and a row that does not open with- role:clears what the row before it left. It prints nothing when the row declares none. No branch is: an empty value,null/Null/NULL/~, once a trailing# commentis cut off. A leg is found by its path SPELLED EITHER WAY: a leading./and a trailing/come off both sides before they are compared, becausestatusholds.gitmodules' path andresumethe manifest's own text (the review's finding 2). The loader is not matched to the letter for a quoted"null"/'~'orfalse/0; no real branch is named so, and the comment says so.root_legs_okholds each leg toleg_branch_of, else the root'stracking_branch, which is bootstrap'sor. The refusal names the leg, the branch it is on, the branch it should be on and where that comes from:leg spec is on 'main', not its own branch 'develop' (thebranch:of its row in project.yaml);leg spec is on '001-my-feature', not its tracking branch 'main' (the root'stracking_branch);Detached at the pin is still not a refusal, and the dirty and every other check are untouched. A leg with no
branch:is refused exactly as before; only the parenthetical is new.The comment above
root_legs_oksays what bootstrap does with a leg's ownbranch:, including bootstrap.py: a leg on another branch whose tracking branch is behind the pin is fast-forwarded and checked out as one at the pin already is, and the family holder's AGENTS-shape.md and bump-leg.py say bootstrap places a leg where that loses no commit (#603) openRepoShape#615's fast-forward and checkout of a branch behind the pin, and the move back to the pin.status:estate_reposgives each leg's row its ownbranch:(else the root's), so everything read under that row (the pin againstorigin/<branch>, the leg's fork check, the fetch line, the "at its pin, which is origin/'s tip" line) is about the branch the leg belongs on, andread_leg's finding names it:on branch main, not develop, thebranch:this leg's own row declares in project.yaml (...). The finding for a leg with nobranch:of its own is byte-identical to before.Text that said the old rule is brought along:
resume --help,AGENTS.mdrule 1, and the comments abovestatus'sestate_reposand inpark'sreport_repos.Tests
seed_leg_member(resume's tests) andseed_root_with_leg(status's) take optional arguments (leg_branch, and for the firstcode_legandspec_path); the defaults build the same estates as before. The new tests sit beside the F3 tests and besideread_leg's tests. Each estate is built with the suite's own helpers and the REALbootstrap.pymounted atupstream/openRepoShape.test_a_leg_on_its_own_declared_branch_is_not_refusedbranch: develop, ondevelopat the pin: exit 0, no refusal, the verb runs, HEAD unchangedtest_a_leg_off_its_own_declared_branch_refuses_the_root_and_is_not_movedmainwith a localdevelopat the pin: refused by name, exit 1, HEAD unchanged, verb not run; then the root's real bootstrap, run by hand, moves the leg ontodevelop, so the refusal is not theatretest_a_leg_with_no_branch_of_its_own_is_held_to_the_roots_and_says_sobranch:on a feature branch: refused as before, naming the root'stracking_branchtest_a_leg_off_its_own_declared_branch_is_a_finding_that_names_it,test_a_leg_on_its_own_declared_branch_is_not_a_findingstatusin both directionstest_leg_branch_of_reads_a_rows_own_branch_the_way_bootstrap_does(22 cases)trim_unquoteandleg_branch_ofcut out ofresume:branch:before and afterpath:, double and single quotes, a trailing comment (also after quotes), an empty value, only a comment,null/Null/~, a row with none beside one with one, the assembly row, paths sharing a prefix,./spec,spec/, a./specasked for, and a row that does not open with- role:test_each_leg_of_one_root_is_held_to_its_own_branchdevelopand a code leg declaring nothing: neither is refused where it belongs; the code leg ondevelopis refused by name and the spec leg is left alonetest_a_leg_spelled_dot_slash_or_with_a_slash_is_one_leg_to_status_and_resume(2 spellings)path: "./spec"orspec/,branch: develop, leg onmain:statusreports it,resumerefuses itRed/green. Every
LANES_*/TMUX*variable unset, private HOME, TMPDIR, basetemp and pycache, under the pytest lock, mount at190133d(v1.1.0):park/resume/status, at both7f84ca4and190133d; 5 passed on the branch. At v1.1.0 the (b) failure isresumeexiting 0 over[spec] spec: on develop at 98154768e7a2 (branch tip == pin), the leg moved.a-trailing-comment-is-cut,...-after-quotes-is-cut,only-a-comment-is-no-branch)null/~casenull,Null,tilde)branchreset whenpath:is read (branch before path)before-path)./and/normalisationstatus/resumetest)leg_branch_ofalways answers nothing (the old behaviour)With the fix every one of these passes.
Modules, this branch merged with
f5c3bcd, submodule at190133d:tests/test_park_resume_commands.py(164 tests),tests/test_status_command.py(202) andtests/test_repo_hygiene.py(189, which holds the three-way byte-identity of the shared resolver and the host-path rule): 555 passed. The full suite was not run locally; CI's Linux jobs are the suite of record.Not in this change
park'sreport_reposhas the same assumption when it counts unpushed commits on a leg's tracking branch, so work on a leg's declared branch is never reported. The independent review of this PR found it (review 5561239715) and it is filed as #242. The comment there now says so.🤖 Generated with Claude Code