Skip to content

make park / make resume: the assembly root and the family holder expose Speckit's verbs, and refuse by name when the overlay is not installed (#77) - #80

Merged
brettheap merged 2 commits into
mainfrom
feat/park-resume-make-targets
Sep 9, 2026
Merged

brettheap merged 2 commits into
mainfrom
feat/park-resume-make-targets

Conversation

@brettheap

@brettheap brettheap commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Lane: xfactory-2 (xFactory-2)
Claim: #77 (comment) (2026-09-09T17:55Z, on Brett Heap's "claim 77 and start on it"), slice S2 of the plan posted on #77
Rulings: #77 (comment) (decision 5: the family holder gets the verbs too)

Governing: #77 (the openRepoShape side). The mechanics live in the Speckit git extension — opensoft/openBenches#30, branch feat/speckit-park-resume, in flight under the same lane. This PR is independent of it: with no overlay installed, both verbs refuse by name.

The assembly root exposes make park and make resume, thinly. Each target tests for .specify/extensions/git/scripts/bash/<verb>.sh; absent, it prints one line to stderr — make park needs the Speckit worktree overlay; install it with: setup-openspeckit — and exits 2; present, it execs the script with $(ARGS), so make park ARGS=--dry-run is the rehearsal. Nothing of park or resume is reimplemented here (ruling 1 on #77: Speckit owns the mechanics). One fact the Makefile and the tests state plainly: GNU make reports ANY non-zero recipe status as its own exit 2 with Error <code> in its diagnostic, so the extension's 0/1/2/3 cannot come back out of make unchanged — nothing here swallows or invents a code, and the refusal's own exit 2 survives intact.

The family holder runs the verbs in the WORKING CLONES beside it, never in members/ (decision 5). scripts/siblings.py — the #79 utility that already knows each member's sibling path and verifies it by origin url and project.yaml id — gains --make <target> and repeatable --make-arg: for each family.yaml member it runs make <target> in <holder>/../<Project>, streams the output under a per-member header, continues past every failure, and reports at the end. A member with no working clone is SKIPPED by name (run \make siblings` first); a sibling that is another repository is skipped, not run; exit 0 ONLY if every member ran and exited 0 — a make resumeon a fresh machine where nobody ranmake siblingsmust not exit 0. With--make, siblings.py clones nothing and fetches nothing. The pinned copies under members/are detached, for bootstrap and validate; the sibling is where the work is, and a holder verb that walkedmembers/would answer "nothing to park" while the person's features sat beside it — a test asserts the pinned copies are never touched.templates/family-root/scripts/bootstrap.py` is untouched (byte-identical to main).

Pinned docs carry the rule to existing roots as upstream-changed through update-shape.py apply: the assembly root's AGENTS-shape.md (park before you leave a workstation; clone, make bootstrap, make resume on the next; never commit under worktrees/) and the holder's AGENTS-shape.md, Makefile and scripts/siblings.py. Templated READMEs get the same for new projects. This repository's README gains ## Carrying in-flight work to another workstation (the two verbs; the manifest is the person's, in a repository they own, named once in ~/.agents/workspace.yaml; resume refuses on divergence; nothing under worktrees/ is ever committed or synced; native Windows parks nothing, the PowerShell mirror lacks it) and AGENTS.md a ## Parking and resuming procedure. Size caps in the hygiene test move with the counts, with dated entries in each function's own style.

Tests: 28 cases in tests/test_park_resume_targets.py — the root's refusal, exec and args pass-through, exit-code pass-through (0 stays 0; 3 comes back as make's 2 with 3 in the diagnostic), .PHONY and help; the holder on the real layout (holder at <F>/<F>, working clones at <F>/<A> and <F>/<B> cloned from bare remotes, pinned copies stubbed to prove they are never run), skip-by-name, refusal-and-continue, wrong-repository skip, --make-arg carrying ARGS with MAKEFLAGS emptied, dry-run, no clone/fetch, --make-arg alone refused. test_scaffold_e2e.py::test_make_targets_exist gains both targets. Full suite: 714 passed, 1 skipped.

Follow-up not in this PR: docs/handbook.html lags the README by the new section.

🤖 Generated with Claude Code

Summary by Sourcery

Add thin park/resume targets and safely dispatch them across family working clones for cross-workstation in-flight work transfer.

New Features:

  • Expose make park and make resume in assembly roots, delegating worktree transfer to the Speckit overlay.
  • Add family-holder dispatch for running park/resume in verified working clones beside the holder, with per-member reporting and failure handling.

Bug Fixes:

  • Prevent family park/resume operations from acting on pinned members/ copies instead of working clones.
  • Ensure missing clones, wrong repositories, refusals, and member failures cannot be reported as overall success.

Enhancements:

  • Validate dispatched make targets and arguments before execution and preserve dry-run and argument pass-through behavior.
  • Document workstation handoff, overlay prerequisites, divergence handling, worktree restrictions, and family-holder semantics.

Documentation:

  • Update project and template documentation with park/resume usage, safety rules, and platform limitations.

Tests:

  • Add coverage for target delegation, refusal messages, argument and exit-status behavior, family dispatch, skip/continue semantics, repository verification, dry runs, and safety constraints.

Chores:

  • Adjust repository hygiene size caps for the expanded documentation.

…se Speckit's verbs, and refuse by name when the overlay is not installed (#77)

Slice S2 of #77, under Brett Heap's RULING of 2026-09-09 (ruling 1: Speckit
owns the mechanics and the root exposes the verbs; ruling 5: the family
holder gets them too) and the claim of 2026-09-09T17:55Z.

The assembly-root template Makefile gains `park` and `resume`, three lines
each: test the extension script, or print `make park needs the Speckit
worktree overlay; install it with: setup-openspeckit` and exit 2. `ARGS`
reaches the script, so `make park ARGS=--dry-run` is the rehearsal both
documents promise. NOTHING of park or resume is reimplemented here — the WIP
commit, the push, the `git worktree add`, the `.specify/feature.json`
rewrite and the soft reset are the Speckit git extension's, one
implementation, and a second one in a Makefile would have no tests of its
own. One `OVERLAY_MISSING` message serves both targets so the two refusals
cannot drift apart.

The family holder gains the same two verbs and runs each member's own IN THE
MEMBER'S WORKING CLONE BESIDE THE HOLDER — `scripts/siblings.py --make
<target>`, with `--make-arg` appended verbatim to that member's `make` line,
which is how `make park ARGS=--dry-run` reaches every member. NOT
`members/<Project>`: that copy is pinned, DETACHED and holds nobody's
features, so a holder that parked it would report "nothing to park" for every
member while the person's work sat untouched next door — the wrong question,
answered confidently. `siblings.py` is where the dispatch belongs because it
already knows where a sibling is and how to prove the clone there IS that
member (its `origin`, its own `project.yaml` id); that check now has ONE
definition, `verify_sibling`, used by the placement half and by the dispatch,
and the 17 existing siblings scenarios pass unedited. With `--make` it clones
nothing and fetches nothing: dispatching is all it does. A member with no
working clone is reported and SKIPPED naming `make siblings`, a member whose
verb refuses has that refusal printed where it happened, every member is
tried, and the run exits non-zero if any was skipped or refused — a `make
resume` on a fresh machine that skipped every member must never read as
success. `scripts/bootstrap.py` is untouched, so the `update-shape.py`
surface for an existing holder is `Makefile`, `AGENTS-shape.md` and
`scripts/siblings.py`.

GNU make reports ANY non-zero recipe status as its own exit 2 and prints
`Error <code>`, so the extension's 1/2/3 cannot come back out of `make`
as 1/2/3. Nothing here swallows or rewrites a code; the Makefile comment,
the new test file's header and its assertions all say so plainly rather
than pretending otherwise.

Docs: the two PINNED `AGENTS-shape.md` copies gain the verbs (park before
leaving a workstation; clone, `make bootstrap`, `make resume` on the next;
never commit under `worktrees/`; the holder's verbs run in the working clones
beside it, and a member that is skipped or refuses is reported and the rest
still run) — that is what reaches existing roots as `upstream-changed`
through `update-shape.py apply`. The templated READMEs gain a "Parking work
for another workstation" block and two rows in the holder's target table.
This repository's own README gains `## Carrying in-flight work to another
workstation`: the two verbs, who implements them, that the record is the
person's in a repository they own and named once in `~/.agents/workspace.yaml`,
that `resume` refuses on divergence, the holder's half and that it runs in
the working clones beside it, that nothing under `worktrees/` is committed or
synced, and that native Windows parks nothing
because the PowerShell mirror has neither script. `AGENTS.md` gains a short
`## Parking and resuming` procedure: the commands, the `setup-openspeckit`
prerequisite, never force-push a parked branch on a person's behalf, and
that the divergence refusal is not a thing to work around.

Tests: `test_make_targets_exist` gains `park:`/`resume:`; the new
`tests/test_park_resume_targets.py` (27 cases) covers the refusal by name in
a scaffolded root with no `.specify/`, the `ARGS` pass-through and the exit
code through an executable stub, `.PHONY`, `make help`, and a real
workstation layout — the holder at `<F>/<F>`, a working clone per member
beside it, bare repositories in a temporary directory — proving the verb runs
in both clones with `ARGS`, that `members/<Project>` (stubbed as `PINNED-*`
on purpose) is NEVER invoked, that a missing clone is skipped by name and the
next member still runs, that a member's exit 3 is reported and the rest still
run, that `--dry-run` runs nothing, and that the dispatch clones and fetches
nothing. The `README.md` and `AGENTS.md` line caps move with a dated entry
each, in the style of the two entries #78 and #79 wrote today.

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 9, 2026 19:55
@brettheap

Copy link
Copy Markdown
Contributor Author

@codex review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 1 day and 6 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds thin make park/make resume entry points to assembly roots and family holders, delegating mechanics to Speckit while safely dispatching holder commands into verified working clones, with explicit refusal/reporting behavior, documentation updates, and comprehensive offline tests.

Sequence diagram for assembly root park and resume delegation

sequenceDiagram
    participant User
    participant RootMakefile
    participant SpeckitOverlay

    User->>RootMakefile: make park or make resume
    alt overlay script missing
        RootMakefile-->>User: stderr refusal with setup-openspeckit
        RootMakefile-->>User: exit 2
    else overlay installed
        RootMakefile->>SpeckitOverlay: exec script with ARGS
        SpeckitOverlay-->>User: park or resume result
    end
Loading

Sequence diagram for family holder verb dispatch

sequenceDiagram
    participant User
    participant HolderMakefile
    participant SiblingsPy
    participant WorkingClone

    User->>HolderMakefile: make park or make resume
    HolderMakefile->>SiblingsPy: --make target --make-arg ARGS
    loop each family member
        alt verified working clone beside holder
            SiblingsPy->>WorkingClone: make target with forwarded arguments
            WorkingClone-->>SiblingsPy: output and exit status
        else no clone or wrong repository
            SiblingsPy-->>User: named skip
        end
    end
    SiblingsPy-->>User: per-member report and aggregate status
Loading

File-Level Changes

Change Details Files
Add thin assembly-root park/resume targets that delegate all mechanics to the Speckit overlay and fail explicitly when it is absent.
  • Add phony park and resume targets with ARGS pass-through and executable overlay checks.
  • Emit named installation guidance and preserve GNU make’s non-zero failure behavior.
  • Document the overlay prerequisite, dry-run usage, divergence refusals, and worktree safety rules.
templates/assembly-root/Makefile
templates/assembly-root/AGENTS-shape.md
templates/assembly-root/README.md
README.md
AGENTS.md
Expose park/resume on family holders and dispatch them into verified working clones beside the holder rather than pinned member copies.
  • Add holder targets that invoke scripts/siblings.py --make and forward ARGS via repeatable make arguments.
  • Add dispatch mode with working-clone path resolution, origin/project identity validation, named skips, per-member output, continue-on-failure behavior, and aggregate status reporting.
  • Ensure dispatch performs no cloning or fetching and rejects --make-arg without --make.
templates/family-root/Makefile
templates/family-root/scripts/siblings.py
templates/family-root/AGENTS-shape.md
templates/family-root/README.md
Expand automated coverage for target delegation, holder dispatch semantics, safety boundaries, and scaffold integration.
  • Add 28 offline tests covering refusal messages, argument and exit handling, phony/help declarations, working-clone execution, skips, refusals, wrong repositories, dry runs, and no clone/fetch behavior.
  • Verify pinned copies are never invoked and that family failures continue while producing a non-zero result.
  • Update scaffold target checks and repository documentation size caps.
tests/test_park_resume_targets.py
tests/test_scaffold_e2e.py
tests/test_repo_hygiene.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new scripts/siblings.py --make path (and the script overall) uses git but does not preflight for git being on PATH, so it can crash with FileNotFoundError instead of producing the documented exit-2 refusal.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR adds thin make park / make resume entry points to both the assembly-root template and the family-holder template, delegating all mechanics to the Speckit git extension and providing a named refusal when the overlay is not installed; it also extends the family holder’s scripts/siblings.py to dispatch these verbs into verified working clones beside the holder.

Changes:

  • Add park/resume Makefile targets in the assembly-root and family-holder templates, including help text, .PHONY, and a clear “install with: setup-openspeckit” refusal when the overlay scripts are missing.
  • Extend templates/family-root/scripts/siblings.py with --make <target> / --make-arg to run make <target> across verified member working clones (skipping safely and reporting outcomes).
  • Add substantial tests for refusal behavior, argument pass-through, and holder dispatch semantics; update documentation and repo hygiene size caps to reflect the new guidance.
File summaries
File Description
tests/test_scaffold_e2e.py Ensures newly scaffolded projects’ Makefiles include park: and resume: targets.
tests/test_repo_hygiene.py Updates README/AGENTS line-count caps with dated justifications for new sections.
tests/test_park_resume_targets.py New suite covering assembly-root refusal/exec behavior and holder dispatch behavior end-to-end.
templates/family-root/scripts/siblings.py Adds --make dispatch feature with per-member verification, reporting, and exit semantics.
templates/family-root/README.md Documents family-holder make park / make resume behaviors and skip semantics.
templates/family-root/Makefile Adds holder park/resume targets implemented via scripts/siblings.py --make ….
templates/family-root/AGENTS-shape.md Adds holder guidance and safety rules for park/resume and working-clone dispatch.
templates/assembly-root/README.md Adds workstation handoff procedure and reiterates thin delegation to Speckit.
templates/assembly-root/Makefile Adds park/resume targets that refuse by name when overlay scripts are absent and otherwise exec them with $(ARGS).
templates/assembly-root/AGENTS-shape.md Adds usage guidance and safety rules for park/resume to the pinned agent instructions.
README.md Adds a new section describing park/resume workflow, refusal message, and family-holder behavior.
AGENTS.md Adds a “Parking and resuming” procedure and safety rules for assistants/operators.
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 752 to 756
args = parser.parse_args(argv)
if args.make_arg and not args.make:
parser.error("--make-arg is only meaningful with --make <target>")

try:
…ttern before they reach the subprocess (SonarCloud S8705 on #80) (#77)

SonarCloud's quality gate on PR #80 raised `pythonsecurity:S8705` on the
dispatch's `subprocess.run([make, target, *make_args], cwd=...)`: both values
arrive on this command's own command line and reach a subprocess's argv with
nothing between them. Nothing was injectable — the call is a LIST with no
shell, so `park; rm -rf /` would be ONE argument make has no rule for, never
a second command — but the check is cheap, it is right, and a file that runs
a subprocess should say on the same screen what constrains its argv.

Two patterns at module level, with the WHY beside them:

    MAKE_TARGET_RE     [A-Za-z][A-Za-z0-9_.-]*
    MAKE_ASSIGNMENT_RE [A-Za-z_][A-Za-z0-9_]*=[^\x00-\x1f\x7f]*

A make TARGET NAME, and a `NAME=value` make ASSIGNMENT whose value carries no
control character — the newline especially. They are narrow because the
holder only ever passes one shape of either, `make park` and `ARGS=<flags>`,
so anything else is a mistake worth refusing by name rather than a case worth
carrying.

`validated_make_call` refuses AT PARSE TIME, before anything runs, with
`make-target-invalid` or `make-arg-invalid` (exit 2) naming the pattern and
the offending value `repr()`'d and saying "Nothing was run." Not at the first
member: a bad value discovered after one member's verb had already committed
and pushed somebody's work would be a refusal that came too late to be one.
`--make ''` reaches that refusal rather than quietly becoming a placement run
— the dispatch branch now tests `is not None` instead of truthiness.

`is_a_make_call` is the one predicate both the parse-time refusal and the
call site use, and the call site restates it: a reader (or a scanner) looking
at the `subprocess.run` finds the constraint beside it instead of three
hundred lines up, and the sanitized values go into named locals immediately
before the call. That branch cannot fire, and says so.

`make_program` now resolves `$MAKE` through `shutil.which` as well as PATH,
so what reaches the subprocess is an absolute executable this file resolved
rather than a string somebody left in an environment variable; `$MAKE` set to
something unexecutable is `make-not-executable` (exit 2) instead of an
exec failure per member.

Tests: `--make` refused for `park; rm -rf /`, the empty string, `../park`,
`park park`, an embedded newline and `park$(id)`; `--make-arg` refused for an
embedded newline, `not an assignment`, `=x`, `9ARGS=x` and a bare `ARGS`;
accepted for `ARGS=--dry-run`, `ARGS=` (what `make park` with no ARGS
produces), a multi-flag value and `SPECKIT_LANE=xfactory-2`; `$MAKE` pointed
at a non-executable refused. Each refusal asserts exit 2, the code by name,
the value in the message, and that no member's stub ran. `-park` is
deliberately not in the target list: argparse refuses a value that looks like
an option before this file's check ever sees it.

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 9, 2026 20:01
@sonarqubecloud

sonarqubecloud Bot commented Sep 9, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation is cohesive and well-tested; only a small user-facing output formatting nit was found in the new siblings dispatch banner.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

templates/family-root/scripts/siblings.py:730

  • The dispatch banner has a broken sentence: it prints "holder. This" on one line and then starts a new sentence on the next line. That reads like an accidental wrap and makes the output harder to follow.
  • Files reviewed: 12/12 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: f5467bd2a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@brettheap
brettheap merged commit 3abeb8a into main Sep 9, 2026
6 checks passed
brettheap added a commit that referenced this pull request Sep 10, 2026
… and drives its own verbs, and resume rebuilds it on the next workstation (#82) (#83)

* park and resume as installed user commands: one word finds the estate and drives its own verbs, and resume rebuilds it on the next workstation (#82)

#82, under Brett Heap's RULING of 2026-09-09 — ruling 1: `--repo` matches a
LOCAL clone by origin and only `resume` clones, from the manifest; ruling 2:
`resume` fast-forwards each sibling's tracking branch with `--ff-only` and
REFUSES BY NAME one that is dirty or on a feature branch; ruling 3: `park`
with no `<Name>` and no estate around the cwd REFUSES and lists the estates
it found; ruling 4: the commands are `park` and `resume` — and the claim of
2026-09-10T00:19Z. Follows #77's S1 (opensoft/openBenches#30, merged
1a1445b) and #80.

The front door over #80's targets, the way the `openRepoShape` command is the
front door over `setup.sh`: `park InkRouter` from any folder on workstation A,
`resume InkRouter` on workstation B. Two bash files at the repository root,
755, and `openRepoShape --install` now places all three.

THEY ADD NO MECHANICS, and that is the whole design. They find the estate and
run its own `make park` / `make resume`, which run the Speckit git extension's
`park.sh` / `resume.sh` — the one implementation of the WIP commit, the push,
the `git worktree add`, the `.specify/feature.json` rewrite and the soft reset.
`park` writes nothing at all; everything `resume` writes it writes with `git
clone`, `git pull --ff-only`, the root's own `make bootstrap` and the holder's
own `make siblings`. Nothing is reimplemented, so there is nothing here for the
extension to start disagreeing with.

ESTATE RESOLUTION, shared by both and NOT a new rule: `<Name>` is a folder
under `$PROJECTS_DIR`, else `~/projects/<Name>` then `~/Projects/<Name>` —
people spell it both ways, and the two are deduped by inode because on a
case-insensitive filesystem they are one directory and listing every estate
twice would read as two estates. A FAMILY FOLDER wins over a standalone root of
the same name, because the holder is what drives the members and a family
answered as one of its own members would park one project and report the estate
parked. What makes a directory a family folder is read and never guessed from
its name: `<dir>/<basename>/family.yaml`, `kind: family-manifest`, and a
`name:` that agrees — `setup-project.py`'s own `family_folder_here`, doubled
name and all. With no `<Name>` the NEAREST estate at or above `$PWD` is used,
so `park` inside a member of a family parks that member. `--repo <owner/name>`
matches the `origin` of a clone under the projects directory, two levels deep,
in any url spelling and case-insensitively; a FILESYSTEM PATH is refused rather
than parsed, for the reason the extension's own `workspace_org_from_url`
refuses one — `/srv/remotes/Name.git` has a name and no owner. THE BLOCK IS
DUPLICATED IN BOTH FILES, deliberately: each is one file a person has on PATH,
and a shared library would be a fourth file for `--install` to place and a
broken command the first time somebody copied only one of them.

`park [<Name>] [--repo <owner/name>] [--dry-run] [--lane <name>] [-- ARGS...]`
resolves, then runs `make park ARGS='...'` in the holder or the root, streaming
it, then prints WHAT DID NOT PARK and why none of it is park's to touch:
commits on a `main` that `origin` has never seen, per repository — the
holder or root, each working clone beside a holder, and each of their legs out
of `.gitmodules` — with both refs required to exist locally, so a detached
pinned checkout and a remoteless clone are silent rather than wrong; a DIRTY
ROOT, which is where a pin bump happens and which also answers for a dirty leg
(`git status --porcelain` at a root reports one as ` M <leg>`); and the
one-line reminder that ignored files never travel at all. `members/<Project>`
is deliberately never read: that copy is pinned and DETACHED and holds nobody's
work, the same reason #80's holder dispatches to the sibling. One summary line,
and make's exit code passes straight through — GNU make flattens the
extension's 1/2/3 to its own exit 2 and prints `Error <code>`, which this
neither rewrites nor rescues.

`resume [<Name>] [--repo <owner/name>] [--workspace <owner/name>] [--org
<org>] [--dry-run] [-- ARGS...]` does five things in order. (1) The workspace
record: `${AGENT_PROTOCOL_ROOT:-$HOME/.agents}/workspace.yaml`, pulled
`--ff-only` when its checkout is there, cloned from the config's own
`repository:` when it is not, and on a machine with NO config at all clonable
only by `--workspace <owner/name>` — which then writes the file's two lines.
That is the only write to that file anywhere in this standard, it happens only
because somebody named the repository, and a config that already names a
different one is a refusal rather than an overwrite. (2) The manifest:
`workspaces/<org>/<Name>.yaml`, matched case-insensitively because a standalone
project's file is named for its `project.yaml` id while the folder a person
types is the repository name; two orgs recording one name is a refusal naming
`--org`. A family file gives the holder repository by the DOUBLED-NAME RULE
(`<org>/<Family>`, since `projects[]` are the members and the holder is not one
of them); a standalone file gives `projects[0].repository` and its `root:`.
(3) The estate: absent, `mkdir -p` the folder, `git clone
--recurse-submodules`, `make bootstrap`, and for a family `make siblings`;
present, the holder or root fast-forwarded and bootstrapped, `make siblings`,
then each member's working clone that was ALREADY THERE fast-forwarded on its
tracking branch and given its own `make bootstrap` — a clone `make siblings`
has just placed is on its branch with its bootstrap run already. RULING 2 is
here: a clone that is dirty or not on its tracking branch is named, skipped and
left exactly as it was, and the rest of the estate still comes back. (4) `make
resume ARGS='...'`. (5) `make siblings`' OWN layout table, reprinted from the
run's log rather than recomputed, because two readings of what is beside the
holder is how they start disagreeing. A refusal or a finding makes the exit
non-zero even when the verb exited 0: a skip is not a pass.

`openRepoShape --install` installs the three from ONE list, idempotently and
per file (`installed at` / `updated at` / `unchanged`, 755 stamped every time).
Run from a file it copies the siblings beside it — so a machine with no `gh`
and no network still gets all three — and fetches only what is missing; run
from stdin it fetches each at the same `$REF` through the same
`fetch_from_repo`. The first four usage lines are byte-identical and
`--version` is untouched.

`$OPENREPOSHAPE_REMOTE_BASE` is `resume`'s documented OFFLINE TEST PATH: a
directory of bare repositories named `<name>.git`, used as origins, exactly
the concession `setup-project.py --local-remote-dir` and `family.py
--local-remote-dir` make, and documented in the usage the way the shim
documents `$OPENREPOSHAPE_SETUP_SH`. It grants no file-protocol permission of
its own — a test that needs `protocol.file.allow` sets it in git's own
environment, which keeps that a test's decision and not a tool's.

Docs: the README's two install paragraphs say THREE commands, and a new
`### park <Name> and resume <Name>, from anywhere` subsection carries the two
words, the `--workspace` line typed once per machine, how the estate is found,
what `--repo` does and does not do, and what they leave behind — an unpushed
`main` commit, a dirty root, ignored files, a clone refused by name, and no
agent session at all. The Windows warning now covers the commands too, which
are bash like the `--install` line that places them. `AGENTS.md` gains the
paragraph and two rules: `park` creates nothing and its refusal is not an
invitation to pick an estate; `resume` resets nothing and its refusal is not a
state to `git reset`, `git stash` or `git checkout -f` out of the way, because
that clone holds the work the refusal exists to protect; and `--workspace` is
never passed on an assistant's own initiative. Both caps move with dated
entries in each function's style (AGENTS.md 342 -> 364, README.md 1141 ->
1197), and both files join `SHIPPED_BASH`, so the shebang, the mode bit,
`set -euo pipefail` and `bash -n` are checked for them.

Tests, `tests/test_park_resume_commands.py`, 44 of them and offline in three
ways at once: the extension is #80's echoing STUB, every remote is a bare
repository in a temporary directory reached through
`$OPENREPOSHAPE_REMOTE_BASE`, and `$HOME` is temporary too, so both spellings
of the projects directory and `~/.agents/workspace.yaml` are the test's own and
never the developer's. Estate resolution has both spellings, `$PROJECTS_DIR`,
family-beats-standalone, the walk-up from a subdirectory, nearest-wins inside a
member, the ruling-3 refusal LISTING what it found, `--repo` in three url
spellings and two levels deep, and its no-match refusal naming `git clone`.
`park` drives the holder's verb with `ARGS`, reports an unpushed main commit
and a dirty root by path, says so when it left nothing behind, never reads
`members/`, and under `--dry-run` moves no HEAD and creates no file. `resume`
is proved end to end on a FRESH machine — the workspace cloned, the config
written to the byte, the holder cloned into `<Family>/<Family>`, the pinned
copies arriving with `--recurse-submodules`, both members placed beside it on
`main`, each member's own `make resume` run, and the layout table printed — and
then on an estate one commit behind: the holder and both siblings
fast-forwarded to `origin/main` with their own bootstraps run, a dirty sibling
and a feature-branched sibling each refused BY NAME and left untouched while
the other still comes back, a deleted member placed again by `make siblings`, a
standalone project rebuilt from `projects[0]`, the missing config refusing and
naming `--workspace`, and two orgs refusing until `--org` answers. The
workspace manifest is written BY HAND at `schema_version: 1` — the schema
`park.sh` writes as of S1 — because a test that needed a workBenches checkout
beside this one would skip on every fork.

`python3 -m pytest tests -q`: 781 passed, 1 skipped. `bash -n` on all three;
`shellcheck` clean on `park` and `resume`, with the two intentional
suppressions carrying their reasons (`SC2016`, where the single quotes are the
literal `ARGS='...'` a person copies, and `SC2088`, where a leading `~` is
being matched and not expanded — the extension's own `workspace-common.sh`
carries the same one for the same reason).

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* park/resume: a refused root is never bootstrapped, a leg off its tracking branch refuses the root, the family folder comes from root: not the lowercase id, --repo takes any spelling, the walk-up skips pinned copies and worktrees (review on #83) (#82)

The adversarial review on PR #83, every finding reproduced against offline
fixtures before it was fixed and every fix carrying a test that FAILS on
e361455. Three of them lose or move somebody's work; the rest are refusals
that fired when they should not have, or did not fire when they should.

F1 — A REFUSED ROOT IS NEVER BOOTSTRAPPED. The holder and the standalone root
were fast-forwarded with `pull_ff_only … || true` and then bootstrapped
ANYWAY. The family holder's `make bootstrap` is `git submodule update --init
--recursive`, which snaps every `members/<X>` back to the recorded gitlink —
so a run that printed "nothing was fast-forwarded and nothing was reset" then
reset exactly the thing the refusal was protecting: a half-finished
`family.py bump`, which lives as a MOVED GITLINK in the holder's index. The
sibling loop had it right (`|| continue`) and the root did not. Both now go
through one `refresh_root`: the legs, then the fast-forward, then the
bootstrap, and a refusal at any step skips the steps after it and says so.
The test stages a moved gitlink plus a scratch file at the holder root and
asserts the staged gitlink and the pinned copy's HEAD are untouched
afterwards.

F2 — THE FAMILY FOLDER COMES FROM `root:`, NOT FROM `family:`. `park.sh`
writes the header's `family:` from `family.yaml`'s **`id:`** — that is what
`workspace_family_name` reads — and an id is LOWERCASE by construction
(`family.py`: `--id … or family.lower()`, checked against `PROJECT_ID_RE =
^[a-z0-9][a-z0-9-]*$`), and the manifest FILE is named for the same id. The
folder, and the holder repository, are the family's CamelCase `name:` — which
is what `is_family_folder` requires the directory to match. Deriving the
folder from `family:` therefore cloned `<org>/inkrouter` into
`~/projects/inkrouter/inkrouter` and then refused "not an estate" over the
half-estate it had just made. The comment claiming there was nothing else in
the file to read it from was wrong: `workspace_manifest_root_value` writes
each member's `root:` as `<family folder>/<project folder>` with the real
casing, so the first segment is the folder and — the holder repository being
the family's own name in its org, #76 ruling 3's doubled-name layout — the
repository name too. `family:` keeps the one job it is good for: saying the
file is a family's at all. THE FIXTURE NOW WRITES WHAT `park.sh` WRITES
(`family: testfam`, `workspaces/<org>/testfam.yaml`, `root: TestFam/Alpha`),
mirrored key for key from `workspace-common.sh`'s writer, and on e361455 the
fresh-machine test fails with `repository 'testfam.git' does not exist`.

F3 — A LEG OFF ITS TRACKING BRANCH REFUSES THE ROOT. `templates/
assembly-root/scripts/bootstrap.py`'s `checkout_tracking_branch` runs `git
checkout <tracking>` in a leg whose local tracking-branch tip equals the pin,
and `checkout -b <tracking> <pin>` in a leg that has no local one — either of
which MOVES A LEG'S HEAD. A leg sitting on a clean feature branch at the pin
leaves the SUPERPROJECT clean, so the dirty and off-branch guards saw nothing,
and `resume` walked that leg off `001-my-feature` onto `main` and exited 0.
`root_legs_ok` now asks each leg `project.yaml` declares, before the pull and
before the bootstrap, and refuses the root by name naming the leg. Detached
AT the pin is not a refusal: that is the state a fresh clone arrives in and
precisely what bootstrap exists to fix. The fixture ships the REAL
`bootstrap.py` and a real submodule leg in one member, and a second test runs
that script directly to show it does move the leg — the guard is not theatre.

F4 — TWO CHECKOUTS OF ONE WORKSPACE ARE NOT TWO ORGS. `FOUND` counted manifest
FILES, and `WORKSPACES=("$ORG_PATH" "$WORKSPACE")` was not deduped, so an
`orgs:` override pointing at a second clone of the same private repository
produced "2 workspace manifests record 'InkRouter'" — a refusal `--org` could
not answer, because both rows named the same org. Candidates are deduped on
`<org>` plus the file's basename, workspace paths on their PHYSICAL directory,
and the refusal counts the orgs that are left.

F5 — `--repo` TAKES ANY SPELLING. It accepted only a bare `owner/name`, so
`TestOrg/Atlas.git`, a trailing slash and the https url a person copies out of
the browser all refused — and the refusal then printed `git clone …
https://github.com/https://github.com/TestOrg/Atlas.git.git`. The flag's value
now goes through `repo_slug_of`, the same reader that reads a clone's
`origin`, so both sides of the comparison come from one parser and the clone
line is a url rather than a url wrapped round another one. `repo_slug_of` also
stopped lowercasing: callers compare with `lower` on both sides, and a
refusal prints back what a person can type.

F6 — THE WALK-UP SKIPS A PINNED COPY AND A LINKED WORKTREE. Both are whole
`project.yaml` roots, so the walk-up answered them: `<Family>/<Family>/
members/<X>`, which is DETACHED and pinned and where a write verb acts on the
wrong copy of a member, and a single-repository project's feature worktree
`<worktree_root>/<branch>`, which came back as an estate named after the
branch and got the write verb run inside the very worktree the extension
parks. `is_pinned_member` (the grandparent carries the `family.yaml`) and
`is_linked_worktree` (git's own `--git-dir` != `--git-common-dir`, which
distinguishes a worktree from a SUBMODULE, where both are the same path)
are now part of `is_project_root`, so the walk-up passes them and reaches the
family folder or the root that owns the worktree.

F7 — A `root:` IS A NAME, NEVER A PATH. `root: ../../elsewhere/X` was joined
to the projects directory and cloned outside it. `is_safe_folder_name` refuses
an empty name, a leading dot, a slash and anything outside
`[A-Za-z0-9_.+@-]`, checked BEFORE any path is built from it — and the same
check is now on the `<Name>` both commands take, so `park ../x` and `resume
/abs` refuse by name instead of being followed.

F8 — THE WORKSPACE CHECKOUT GETS THE SAME GUARD. It was pulled with a bare
`git pull --ff-only` whose failure was a WARNING, and then read as if it were
current. It goes through `pull_ff_only` now and a refusal is a refusal: the
record is what the whole run reads, and cloning an estate out of a manifest
whose checkout is dirty or mid-conflict is the wrong thing to do quietly. The
branch is passed in rather than derived, because a private repository declares
no tracking branch and being on `master` is not a finding.

F9 — A REFUSED MEMBER DOES NOT GET THE VERB. The holder's `make resume` is
`siblings.py --make resume`, whose whole flag set is `--root`, `--dry-run`,
`--make` and `--make-arg`: there is no exclusion to hand it, so a clone this
run said it would not touch was getting the verb run in it anyway. With a
refusal on the board the verb now runs per NON-REFUSED sibling, and the
holder's own dispatch is used whenever there is nothing to exclude — every
ordinary run. Adding an `--exclude` to `siblings.py` is a change to a PINNED
template that reaches every holder through `update-shape.py`, and it is not
this object's to make; the code says so where a reader will ask.

F10 — `--install` PLACES ALL THREE OR NONE. It placed one file at a time and
died on the first fetch that failed, leaving a NEW `openRepoShape` beside a
missing `park` — a half-install that prints `installed at` — with nothing on
screen to say which of the three were missing. `collect_commands` now stages
every one in the temporary directory first (copying the siblings beside this
file, fetching only what is not there) and places nothing until all three are
in hand; a failure names what was not fetched and says that nothing already
installed was replaced. A final `openRepoShape: 3 of 3 placed` line, because
three lines on screen cannot tell a reader whether a fourth was meant to be
there. `fetch_from_repo` returns instead of dying so a caller can collect a
set; `fetch_or_die` is the one-file spelling `setup.sh` still uses.

CI AND THE DUPLICATION CLAIM. `.github/workflows/tests.yml` parses `park` and
`resume` with `/bin/bash` too: they are installed onto the same macOS PATH by
the same `--install`, and a bash-4 construct in either would otherwise be
found by the person who typed `park InkRouter`. And the PR body's claim that
the two resolvers are duplicated byte for byte is now TRUE and TESTED — one
`# --- BEGIN shared estate resolver` block, identical in both files, with a
test that compares the two slices. It is duplicated for the reason it always
was: each command is one file a person has on PATH, and a shared library would
be a fourth file for `--install` to place.

NITS TAKEN. `park`'s unpushed check reads each root's own `tracking_branch:`
instead of a hard-coded `main`, and the report names the branch — a project
that ships from `trunk` was being asked about a branch it does not have and
answered "nothing". The README's "a green run is the only green run" was true
of `resume` and false of `park`, which passes `make park`'s status through and
can exit 0 with a report: the paragraph now says which of the two to read.
`AGENTS.md` gains the leg refusal, because an assistant that read only "dirty
or on a feature branch", looked at a clean root and concluded the refusal was
spurious would run `git checkout main` in the leg — the exact loss the guard
prevents, performed by hand. Both usage texts document the `--repo` spellings
and the leg refusal. Caps move with dated entries: AGENTS.md 364 -> 368,
README.md 1200 -> 1203.

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Sep 10, 2026
…he family workstation layout, --family, park/resume and the user commands, the workspace form; a guard that the page follows the README's outline (#89) (#90)

* docs/handbook.html: the designed reading of the README at cbca5b4 — the family workstation layout, --family, park/resume and the user commands, the workspace form; a guard that the page follows the README's outline (#89)

The page was cut from README.md at `c093813` (#69, PR #71) and #88 fixed one
word in it. Everything the README gained in between was missing, so the
designed page told a newcomer less than the file it is a reading of. This is
the regeneration, at `cbca5b4`, in the page's own design and voice.

WHAT THE PAGE GAINED

- `#park`, a new section between "Keeping shape current" and "The degrade
  rule" — the whole of `## Carrying in-flight work to another workstation`
  (#80): the two root verbs and `ARGS=--dry-run`; `### park <Name>` and
  `resume <Name>`, from anywhere (#83) with the estate resolution, `--repo`,
  the fresh-machine `resume --workspace`, and what the two leave behind
  (including READ THE LINES, not the exit code); `### Who implements it, and
  what the record is` — the Speckit git extension rather than this
  repository, the person's record named once in `~/.agents/workspace.yaml`,
  `<user>-wip` with the home-org default and the opt-in `orgs:` map (#86),
  nothing under `worktrees/` ever committed, `resume` refusing on divergence,
  the family holder's dispatch into the WORKING clones, and native Windows
  parking nothing. One rail entry, `Park and resume`.
- `#families` gained two `<h3>`s: "The workstation layout: a plain folder,
  and two copies of every member" (#79) — the plain `<Family>/`, the holder
  at `<Family>/<Family>`, `--into` as the PARENT and `--work-dir` as the
  override, `make siblings`, which copy you work in, and that nothing ever
  moves a checkout — and "The InkRouter example", whose five `family.py`
  commands and two trees are the README's current ones (the block it
  replaced still named `../InkRouter` as the holder). Plus the holder's own
  `make validate` and its COPY pin.
- `#start` gained "Joining a family" (`--family <Family>`, and that standing
  in the folder needs no flag at all — AGENTS.md) and "What setup.sh actually
  runs", the seven-command flow including `mkdir -p <into>/<Family>` (#78),
  with the developer path from a checkout. The install step and the
  masthead's fastest path now say THREE commands, `unchanged` per file, and
  what `park` and `resume` are (#83). The rehearsal flag's Windows spelling
  is the README's own `py setup-project.py … --local-remote-dir .\rehearsal`
  (#70), replacing an aside that told the reader to go and read `--help`.
- `#legs` links `<user>-wip` to where such a repository lives, and gained the
  paragraph a reader of `openDox` needs: a neutral product may elect the
  shape and be its own assembly root, `also_matches` recorded rather than
  thrown away. `#layout` lists `park` and `resume`. `#agents` gained the rule
  that a refused `park` or `resume` is never worked around with a reset, a
  stash or a force-push, and that `--workspace` is nobody else's flag to
  pass.

FIDELITY. 39 code blocks on the page: 31 byte-identical to a README block
(whole, or a contiguous run of its lines), 0 re-wrapped, 5 quoted from README
prose, 2 from README inline code, 1 assembled by the page from three quoted
commands (the masthead). Every one of the 13 GitHub anchors resolves to a
real README or AGENTS.md heading, all 27 in-page hrefs resolve to an id on
the page, and the file still parses with `html.parser` with every tag
balanced: one file, inline CSS and one inline script, one Google Fonts link
with real fallback stacks, no images and no other network call.

THE GUARD. `tests/test_repo_hygiene.py::test_the_handbook_follows_the_readmes_outline`
ties the two files together, because nothing did: an explicit
`README_HEADING_TO_HANDBOOK_ID` maps every `## ` heading of README.md to the
id of the page element that carries it, so a new README section with no entry
fails NAMING THE HEADING and a heading deliberately folded into a neighbour
is a named decision; `HANDBOOK_SECTION_WITHOUT_A_README_H2` accounts for the
four page sections that are not a README `## ` at all, so a page section
cannot become unexplained. And the page names the README commit it is a
reading of, exactly once, whose README must be byte-identical to the one in
the tree — `git diff <sha> -- README.md`, against the working tree, so the
page goes stale the moment the README changes rather than one commit later.
That last check is skipped, and only that one, when the named commit is not
in the clone: a `fetch-depth: 1` CI checkout has one commit and the object is
absent there, while the outline checks are tree-only and run everywhere.

Evidence: `python3 -m pytest tests/test_repo_hygiene.py -q` 46 passed;
`python3 -m pytest tests -q` 889 passed, 1 skipped (`SHAPE_MEDXEHR_CLONE` is
unset — pre-existing and unrelated). The five ways the new guard can fail
were each exercised in a throwaway clone. Docs and one test only; no README,
script, template or workflow change, and both size caps are untouched.

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* tests.yml: fetch full history so the handbook's staleness check bites in CI (#89)

Ruled by Brett Heap on 2026-09-10: *set fetch-depth 0 so the staleness check
bites in CI.*

`test_the_handbook_follows_the_readmes_outline` compares README.md against
the commit `docs/handbook.html` says it is a reading of, which needs that
commit's OBJECT present. `actions/checkout`'s default `fetch-depth: 1`
fetches one commit, so the object was absent and that one check skipped
itself: green in CI, red only on the machine where the page would be
regenerated, which is the wrong way round. All three jobs — `tests`,
`tests-windows`, `tests-macos`, the only three `actions/checkout` steps in
the file — now check out with `fetch-depth: 0`, and each says why in a
comment naming the test.

The test's BEHAVIOUR is unchanged: the "skipped when the named commit is not
in this clone" branch stays, because a `git clone --depth 1` still hits it
and a failure about the clone is not a failure about the page. Only its
docstring moves, to say that CI is no longer such a clone.

Evidence: `python3 -m pytest tests/test_repo_hygiene.py -q` 46 passed (with
tests/test_windows_paths.py, 84 passed). The workflow parses under PyYAML and
compares equal to HEAD's parse with the three checkout `with:` blocks removed
— nothing else in the file moved. No actionlint on this machine; CI's own
actionlint step lints `templates/assembly-root/.github/workflows/validate.yml`
only, which is untouched.

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Sep 10, 2026
… continuing past a refusal; resume keeps the refusal; the handbook guard names the README by blob hash (#91) (#93)

* park: with no estate around you it parks every estate, in name order, continuing past a refusal; resume keeps the refusal (#91)

Brett Heap's RULING, 2026-09-10, verbatim: "for the park-all, lets make that
be park with no estate option. so just park will do park-all." This
SUPERSEDES ruling 3 of #82 ("with no <Name> and no estate around the cwd
this REFUSES and lists what it found") for that one case only: `park
<Name>`, `park --repo`, and the walk-up inside an estate — NEAREST STILL
WINS, so standing inside an estate parks that estate, never the whole
workstation — are exactly as they were. `resume`'s own bare form keeps
ruling 3's refusal deliberately: rebuilding every estate this machine has a
record of, by accident, is the opposite risk from `park` failing to park
the one you meant, and #91 names `park` only.

THE MECHANISM. The single-estate procedure — check the Makefile, build
`ARGS`, print the banner, run `make park`, print what did not carry, print
the outcome line — is factored out of the resolve-and-run tail into
`run_one_estate`, which RETURNS a status instead of exiting; the
single-estate call site (`park <Name>`, `park --repo`, bare `park` inside an
estate) turns that return straight into its own exit code, so a named run
behaves exactly as it always did. A new `refuse` helper is the print half of
`die` without the `exit`, so a park-everything run can report one estate's
"no Makefile" refusal and carry on to the next; `die` is now `refuse` plus
the exit, so the two can never print differently by accident.

`all_estate_dirs` discovers every estate under the projects directory with
the SAME rules the refusal's own listing already used — a family folder
(`<X>/<X>/family.yaml`) or a standalone root (`<X>/project.yaml`), one level
under `$PROJECTS_DIR` or both `~/projects`/`~/Projects` — which is what
already excludes a pinned `members/<X>` copy (three levels down) and a
linked feature worktree (`is_project_root` itself refuses one). It sorts by
NAME rather than by discovery order, so an estate under `~/projects` and one
under `~/Projects` interleave alphabetically instead of one spelling's
estates printing entirely before the other's.

`park_every_estate` prints the list first (`park: no estate around <cwd>;
parking every estate under <dir>:`, then the same rows the old refusal
printed), then runs `run_one_estate` for each discovered estate in name
order under an `=== park <Name> ===` header, CONTINUING PAST A REFUSAL OR A
NON-ZERO `make park` rather than stopping at the first one. A skip is not a
pass (#80's rule): an estate whose own `make park` exited 0 but still left
something behind ($LEFT > 0) counts against the overall result exactly as a
refused one does. The run ends with one summary line per estate (`parked`,
`refused (exit N)`, or `findings (N thing(s) not carried)`) and `park: N
estate(s) parked, M refused or with findings`; it exits 0 only when every
estate ran clean, else 1. `--dry-run`, `--lane` and `-- ARGS` are read once
per estate inside `run_one_estate`, so they reach all of them. With NO
estates anywhere, the old ruling-3 refusal and its `(none)` listing are
untouched, and it still exits 2 — there is no "every estate" to park-all
there, so this is not this object's case to change.

`resume` is UNCHANGED behaviourally: bare `resume` with no `<Name>` and no
estate around the cwd still refuses. Its header gains a note saying so
deliberately and why, so a reader of one file is not left wondering why the
sibling command diverged.

The shared estate resolver block — `# --- BEGIN`/`# --- END shared estate
resolver` — is untouched byte for byte in both files; every addition here is
either above it (the header comment, the `refuse` helper) or below it
(`require_make`, `run_one_estate`, `all_estate_dirs`, `park_every_estate`,
the resolve `else` branch). `test_the_two_commands_carry_the_same_resolver_
byte_for_byte` stays green.

Docs: `park --help`'s "With no <Name>" paragraph, the README's "How the
estate is found" paragraph, and AGENTS.md point 5's opening sentence all say
the same new thing — park-everything for `park`, the kept refusal for
`resume` — rather than one going stale next to the other. Caps move with
dated entries, in each function's own style: AGENTS.md 374 -> 381, README.md
1219 -> 1225.

Tests, `tests/test_park_resume_commands.py`: the old
`test_no_name_and_no_estate_refuses_and_lists_what_it_found` becomes four
park-everything tests — two estates (one family, one standalone) parked in
name order with the list printed first; one estate's `make park` exiting
non-zero reported while the other still parks, overall exit 1; the walk-up
still winning inside an estate, asserting the OTHER estate's stub never ran;
and `--dry-run`/`--lane`/`-- ARGS` reaching every estate. The existing
`test_no_name_and_no_estates_at_all_says_none` (exit 2, "(none)") and
`test_resume_with_no_name_and_no_estate_refuses` (resume unchanged) are
untouched, and both still pass.

`python3 -m pytest tests/test_park_resume_commands.py tests/test_repo_hygiene.py -q`:
122 passed. Full suite, `python3 -m pytest tests -q`: 891 passed, 1 skipped.
`bash -n` clean on both files; `shellcheck` (docker koalaman/shellcheck:stable)
clean on both, no new suppressions.

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* The handbook names the README it was cut from by BLOB HASH, so the guard needs no history and squash-merges cannot orphan it; the page's park paragraph follows #91 (#91)

THE FLAW. #90 (5777ca8) landed `test_the_handbook_follows_the_readmes_outline`'s
check (4) as `git diff <sha> -- README.md` against the commit the footer
named, and #91's own README edit was about to turn that red in CI the moment
it merged with main. Worse than one red check: every PR in this repository
SQUASH-MERGES, so the README commit a feature branch names in the footer
never exists on main under that sha at all — it survives only as long as the
remote branch does, and once the branch is deleted the object is gone even
locally. The check needed `fetch-depth: 0` just to have a chance of seeing
that commit in CI, and even then it was diffing against something a
squash-merge workflow does not keep. `git merge --no-ff origin/main` (merge
commit 2c10faf) brings #90 in first, with no conflicts — #90 touched
docs/handbook.html, tests/test_repo_hygiene.py and
.github/workflows/tests.yml only, none of which #91 had touched yet, and
tests/test_repo_hygiene.py's own two cap edits (AGENTS.md 374->381, README.md
1219->1225) sit far enough from #90's pure append (the new test at EOF) that
git auto-merged the file with no help.

THE FIX. Check (4) now compares CONTENT, never history: the footer names
`README.md blob <hash>`, and the test asserts that hash equals
`git hash-object README.md` of the WORKING TREE file — no commit, no diff, no
`git cat-file -e` existence probe, no skip branch for a shallow clone. Two
files with identical bytes hash identically regardless of which commit either
one is, or is not, on; a squash-merge cannot orphan a blob hash the way it
orphans a feature-branch commit, because the blob IS the README, checked out
fresh on every clone at every depth. `HANDBOOK_README_SHA` (matched `README.md
at <hex>`) is now `HANDBOOK_README_BLOB` (matched `README.md blob <hex{40}>`,
full-length only, so it can never pick up one of the two commit shas sitting
in the prose sentence beside it). The footer keeps its history as PROSE —
"First cut from README.md at c093813 (#71); regenerated at cbca5b4 (#89)" —
which is not machine-checked and does not need to move for a small sync fix
like this one; only the blob hash moves, to this branch's final
README.md (e2598c1).

The test's docstring drops the now-false claim that check (4) skips itself in
a shallow clone (it never skips now, and needs no history to be exact in one)
and says what changed and why, citing #91 alongside #89's original
precedent. `.github/workflows/tests.yml`'s three `fetch-depth: 0` lines are
UNTOUCHED per Brett Heap's instruction — they stay a generally useful default
for whatever history-based check this suite grows next — only their comments
change, to say the handbook's own check no longer depends on them.

THE PAGE ITSELF. `docs/handbook.html`'s `#park` section's "How the estate is
found" paragraph now says what README.md says as of #91: with no estate
around the cwd either, `park` PARKS EVERY ESTATE it finds instead of
refusing (Brett Heap's RULING of 2026-09-10, #91, superseding ruling 3 of #82
for this one case), listing what it found and running itself on each in name
order, continuing past a refusal. The page's own `<span class="refuses">`
callout beside it — previously "Nothing is parked by guess: ... park REFUSES"
— is repurposed rather than deleted, since it is `resume`'s bare form that
still genuinely refuses now: "`resume` keeps the old refusal ... rebuilding
every estate on a fresh machine by accident is the opposite risk from `park`
failing to park the one you meant." Same `<p>` count before and after (two),
no `<section>`, `id`, or `<a class="src">` touched, so checks 1-3 and every
`dt`/`dd`/`div`/`ul`/`li`/`h2`/`h3`/`h4` count on the page stay balanced —
verified with a small tag-count script, not just by eye.

Evidence: `python3 -m pytest tests/test_repo_hygiene.py -q` 46 passed (was
red on `test_the_handbook_follows_the_readmes_outline` before this commit, on
the merge alone). `python3 -m pytest tests -q`: 892 passed, 1 skipped.
`python3 -c "import yaml; yaml.safe_load(open('.github/workflows/tests.yml'))"`
parses clean. A throwaway corrupted blob hash was confirmed to fail the guard
with a clear diff before being reverted, so the assertion is proven to bite,
not just to pass.

Lane: xfactory-2 (xFactory-2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@brettheap
brettheap deleted the feat/park-resume-make-targets branch September 11, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants