Repository navigation
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
Conversation
…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>
|
@codex review |
There was a problem hiding this comment.
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.
Reviewer's GuideThis PR adds thin Sequence diagram for assembly root park and resume delegationsequenceDiagram
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
Sequence diagram for family holder verb dispatchsequenceDiagram
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
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
🟡 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/resumeMakefile 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.pywith--make <target>/--make-argto runmake <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.
| 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>
|
There was a problem hiding this comment.
🟢 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
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
… 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>
…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>
… 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>



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 parkandmake 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, itexecs the script with$(ARGS), somake park ARGS=--dry-runis 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 withError <code>in its diagnostic, so the extension's 0/1/2/3 cannot come back out ofmakeunchanged — 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 andproject.yamlid — gains--make <target>and repeatable--make-arg: for eachfamily.yamlmember it runsmake <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 — amake resumeon a fresh machine where nobody ranmake siblingsmust not exit 0. With--make, siblings.py clones nothing and fetches nothing. The pinned copies undermembers/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-changedthroughupdate-shape.py apply: the assembly root'sAGENTS-shape.md(park before you leave a workstation; clone,make bootstrap,make resumeon the next; never commit underworktrees/) and the holder'sAGENTS-shape.md,Makefileandscripts/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;resumerefuses on divergence; nothing underworktrees/is ever committed or synced; native Windows parks nothing, the PowerShell mirror lacks it) and AGENTS.md a## Parking and resumingprocedure. 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 with3in the diagnostic),.PHONYand 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-argcarryingARGSwithMAKEFLAGSemptied, dry-run, no clone/fetch,--make-argalone refused.test_scaffold_e2e.py::test_make_targets_existgains both targets. Full suite: 714 passed, 1 skipped.Follow-up not in this PR:
docs/handbook.htmllags 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:
make parkandmake resumein assembly roots, delegating worktree transfer to the Speckit overlay.Bug Fixes:
members/copies instead of working clones.Enhancements:
Documentation:
Tests:
Chores: