Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3f47747
family.py: `init` refuses to land inside a checkout of openRepoShape …
Oct 12, 2026
3e07770
family.py and siblings.py: every command printed for pasting spells i…
Oct 12, 2026
5b59233
family.py: `add`, `bump` and `remove` refuse to commit onto the holde…
Oct 13, 2026
9a70871
Merge origin/main (03dc729b) into fix/family-commands-and-branch
Oct 13, 2026
10c93de
Merge origin/main (5a253877) into fix/family-commands-and-branch
Oct 13, 2026
1f0161c
Merge origin/main (9736424e) into fix/family-commands-and-branch
Oct 13, 2026
4bfa6b5
family.py, siblings.py, setup-project.py: a holder that kept an older…
Oct 13, 2026
c47102d
family.py and the holder's docs: the holder's AGENTS-shape.md and REA…
Oct 13, 2026
bb11b63
Merge origin/main (48585e62) into fix/family-commands-and-branch
Oct 13, 2026
11ae0a8
family.py, family-root README.md, test_family_printed_quoting.py: the…
Oct 13, 2026
3451a56
Merge origin/main (994be398) into fix/family-commands-and-branch
Oct 13, 2026
8e1471c
Merge branch 'main' into fix/family-commands-and-branch
brettheap Oct 13, 2026
708f3bb
Merge origin/main (aa4e1f59) into fix/family-commands-and-branch
Oct 13, 2026
fcbc7a2
Merge origin/main (47b1e06c) into fix/family-commands-and-branch
Oct 13, 2026
22733ce
Merge branch 'main' into fix/family-commands-and-branch
brettheap Oct 13, 2026
5a2776d
Merge origin/main (f391e58d) into fix/family-commands-and-branch
Oct 13, 2026
654df49
Merge origin/main (8cd7dbe5) into fix/family-commands-and-branch
Oct 13, 2026
e42ef74
Merge branch 'main' into fix/family-commands-and-branch
brettheap Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,33 +368,34 @@ nothing. Use one when the parts DEPLOY SEPARATELY; one project is what parts
that ship together already are.

```sh
python3 scripts/family.py init --org <org> --family <Name> [--reuse-empty-repo]
python3 scripts/family.py init --org <org> --family <Name> --into .. [--reuse-empty-repo]
git -C <path> switch -c family/add-<project> # a branch first: see 3
python3 scripts/family.py add --family-root <path> --member <org>/<Project>
python3 scripts/family.py bump --family-root <path> --member <Project> --to <sha>
python3 scripts/family.py siblings --family-root <path>
```

1. **`init` creates a repository, so the human must say so first.** There is
no prompt here — `family.py` is not `setup.sh` — which makes it your job
to get an explicit yes before you run it, and to run `--dry-run` first and
show them the plan. If `<org>/<Name>` already exists and is EMPTY, add
`--reuse-empty-repo`; if it has commits, stop and ask.
1. **`init` creates a repository, so the human must say so first.** No
prompt asks — `family.py` is not `setup.sh` — so get an explicit yes, and
run `--dry-run` first and show them the plan. If `<org>/<Name>` exists and
is EMPTY, add `--reuse-empty-repo`; if it has commits, stop and ask.
**It lands at `<into>/<Family>/<Family>`** — `--into` is the PARENT
directory and defaults to where you are standing, the folder `<Family>/`
is created in it, and the holder goes inside that. Standing in the family
folder already, it does not nest a second one. `--work-dir <dir>` is the
override (`<dir>/<Family>`, no folder) and the two are refused together.
directory, the folder `<Family>/` is made in it (not again when you stand
in it already) and the holder goes inside that. `--into` defaults to where
you stand, refused inside a checkout of this standard, hence `--into ..`
above (`family-landing-in-standard`). `--work-dir <dir>` is the override
(`<dir>/<Family>`, no folder) and the two are refused together.
Say the landing path back to the human: the doubled `<Family>/<Family>` is
deliberate and reads like a mistake until somebody explains it.
2. **A family pins ASSEMBLY ROOTS, never legs.** `--member <org>/<Project>`,
never `<Project>-spec`. The tool refuses a leg, and the refusal is
correct: a leg has no `project.yaml` and belongs to its own root.
never `<Project>-spec`: a leg has no `project.yaml`, and is rightly refused.
3. **Each of `add`, `bump` and `remove` writes ONE commit** moving the
gitlink and the pin together. Land it as a pull request; never suggest a
push to the default branch. Do not hand-edit `family.yaml`'s `members:`
gitlink and the pin together, on a branch you make first (see above): the
tracking branch is refused (`family-on-tracking-branch`). Push the branch
as its `NEXT` line says and land it as a pull request; never suggest a
push to the default branch. Never hand-edit `family.yaml`'s `members:`
block — the tool rewrites it wholesale. `bump` takes the same repeatable
`--trailer "<Key>: <value>"` as `update-shape.py apply`, for the lines its
commit must end with.
`--trailer "<Key>: <value>"` as `update-shape.py apply`.
4. **The member must be scaffolded or adopted FIRST.** A family cannot make a
project out of a repository; it can only pin one that already is.
5. **`make siblings` in the holder — or `family.py siblings --family-root
Expand All @@ -407,11 +408,10 @@ python3 scripts/family.py siblings --family-root <path>
parent folder is not named after the family it warns and prints an `mv`,
and running that `mv` yourself relocates a checkout out from under a
shell, an editor and every agent lane, and breaks any linked worktree
(its `.git` file carries an absolute path). It also never touches an
existing clone beyond `git fetch`: no checkout, no reset, no pull,
because somebody is working in there. A directory that is a clone of a
different repository is reported and skipped; the exit is the human's
hand, not an overwrite.
(its `.git` file carries an absolute path). An existing clone gets only
`git fetch` — no checkout, no reset, no pull — because somebody works in
there; one of a different repository is reported and skipped, and the
exit is the human's hand, not an overwrite.
6. `update-shape.py` updates a family root exactly as it updates a project,
with the same four refusals, and mirrors into `family.yaml`. An existing
holder receives `scripts/siblings.py` that way: `check` reports it as
Expand Down
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -900,27 +900,27 @@ InkRouter/ the family FOLDER (see the layout above)

```sh
python3 scripts/family.py init --org InkRouter --family InkRouter --into .. --reuse-empty-repo
git -C ../InkRouter/InkRouter switch -c family/add-irrs
python3 scripts/family.py add --family-root ../InkRouter/InkRouter --member InkRouter/IRRS
python3 scripts/family.py bump --family-root ../InkRouter/InkRouter --member IRRS --to <40 hex>
python3 scripts/family.py remove --family-root ../InkRouter/InkRouter --member IRRS
python3 scripts/family.py siblings --family-root ../InkRouter/InkRouter
```

`add`, `bump` and `remove` each write ONE commit, with explicit pathspecs,
moving the gitlink and `members[].pin` together — the same lockstep rule an
assembly root applies to its legs, and `scripts/validate-family.py` refuses
when they disagree. Each row also records the member's own `project.yaml`
`id`, so the validator can check the tree mounted at `members/<Project>` is
the project the row claims rather than merely a repository at the right
commit. **A family pins assembly roots, never legs**: adding `<Project>-spec`
is refused, because a leg has no `project.yaml` and belongs to its own root.
`add`, `bump` and `remove` each write ONE commit, with explicit pathspecs, on a
branch made first (the tracking branch is refused) and print its push: the
gitlink and `members[].pin` move together, the lockstep rule an assembly root
applies to its legs, which `scripts/validate-family.py` enforces. Each row
records the member's `project.yaml` `id`, so the validator can check
`members/<Project>` holds the project the row claims, not merely a repository
at the right commit. **A family pins assembly roots, never legs**:
`<Project>-spec` is refused; a leg has no `project.yaml`.

`make bootstrap` in the holder fetches every member and its legs — resolving
the same GitHub App or `SHAPE_LEGS_TOKEN` credential the workflow does,
because a member is a private assembly root that mounts two private legs of
its own — and then runs each member's own `make bootstrap`. `make validate`
runs the family validator and then each member's `make validate`. A member
that could not be fetched is reported and skipped, never a failure.
the GitHub App or `SHAPE_LEGS_TOKEN` credential the workflow does, as a member
is a private assembly root mounting two private legs — then runs each member's
own `make bootstrap`; `make validate` runs the family validator, then each
member's. A member it cannot fetch is reported and skipped, never a failure.

The holder carries the same COPY pin an assembly root does, so
`update-shape.py` re-syncs it the same way; it mirrors into `family.yaml`
Expand Down
23 changes: 20 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,9 +711,11 @@ options:
create a bare repository here instead of calling `gh
repo create` (the TEST path; no network)
--into DIR the PARENT directory the family folder is made in
(default: the directory you are standing in). The
holder lands at <into>/<Family>/<Family>, beside where
`make siblings` clones the members.
(default: the directory you are standing in, which is
refused inside a checkout of openRepoShape: from its
root, pass `--into ..`). The holder lands at
<into>/<Family>/<Family>, beside where `make siblings`
clones the members.
--work-dir WORK_DIR THE OVERRIDE: land the holder at <work-dir>/<Family>
and make no family folder at all. Mutually exclusive
with --into.
Expand All @@ -730,6 +732,11 @@ usage: family.py add [-h] --family-root FAMILY_ROOT --member ORG/PROJECT
[--at COMMIT] [--local-remote-dir LOCAL_REMOTE_DIR]
[--trailer "KEY: VALUE"] [--dry-run]

Mount and pin a member. Its ONE commit lands on the branch the holder stands
on: the real run refuses the holder's tracking branch and a HEAD on no branch,
naming the `git switch -c` that makes one, and ends by printing the `git push`
of that branch. --dry-run runs on any branch.

options:
-h, --help show this help message and exit
--family-root FAMILY_ROOT
Expand Down Expand Up @@ -759,6 +766,11 @@ options:
usage: family.py bump [-h] --family-root FAMILY_ROOT --member PROJECT
--to COMMIT [--trailer "KEY: VALUE"] [--dry-run]

Move a member's gitlink and pin together. Its ONE commit lands on the branch
the holder stands on: the real run refuses the holder's tracking branch and a
HEAD on no branch, naming the `git switch -c` that makes one, and ends by
printing the `git push` of that branch. --dry-run runs on any branch.

options:
-h, --help show this help message and exit
--family-root FAMILY_ROOT
Expand All @@ -783,6 +795,11 @@ options:
usage: family.py remove [-h] --family-root FAMILY_ROOT --member PROJECT
[--dry-run]

Unmount a member and drop its row. Its ONE commit lands on the branch the
holder stands on: the real run refuses the holder's tracking branch and a HEAD
on no branch, naming the `git switch -c` that makes one, and ends by printing
the `git push` of that branch. --dry-run runs on any branch.

options:
-h, --help show this help message and exit
--family-root FAMILY_ROOT
Expand Down
12 changes: 8 additions & 4 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -1281,12 +1281,14 @@ <h3>The InkRouter example</h3>
…six more, added as they arrive
IRRS/ IRSS/ … the same members as WORKING clones, on main</code></pre></div>
<div class="code" data-lang="sh"><pre><code>python3 scripts/family.py init --org InkRouter --family InkRouter --into .. --reuse-empty-repo
git -C ../InkRouter/InkRouter switch -c family/add-irrs
python3 scripts/family.py add --family-root ../InkRouter/InkRouter --member InkRouter/IRRS
python3 scripts/family.py bump --family-root ../InkRouter/InkRouter --member IRRS --to &lt;40 hex&gt;
python3 scripts/family.py remove --family-root ../InkRouter/InkRouter --member IRRS
python3 scripts/family.py siblings --family-root ../InkRouter/InkRouter</code></pre></div>
<p><code>add</code>, <code>bump</code> and <code>remove</code> each write ONE commit, with
explicit pathspecs, moving the gitlink and <code>members[].pin</code> together &mdash; the
explicit pathspecs, on a branch made first (the tracking branch is refused) and print its
push, moving the gitlink and <code>members[].pin</code> together &mdash; the
same lockstep rule an assembly root applies to its legs, and
<code>scripts/validate-family.py</code> refuses when they disagree. Each row also records the
member&rsquo;s own <code>project.yaml</code> <code>id</code>, so the validator can check the
Expand Down Expand Up @@ -1873,9 +1875,11 @@ <h2>For AI assistants</h2>
from the pipe, added (#492); the statements the v1.0.0 accuracy pass found false, corrected
here as in the README, added (#483); a taken clone path refused up front, added (#498);
<code>--reuse-empty-repo</code> reusing an empty leg and a leg holding the scaffold&rsquo;s
own seed, and the one ruleset exit that re-run is, added (#462) &mdash; a page cannot name
the commit that contains it, and the blob below is the fact a test checks.
README.md blob 8c658efcc9ca35144eb9be15cb1a9b7059041e30. Every command, flag, exit code
own seed, and the one ruleset exit that re-run is, added (#462); <code>family.py</code>
<code>add</code>, <code>bump</code> and <code>remove</code> committing on a branch made first,
never the tracking branch, and printing the push of it, added (#487) &mdash; a page cannot
name the commit that contains it, and the blob below is the fact a test checks.
README.md blob ab6ec9b00c1b7d0d4f1d0be075f2d3afcf8d8a3f. Every command, flag, exit code
and file name on this page is quoted from that README and from AGENTS.md.</p>
</footer>
</main>
Expand Down
Loading
Loading