Repository navigation
openRepoShape v1.0.0
openRepoShape v1.0.0
openRepoShape is the convention, as data and as running code, for a project
that spans several repositories: what they are called, how they are pinned to
each other, how one clone brings the whole project down, and what an AI
assistant does when asked to scaffold one. Its preferred shape, the Triad, is an
assembly root that mounts a spec leg and a code leg. The shape is elective and
confers nothing. Version 1.0 promises that every procedure in AGENTS.md works
end to end for ordinary repositories on Linux, macOS and Windows. On Windows,
make park and make resume run under WSL2, as the README says: native
Windows parks nothing. The release is tagged only when every in-scope issue is
closed and the full test suite passes on all three systems on the release
commit. Edge cases that only unusual input reaches
are listed below as known limits.
What 1.0 contains
- New project:
openRepoShape <Project> --org <org>, or./setup.sh, or
py setup-project.pyon Windows. It runs a preflight, checks the names,
shows the plan and asks for one typedyes. - Direct scaffold:
scaffold-project.py. Add--pin open<Product>@<commit>
for a declared descendant. - Adopt in place:
adopt-project.py plan, thencheck, thenexecute. - Compliance check:
openRepoShape --doctor [<path>]or
shape-doctor.py --root <path>. - Stay current:
update-shape.py check, thenapply. - Advance a leg:
scripts/bump-leg.py --root <path> --leg spec|code --to <40-hex>. - Families:
scripts/family.py init|add|bump|remove|siblings. - Park and resume:
make parkandmake resume. - Each project's own gate:
make bootstrapandmake validaterun pinned
copies of the scripts, so the gate never needs this standard.
Requirements and platforms
gitand Python 3.9 or newer. The tools use the standard library only.gh, logged in, wherever a tool creates repositories or pull requests on
GitHub.git-filter-repois needed for adoption only.makeis optional.openRepoShape --preflightchecks the machine and creates nothing. On
Windows, runpy setup-project.py --preflight. It names what is missing,
and offers to installgh, log it in and set up its credential helper,
each behind a typedyes.setup-project.py --preflightalso offersgit.- Linux and macOS are supported, and the stock macOS
/bin/bash3.2 is
enough. Native Windows usessetup-project.py, after
git config --global core.autocrlf false. WSL2 runs the bash entry
points. Park and resume do not work on native Windows; run them under
WSL2. - CI runs the full suite on
ubuntu-latest,macos-latestand
windows-latest.
How to pin 1.0.0
A project pins the standard in contracts/shape-pin.yaml, which always
records a 40-hex commit. Name bfed143e89f97d3480cf5373051cb1ace82db312 itself rather than the tag: a
tag can be moved and a commit cannot, as the README and AGENTS.md say of every
pin.
-
New project: run
OPENREPOSHAPE_REF=bfed143e89f97d3480cf5373051cb1ace82db312 openRepoShape <Project> --org <your-org>.
The command forwards--shape-ref bfed143e89f97d3480cf5373051cb1ace82db312tosetup.sh. Outside a
checkout,setup.shandsetup-project.pytake--shape-refdirectly. -
Existing project or family holder: run these from a checkout of the
standard.checklists the files that change.applysays how many it
will re-copy and asksType yes to proceed:before it writes anything:./update-shape.py check --root <path> --at bfed143e89f97d3480cf5373051cb1ace82db312 ./update-shape.py apply --root <path> --at bfed143e89f97d3480cf5373051cb1ace82db312 \ --branch shape/update-bfed143e89f97d3480cf5373051cb1ace82db312 git -C <path> push -u origin shape/update-bfed143e89f97d3480cf5373051cb1ace82db312
Then open a pull request from that branch.
-
Confirm: run
./shape-doctor.py --root <path>from a checkout at
bfed143e89f97d3480cf5373051cb1ace82db312.COMPLIANT, exit 0, means the pin names that commit.
What changed on the way to 1.0
adopt-project.py
- In-place adoption. The repository becomes the assembly root and keeps
its name and history. The legs are extracted withgit filter-repo, and the
split lands as one pull request. Every source path is verified by blob
sha, a kept root.gitattributesas the source's bytes plus the block
below.
The plan asks about any path it cannot place, an empty leg is seeded with
--allow-empty-leg, and thexf-project-<id>topic is set
(#5, #16, #17, #24). - Branches and pins. The tracking branch defaults to the source's default
branch, andcheckandexecuterefuse one that is no branch of the
source (plan-tracking-branch-missing). The topic is set before the pull
request, and NEXT namesscripts/bootstrap.pywhen the source kept its own
Makefile (#493).plan --pintakes<product>@<commit>, andexecute
writes the pin file the scaffold writes, so the adopted root's
validate-pins.pypasses; a--pin-sourcethat cannot answer is
pin-source-unreadable(#508). A pinned product such as007is listed
inproject.yamlas text, not as the number 7 (#515). - The source's own name.
planrefuses an--organd--projectthat
name another repository than the source's, compared without case
(adopt-not-the-source).checkandexecuterefuse a plan edited to
name one (plan-not-the-source), and a source whose own name the naming
policy refuses is told to rename it first (#513). - Keywords as typed.
planwrites a value bare only where the project's
own YAML loader reads it back as that text, so--spec-path null,
--id nulland--tracking-branch true, on a source with a branchtrue,
adopt as typed. Before, the first mounted the leg atspec, the second made
the idnorthwind, and the third was refused about a value nobody typed. An
explicit null leg path in a plan (spec_path: null) is a
plan-bad-leg-pathfinding, not the defaultspec. A backstop
(adopt-manifest-misread,plan-manifest-misread) refuses, before any leg
exists, a value the loader would read back as something else from the line
executewrites. The shipped templates quote those lines, so it names none
today (#518). - Refused before any leg exists.
executerefuses everythingcheck
finds before it creates a leg. That covers bad mount paths, root paths that
clash with the shape's files, the source's ownproject.yamlkept in the
root, a symlink with an empty target, values no file can hold
(adopt-unwritable-value), and paths it cannot extract, whichplannow
asks about. A split branch an earlier run left, or a branch in its way, is
adopt-branch-existsorplan-split-branch-taken, and a hand-edited key
of the wrong type, or a date the tools cannot read, is a finding, not a
traceback
(#170, #187, #198, #231, #270, #313, #353, #356, #377, #382, #387, #435). - Symlinks the root keeps.
checkfollows each kept symlink that a
path the shape writes runs through. One the shape cannot write through,
because it dangles, leads out of the tree, enters a leg or a submodule,
loops, or carries the shape's scripts out of their directory, is a
plan-root-symlinkfinding, andexecuterefuses it before any leg
exists. Where git checks a symlink out as a file,executealso refuses
one the shape writes beneath (adopt-symlinks-off). Such links used to
break the run after both legs were pushed, or put the shape's files
outside the assembly. A link at a shape path to something that is there
still gets the copy beside it (#504). - A source's own
.gitattributes. A source that keeps one at its root
gets one marked block of/<copy> text eol=lflines appended to it in
the split, a line for each pinned copy outsideshape/, so a Windows
clone writes those copies with the bytes the pin records. The
verification counts that file as the source's bytes followed by exactly
that block, andupdate-shape.py apply --addextends the block (#511). - Submodules.
.gitmodulesstays in one leg with its submodules. An
unreadable one is aplan-gitmodules-unreadablefinding, not a crash, and
relative URLs are checked from both sides. Verification accepts no root
gitlink but the two leg mounts, each at the commit pushed to its leg
(adopt-root-submodule,adopt-mount-commitand others).execute
records inproject.yamlthe leg each kept submodule went to, and the
doctor holds it there
(#162, #173, #194, #226, #240, #245, #268, #394). - Ignore rules and odd names. Shape files are committed even when the
source's.gitignorematches them, and leg mounts use--force. Findings
spell out control characters and bytes that are not UTF-8, in a
hand-editedquestion:and a submodule's name too, follow-up searches take
paths literally, and a cp1252 console gets escaped names
(#182, #200, #223, #228, #246, #247, #378, #384). - Late failures. After a leg exists, a failure prints
REFUSED <step>.
and what the run left behind (#445). Exit 1 means the verification failed.
Exit 2 withIS pushedis finished by hand with the command it prints,
never by re-running (#371).
update-shape.py and bump-leg.py
- Re-syncing copies.
checkgives each pinned file a verdict
(unchanged,upstream-changed,locally-modified,bothand others)
and names files the standard has added (upstream-added). It exits 1 when
there is work to do.applyre-copies and re-pins, and rolls back if the
project's validators fail.--addand--accept-localeach name one file
and can be repeated. Family holders, and their README'sShape:line, update the same
way, and the commit and refusals name the holder's own manifest
(#10, #29, #47, #52, #85, #152, #159). - Local edits and merged copies. No NEXT line,
check's or the
doctor's, passes--accept-local, so a locally edited copy meets
update-local-drift, which leaves the choice to a person. Ashape/
collision copy merged into its original and removed ismerged-away, and
applydrops its pin row (#512). - Landing.
apply --branchtakes--trailerlines, and a non-ASCII
trailer is committed as typed under any locale. Before it writes,
apply --branchrefuses a pin, manifest or copy that differs from HEAD
(update-branch-uncommitted). Thegh pr createit prints targets the
root's own branch and repository, and every NEXT line quotes its paths for
the shell. A failed push keeps the commit and prints a NEXT line. A later
--prrun opens the pull request, unless the tree has changed since
(update-rerun-uncommitted)
(#112, #218, #227, #374, #376, #379, #474). - Clean rollback. A failure before the commit restores everything.
Additions through links or under files are refused (update-add-symlink,
update-add-not-a-directory), and so is a rewrite of a copy, the pin or
the manifest kept as a symlink (update-rewrite-symlink;checkcalls
such a copycopy-is-link). A shape pin whosefiles:is not a list is
refusedupdate-pin-row, and the project's validators report
shape-pin-row, where all of them ended in a traceback. Pin rewrites keep
layout and comments, and non-ASCII project names work under cp1252
(#224, #233, #269, #281, #343, #346, #347, #380). - bump-leg.py. One commit moves a leg's gitlink, pin and workflow
references, and--dry-runshowsold -> newand the trailers first. The
dry run also runs on the tracking branch, and names thegit switch -cthe
real run needs. Untracked workflows are refused
(bump-leg-workflow-untracked), and unreadable files are refused by name.
Case-only renames are followed, and any failure rolls back. After a bump,
make bootstrapmoves the leg's tracking branch up to the new pin when
that branch is behind it
(#35, #151, #222, #264, #344, #352, #403, #484, #490).
setup, scaffold, family and the shared helpers
-
Front door.
setup.shself-bootstraps from upstream with an explicit
--org, so no fork is needed. It is a shim oversetup-project.py, the
native Windows way in.openRepoShapeinstalls as a command. The preflight
offers installs behind a typedyes.--local-remote-dirrehearses a run
with nothing created on GitHub, and--familylands the clone in a family
folder.setup.sh --helpoutside a checkout answers without cloning
anything. Every question is asked at the person's own terminal, even under
curl … | bash; with no terminal at all, the run refuses and names that
terminal as the way on. A clone path that is already taken is refused
before anything is created
(#2, #3, #14, #37, #53, #56, #57, #60, #62, #65, #75, #78, #452, #491, #509). -
Naming and descent. Names are classified offline against six families.
A descendant must pin itsopen<Product>, directly or through a recorded
chain. The defaultreference:follows the election date. The Triad
advisory speaks once and never blocks, andsingle-repository.yamlsilences
it. A--link-sourcevalue isPRODUCT=PATHonly when its key names a
chain link, and a link source that cannot be entered is a warning, not a
traceback (#4, #6, #19, #27, #42, #45, #86, #164, #453). -
Scaffold refusals before anything exists, dry runs included:
- colliding leg paths (
scaffold-leg-path-collides); - pins the shape cannot write, or two products that share one
SHAPE_PIN_SOURCE_<PRODUCT>variable (scaffold-pin-collides); - bad chain links;
- manifest values the project would misread, and text no file can hold;
- a
--tracking-branchgit would not take as a branch name, here and in
family.py init.
Shape files and leg mounts get past the operator's own ignore rules, and
templates are filled in one pass. They quote every text value they write,
so--id 007or--tracking-branch 2026reads back as typed, and a leg
path any template line would misread is refused. A--pinproduct such
as007is listed as text, so the root's ownvalidate-pins.pypasses
(#185, #201, #234, #235, #256, #266, #271, #314, #316, #351, #355, #395, #409, #517). - colliding leg paths (
-
Ruleset refusals.
--reuse-empty-reporeuses a leg that is empty or
holds only the scaffold's own seed. The ruleset hint offers that one re-run,
by an operator holding the bypass right, and names the run's tracking
branch, notmain(#373, #506). -
Each project's own gate.
make bootstrapplaces each leg at its pin,
and.gitattributeskeeps LF line endings.AGENTS-shape.mdcarries the
rules for assistants, and a<user>-wiptemplate ships too. Thevalidate
workflow reads private legs with a GitHub App orSHAPE_LEGS_TOKEN, and
carries on with a warning without one.validate-pins.pyrechecks
neutral-product pins from a local checkout where it can, else through
gh apiwith the operator's credentials, as the docs now say. It refuses
git-not-runnable(exit 2), and a workflow file it cannot read as UTF-8
is oneworkflow-unreadablerefusal naming every such file.
make bootstrapescapes what the console cannot print, so anamesuch as
日本underPYTHONIOENCODING=asciino longer ends it in a traceback
(#1, #7, #8, #12, #13, #15, #18, #25, #30, #58, #154, #312, #419, #446, #456, #465). -
Families. A holder pins member roots under
members/, and
make siblingsclones working copies beside it without moving anything.
initasks the holder's own reader and validator aboutfamily.yaml
before it creates anything.add,bumpandremovehave--dry-run.
They check before they write, and restore the holder on failure.add
refuses duplicates by name or id (member-already-present,
member-duplicate-id), andbump --toan annotated tag, a tree or a blob
is refusedmember-to-not-a-commit. A repeatedbumpsaysnothing to do
and exits 1.make siblings,parkandresumerefuse by name when
scripts/siblings.pyis missing.initrefuses its default landing
inside a checkout of the standard (family-landing-in-standard), and
add,bumpandremoverefuse the holder's tracking branch
(family-on-tracking-branch) and end with a quoted NEXT that pushes
their own branch
(#16, #79, #219, #221, #236, #308, #315, #317, #318, #345, #350, #381, #397, #399, #400, #406, #416, #466, #503, #507). -
Park and resume.
make parkandmake resumehand off to the Speckit
git extension. The installed commands moved to opensoft/openRepoTools
(#80, #83, #93, #94). -
Shared helpers. The YAML reader refuses a file it cannot open or
decode as UTF-8 (yaml-unreadable) and text it cannot parse
(yaml-unparsable), and several flow and quoting forms it used to misread
are now read exactly or refused. Output from git andghis read as UTF-8
under any locale. "Same repository" now has one definition
(#156, #160, #193, #230, #244, #257, #265, #284, #390). A refusal built
without a remedy of its own prints itsREFUSEDline alone. Before, each
tool printed the assembly root's lockstep remedy under it, in a family
holder too, and the doctor could offer it as a NEXT. The root's
validate-pins.pystill prints it.family.py addrefuses a
.gitmodulesgit cannot read asgitmodules-unreadable, as the holder's
gate does (#494).
shape-doctor.py
-
One verdict.
openRepoShape --doctorfetches nothing into the
repository, and writes nothing there but a--placement-planfile you
name. Run outside a checkout of the standard, it clones the standard
into a temporary directory and keeps it, naming it on the report's last
line, so a NEXT that names a file in it still runs after the command
exits.--jsonand--placement-plan <file>pass through to
shape-doctor.py(#510). It prints a row per check, then one verdict:COMPLIANT: exit 0;COMPLIANT, SHAPE BEHIND,DRIFTED,MISPLACEDorINVALID: exit 1;NOT A SHAPE ROOT: exit 2;CANNOT ANSWER: exit 3.
Only a
FINDINGmoves the verdict. The machine check is now--preflight
(#96). -
Placement. The
placementrow checks each leg's paths against the
adoption policy.--placement-planwrites a plan for a person to resolve
(#100, #186). -
NEXT commands you can paste. A NEXT is the command alone; the sentence
that explains it is aWHYline of its own, andwhyin--json. They
are quoted for sh or PowerShell, useCopy-Item -LiteralPathon Windows,
and set thePYTHONPATHa validator needs. A gitlink committed by hand is
offered both repairs,bump-leg.py --tothe gitlink's commit or the pin's
(#102, #106, #107, #108, #110, #287, #311, #354, #402, #495). -
More accurate verdicts. An unloadable manifest or shape pin is now
INVALID, where it wasNOT A SHAPE ROOTorCANNOT ANSWER, and a root
in the middle of a merge isINVALID (merge), exit 1, where it could read
COMPLIANT. A misplacedfamily.yamlis amanifest kindsfinding, and a
crashed validator says so. A pin that does not load is put back from git,
where the NEXT said to remove it, and a conflicted shape pin is taken
alone, its conflicted copies left to a person to merge. A fork beside a
holder is no longer taken for the member
(#147, #220, #232, #267, #357, #358, #375, #404, #468, #496).
The test suite, CI and the docs
- Three operating systems. Every pull request runs the full suite on
Linux, macOS and Windows, and the Windows job installsgit-filter-repo,
so the adoption suites run there too. Every push tomainruns it on
Linux. actionlint checks this repository's workflow and both shipped ones
(#9, #11, #53, #57, #124, #129, #336, #511). - Hermetic tests. No test reaches the real
gh(#274, #349), and the
suite scaffolds into local bare repositories, never GitHub. Tests no longer
depend on the host's paths, pin-source variables or git identity, and a
failing assert names what broke. The two session fixtures that clone a
scaffold, andsetup-project.py --local-remote-dir, clone a just-pushed
local remote through git, so git's background maintenance no longer breaks
the clone
(#64, #116, #120, #128, #131, #310, #335, #467). - Docs. The README has a quick start for each platform, and
docs/handbook.htmlis a one-page reading of it.docs/cli.mdis generated
from every tool's--help. A test ties each refusal code AGENTS.md names to
the tool that prints it, and the docs name$AGENT_PROTOCOL_ROOTrather
than a fixed path.adopt-project.py plan --helpsays what--spec-path
and--code-pathare, their defaults, and the two refusalsplanmakes on
them (adopt-bad-leg-path,adopt-leg-path-collides). An accuracy pass
corrected 28 statements in the README and AGENTS.md
(#23, #46, #70, #71, #73, #88, #90, #98, #161, #309, #396, #401, #505). - Maintainability. Static-analysis findings were fixed, long functions
split and docstrings corrected, with no change in behaviour
(#67, #117, #121, #125, #139, #140, #141, #142, #143, #144, #145, #398).
openRepoShape itself stays a single repository (#372).
Known limits
Each line names the input that triggers the limit, then what you see. Each
limit stays open under its area's tracking issue. A number at the end of a
line, such as (#524), is the issue in the v1.1.0 milestone that groups it
with others for one pull request.
adopt-project.py
- #388: A source symlink whose target begins with a NUL byte:
checkpasses, andexecutefails after both legs exist, with nothing on stderr. - #389: A plan that keeps the source's own
project.yamlin the root:checkandexecutenow refuse it before any leg exists, but verification never checks the root manifest itself, so nothing behind that refusal would catch it. - #417: A relative submodule URL under
--local-remote-dir:checkandexecuteresolve it from different remotes, so one can refuse what the other passes. - #418: A kept submodule whose URL names no repository:
adoption verified, then the recursive clone theNEXT:line prints fails. - #420: A hand-edited
adopt_branch:that git refuses as a branch name, such asa..b:checksaysplan ok, andexecutefails after both legs exist. A badtracking_branch:is caught bycheckasplan-tracking-branch-missing. - #425: A source whose default branch name is not UTF-8:
plan,checkandexecuteend in aUnicodeDecodeErrortraceback. - #427: An
originwhose organisation is not ASCII or holds a space:planrefusesunsafe-value: --org, a flag nobody passed. - #449: A hand edit that makes a neutral pin's
commit:andtree_sha256:disagree:adoption verified, then the root'svalidate-pins.pyfailsneutral-pin-digest-mismatch. - #451: A
--pin-sourcethat nothing reads, even a missing path:plan(and the scaffold's--dry-run) exit 0 without a word. - A moved or dropped non-ASCII path under
LC_ALL=C PYTHONUTF8=0:executeends in aUnicodeEncodeErrortraceback after both legs are pushed (#527). - A kept or dropped name holding a line break, a control character or a byte that is not UTF-8: it reaches the split commit message and the pull request body unspelled.
- A source with its own
single-repository.yaml:planasks the general top-level YAML question. A dedicated rule (drop or root, defaulting to drop) is planned (#528). - A source branch that differs from the split branch only by letter case, on macOS or Windows:
checkpasses, andexecutefails after both legs exist (#520). - A name holding a backtick or a double quote, or wrapped in backticks: its spelled form can name another entry, or make the plan unreadable.
- A path policy whose
question:or rule id holds control characters:plan's terminal report prints them raw. - A hand-edited
adopt_branch:such asa=b:checksaysplan ok, andexecuterefusesunsafe-valuebefore any leg exists (#520). - A hand-edited plan with a bad
id:ororg:beside a bad leg path:checkandexecutename different problems first. No leg is made either way. - A project adopted with a
--spec-pathother thanspec: the root Makefile still exportsCONTRACTS_DIR ?= $(CURDIR)/spec/contracts, so the code leg's tooling points at a directory that does not exist (#522). - A nested
--code-path, such aslegs/codebesidelegs/spec: the cross-leg follow-up names the spec leg../legs/spec/…relative to the code leg, where../spec/…is right (#522). plan,checkorexecuterun without--work-dir: each leaves anopenreposhape-adopt-*directory in the system temp folder, empty or holding the working trees, and nothing removes it (#526).plan --source <org/repo>without--work-dir, and the temporary clone cleaned beforecheckorexecute: they refusesource-unclonable, with text about the repository's name. Passing--sourceagain recovers (#526).- Any adoption: the follow-up
planprints, and the split commit lists, says to open the split as a pull request, whichexecutedoes itself against GitHub, and calls the extracted legs seeded (#523). execute --local-remote-dir: it ends withNEXT: review the pull request, though none was opened (#523).- An answer in the plan with no
resolution:beside it:checksaysplan ok, though AGENTS.md says such an answer will be read as a finding (#523). - A verification that fails against GitHub (exit 1): the pull request and the topic are already in place, and neither the report nor AGENTS.md says so. Deleting the split branch, as the way out says, closes the pull request (#523).
planwith no--elected-byand nogit config user.name: it is refusedadopt-manifest-invalid, naming--elected-by, a flag nobody passed (#529).- A path the plan answered by hand, such as
spec/examples/: the doctor'splacementrow keeps listing it asreview_required, becauseexecuterecords only the submodule answers (#525). - A local source or assembly that hides a branch with
uploadpack.hideRefsortransfer.hideRefs:checkdoes not see it, andexecute --local-remote-dirpushes both legs before the split push fails (#520). - A submodule moved to the other leg on purpose after adoption: the doctor reads
MISPLACEDfrom then on, and nothing says how itssubmodules:row is changed. A row whose submodule is gone is never reported (#525). - A pinned product that only a standard YAML reader misreads, such as
yes,onor.5:executelists it bare, because only the project's own reader is asked. The project's validators read it as text. - A source cloned from a local clone or mirror whose path does not end in
<org>/<repo>:planrefusesadopt-not-the-sourceeven with the right--org, and says the name was read from the path (#521). - An
originthat is a Windows path with backslashes, or a URL with a trailing slash: it names no repository, so--organd--projectare not compared with it, and a mistyped--projectplans with exit 0 (#521). plan --source <org>/<repo>with another--project:adopt-not-the-sourcecomes only after the clone, though the name is known before it.- A hand-edited plan in the source's own refused name, such as
billing-service:checkreportsnaming-unclassifiedwithout the rename-first remedy thatexecutegives. - An
originremoved afterplan, orexecute --sourcea copy with noorigin: the plan's recordedsource:repository is not consulted, so a plan hand-edited to another name passes, andexecuteacts on that name. - A path
originsuch as…/straße/Atlas.gitwith--org strasse: the source check takes them for one repository, though GitHub folds ASCII case only. - A hand-edited
org:that is null, deleted,falseor0, for a source whose repository is literallyNone/<name>(orFalse/,0/):checksaysplan okwhere a textorg:findsplan-split-branch-taken.executestill refuses. - A file the root keeps at
contracts/shape-pin.yaml, or a link there to a declared pin's file:checkpasses,executefailsadopt-shape-pin-unreadableafter both legs are pushed, and the first bootstrap fails (#524). - A declared pin's file reached through a root-kept link, such as
contracts -> governancewith agovernance/openglass-pin.yaml:checkandexecutepass, and the first bootstrap failsyaml-unparsableorneutral-pin-unreadable. - A link at a path the shape writes to a name the clone's
.gitdoes not hold underobjects/,refs/,logs/ormodules/, such asCLAUDE.md -> .git/refs/remotes/origin/main:checkpasses, andexecutewrites the shape's file into the clone's.gitand saysadoption verified, or, underobjects/, fails after both legs exist. - A link at a path the shape writes whose target climbs back out of
.gitor a directory in it, such asCLAUDE.md -> .git/refs/../HEAD: it is refusedplan-root-symlinkbefore any leg, saying.git/refsis not a directory in the assembly root, though the shape's copy could go beside it. - A kept link above a path the shape writes, where git checks a symlink out as a file:
check'splan-root-symlinksays where the link leads, though the clone holds it as a file, and its "change where it points" leads on toexecute'sadopt-symlinks-off(#531). - A link at a path the shape writes to
.git/reftable, on git 2.45 or later with the reftable backend: it is refusedplan-root-symlink, though the shape's copy could go beside it. Untested: read from the code. - A hand-edited plan with a bare number or boolean in a text field (
id: 007,tracking_branch: true): it is turned into text ("7","True") without a word, andcheckandexecutepass. - A hand-edited pin whose product holds a tab:
checkreports two findings about the one value,plan-pin-malformedandplan-unwritable-value. No leg is made. - A root-kept link at a shape copy's path, such as
shape/README.md -> ../README.md:check'splan-root-blocks-shape-copysaysexecutewould write through it after both legs are pushed, butexecuterefuses it before any leg exists. - A
--work-dirthe user cannot write:checkpasses, andexecuterefusesextracting the spec legafter both leg repositories are pushed (#524). - On Windows, a source path holding a character NTFS refuses (a TAB, a control character): it cannot be checked out there, so adopting it is untested on Windows.
- On Windows, a source path that is not UTF-8: it cannot be created there, so adopting it is untested on Windows.
- On Windows, a source tree name holding a newline, a TAB or a backslash: its submodule plan is untested on Windows.
- On Windows, a source that tracks symlinks, without
core.symlinks=true: the links check out as plain files, so adopting one with a root-kept link the shape writes through, or a mount through a kept link, is untested on Windows. - A source whose root
.gitattributesis a symlink: the split appends no line-ending block, and git reads no.gitattributesthrough a link, so on Windows the pinned copies at the root can still check out with CRLF. The plan's follow-up lists the lines a regular file needs. - A nested
.gitattributes, such asscripts/.gitattributes, that setseolor-textfor a pinned copy under it: it outranks the root block for that copy, so a Windows checkout can still write the copy with CRLF (#530). - A root adopted before #511 landed, from a source that kept its own root
.gitattributes: it has no block, so on Windows its pinned copies check out with CRLF andmake bootstrapreportsshape-copy-drift.update-shape.py apply --addonly extends a block that is there. - A pinned copy written through a root-kept symlinked directory, such as
.github -> ci: the block names it by the link's spelling,/.github/workflows/validate.yml, which governs nothing, so under a source rule such as* text=auto eol=crlf,validate-pins.pyreportsshape-copy-driftfor that copy. - A source
.gitattributescommitted with CRLF line ends under a rule other thantext=auto: the split renormalises it to LF, andexecutefailsadopt-lost: .gitattributesafter both legs exist. - A source
filter=orworking-tree-encoding=rule that matches a pinned copy: the block settles line endings only, so the rule still applies. With*.py working-tree-encoding=UTF-16LE-BOM,executefails atgit add -Aafter both legs exist.
update-shape.py and bump-leg.py
- #422: A pinned file kept as a hard link:
applyrewrites it through its other name, which may lie outside the root. - #423: A file name that is not UTF-8 under
core.quotePath=false:bump-leg.py, the scaffold andfamily.py initend in aUnicodeDecodeErrortraceback, not a refusal or warning. - #428: An
applywhose rollback cannot put one file back (an unwritable copy, a full disk): anOSErrortraceback, with the remaining files left unrestored. - #431:
apply --branchstarted from a directory git cannot open: a good branch name is refused asupdate-branch-invalid. - #437: A trailing comment on the pin's
files:or the manifest'sshape:line:applyrefusesupdate-pin-unrecognisedorupdate-manifest-no-shape, thoughchecksent you there. - #438: A leg pin written as
digests: {tree_sha256: …}:bump-leg.py --dry-runexits 0, and the real run refusesbump-leg-pin-unrecognised. bump-leg.py --toan annotated tag's own id: the dry run shows the tag id and exits 0, while the real run is refused and rolled back with exit 2 (#534).- A locally edited
scripts/repo_shape.pykept withapply --accept-localwhile taking the newbootstrap.py:make bootstrapends in anImportError, thoughapply's validators pass andcheckthen reads everything unchanged (#536). applywithout--branch, moving the pin back to what HEAD already commits: it still prints a commit line, and pasted, git answersnothing to commit(#535).- A root under a path holding a space, such as
My Projects/: the twoswitch -cremedies and the closing push line thatbump-leg.pyprints are unquoted, and paste as another command (#532). - A root or path holding a space or a shell character:
update-shape.py'supdate-branch-existsremedy, itsADDED UPSTREAM--addlist, itspass --addnote and itsupdate-local-driftremedy print unquoted (#532). - A Windows root whose path holds an apostrophe (
C:\Users\Jane O'Neill\Atlas): the commands every tool but the doctor prints, the shipped validators' included, are quoted for a POSIX shell, which PowerShell does not read as one argument (#532). - Every bump:
bump-leg.pysaysmake bootstrapre-places the leg on its tracking branch, which it does only when that branch is at or behind the pin, or ahead of it with no commit of its own (#533). - A commit made on a detached leg whose branch is already at the pin:
make bootstrapchecks the branch out, leaves the commit on no branch, and says nothing (#533). - A leg on a branch with commits of its own past the pin: bootstrap's
not moving an existing branchline says the checkout stays at the pin, though it stays on the branch (#533). - A checkout git refuses while the leg is already on its branch at the pin, such as behind a stale
index.lock:make bootstrapprintschecked out, and then git said:though nothing was checked out (#533). bump-leg.pyandapplylist the paths they staged, not the ones the commit changed:applynamesproject.yamlbeside the pin even when only the pin moved, as when it drops a merged-away row. Repairing a gitlink moved by hand printsX -> Xwhile the pin moves.- An
update-conflictrefusal: its remedy says to read the upstream bytes withgit show <target>:<source path>, but nothing names the source path (#535). - A copy re-pinned with
--accept-local: the commitapplywrites still says the copies are not hand-edited (#535). apply --branchwithout--pr: thegh pr createit prints carries no--title,--bodyor--fill, soghasks for them at a prompt (#535).- Merged-away rows beside a pinned copy kept as a symlink: the doctor's
pinsrow namesupdate-shape.py checkand says itsapplydrops their rows, though thatapplyis refusedupdate-rewrite-symlink. - A root with
copy-is-link,copy-missing,upstream-removed,unmappedoralready-at-targetrows: the doctor'sshape currencycounts fall short of its rows. - The shape pin's
files:key written quoted ("files":) or with a space before the colon, and likewise the manifest'sshape:key: the validators andcheckaccept it, andapplyrefusesupdate-pin-unrecognised, orupdate-manifest-no-shapesaying the manifest has noshape:block. bump-leg.py --dry-runover a pin that its own validators already refuse, such as one with a stray tab line: it exits 0 and prints theold -> newline, and the real run exits 2bump-leg-validators-redand rolls back.
setup, scaffold, family and the shared helpers
- #362: A holder index holding
Members/…, a case variant ofmembers/, on macOS or Windows:family.py addcannot add any member. - #363: An uncommitted
git rm -r Memberson macOS or Windows:addcommits the gitlink asMembers/<P>, and fresh clones leave the member empty. - #364: An untracked
Members/<P>directory: refusedmember-path-takenon macOS and Windows, but mounted beside it on Linux. - #392: A flow collection with an empty entry or a leading
:,#,-or?([,a],[:]): the YAML reader accepts it, and reads[a: b]as a string. - #393: A plain value that opens with
],},,,@, a backtick or%: the YAML reader takes it as text, where YAML refuses it. - #407: An uncommitted deletion of the holder's
.gitmodules:family.py removecommits it with the member, and fresh clones leave every member empty. - #421:
--tracking-branch +x:--dry-runpasses, and the push fails after every remote exists. - #426: A
git config user.namethat is not UTF-8, with no--created-by:family.py initends in aUnicodeDecodeErrortraceback. - #433: A
--local-remote-dirpath that is not UTF-8: the scaffold andfamily.py initend in a traceback after the bare remotes exist. - #434: A
.github/workflows/directory the user cannot list:validate-pins.pyends in aPermissionErrortraceback. - #441: A bare
--link-source <path>in a chain of three or more links: one tree answers for two links, and a correct chain is reported broken. - #442: An empty path, as in
--link-source openXdox=or--pin-source openXdox=: the current directory is read, and a correct chain is reported broken. - #443: A scalar (
5,openXdox) whereneutral_product_pins:orreferent_chain:should be a list: the validators crash, or read it one letter at a time. - #444: Product names that fold differently (
openß,openss): the chain readers treat them as one product, and the pin validator as two. - #450: A
--pin-sourcepath holding a line break or a byte that is not UTF-8: it is written into the pin file as it stands, or the scaffold crashes after all three repositories exist. - #454: A family
nameholding a lone surrogate, or any run underPYTHONIOENCODING=ascii:make siblings,parkandresumeend in aUnicodeEncodeErrortraceback. - #455: A lone surrogate or NUL in
tracking_branchor a leg'sbranch::make bootstrapends in a traceback, whilevalidate-manifest.pysaysmanifest ok. family.py bump --toa commit whoseproject.yamlidanother row already pins:bumpcommits, and the holder's validator then refusesmember-duplicate-id(#541).- An annotated tag's own id:
family.py add --atpins the commit it tags, whilebump --torefuses it (member-to-not-a-commit). As ruled,bump --tois to peel it too (#541). - No git on
PATH: the shippedbootstrap.pyscripts end in aFileNotFoundErrortraceback. - A directory name that is not UTF-8:
validate-repository-naming.pyends in aUnicodeEncodeErrororUnicodeDecodeErrortraceback. - A holder member directory the user cannot enter:
validate-family.pyrefusesgit-not-runnableand says to put git onPATH(#539). - Re-running before the commit, after the
git rmthatmember-gitlink-unnamedprints: the finding repeats, and the same command then fails. - A shape-pin
files:row with nopath:: the shipped validators report the pathNoneas missing, instead of refusing the row. - A YAML key written twice: the reader keeps the last one without a word.
--org OpenSoft, or any casing ofopensoftbut the lower-case one: the upstream-organisation guard compares exactly and lets it through, though GitHub reads an organisation name in any case (#538).- The plan a person approves: it names neither the tracking branch nor, in local mode, the visibility.
- A
--pinpassed to setup after--: step (4) never asks the naming check about it, so the report says no referent pin is declared, and the plan then says domain-descendant (#538). - Inside setup, the scaffold's own
NEXT STEPSblock prints just before setup runs those steps itself (#538). --local-remote-dir: the clone in the scaffold'sNEXT STEPSfails withtransport 'file' not allowed, because it lacks the-c protocol.file.allow=alwaysthat setup passes (#538).- Step (7)'s by-hand recovery under
--local-remote-dir: its recursive clone lacks-c protocol.file.allow=alwaystoo (#538). - A clone path or
--intoholding a space: setup's closingcdand step (7)'s by-hand recovery print the paths unquoted, and the shell splits them (#538). -- --dry-runpassed through setup: step (7)'s refusal says the three repositories WERE created when none were (#538).- A file standing where
--familyputs the clone's parent folder: step (3) passes, all three repositories are created, and the run ends in aFileExistsErrortraceback (#538). scaffold-project.py --dry-runagainst remotes that already exist: it does not ask, soscaffold-remote-existscomes only after the yes, before anything is created (#537).- A printed value holding a C1 control or a direction-override character: setup escapes only C0 controls and DEL, so it reaches the terminal.
- A re-run where
ghcan see a leg that git cannot read: the scaffold refuses with the generica git or gh command failed, notscaffold-remote-exists, and on a terminal git can stop to ask for a username (#542). - A re-run that reuses legs: its visibility notes say the two legs were just created, and can contradict its own per-leg warning (#542).
- A seed found only on
seed/scaffold, the branch the old ruleset hint made: the re-run accepts it and createsmainat it, and says nothing aboutseed/scaffold, which on GitHub may stay the leg's default branch. - A ruleset that refuses
adopt-project.py's push: its hint still offers the seed-branch-and-pull-request exit that the scaffold no longer offers (#523). - A
--pin-sourcegiven as an absolute path: the committed pin file records the scaffolding machine's own directory (#540). - A lone
shape-copy-drift:validate-pins.pyfollows it with THE LOCKSTEP RULE, about moving a gitlink, a pin and the workflows together (#539). family.py init --dry-runagainst a holder repository that already exists: it exits 0, and the real run refusesfamily-remote-existsafter leaving an empty<Family>/folder (#537).family.py bump --toa commit not yet pushed to the member's remote: it fails in git's words (reference is not a tree), exit 2, and does not say to push first (#541).- A member whose
tracking_branchis not its remote's default branch:make siblingsclones the default branch, not the tracking branch the docs promise (#543). family.py siblings: it runs the standard checkout'ssiblings.py, not the holder's pinned copy, so a holder pinned at an older shape runs other code thanmake siblingsdoes, and thesiblings-script-missingremedy says otherwise (#543).make siblingsin a holder whose folder is not named after the family: it clones the members, then printsmvlines for the holder alone, so a re-run after them clones the members again (#543).- The holder's
validate-family.pyremedies that say to runfamily.py add(member-gitlink-absent,member-gitlink-unnamed): they name no branch step, so theaddis first refusedfamily-on-tracking-branch, whose remedy names thegit switch -c(#539). - The README's and the handbook's
--intosentence: it gives the default, but not thatinitrefuses that default inside a checkout of the standard (family-landing-in-standard). - A
--pinproduct that begins with a digit or-, such as3dGlassor0x1F: the scaffold may write it bare whereadopt-project.pyquotes it, so the two tools spell the same list differently. The project's own reader reads both back as typed (#540).
shape-doctor.py
- #342: A
.pthfile in the operator's Python setup that reaches arepo_shape.py: themanifestrow passes for a root that has lost its own copy, and CI then fails. - #410: A crash of the standard's own naming script, or a directory name that is not UTF-8: the doctor reports
INVALID (naming)about the project. - #413: A leg or member whose detached HEAD holds the only copy of a commit: pasting the
legsormembersNEXT moves it, so only the reflog keeps the commit. - #414: A mount with no
.git, an unborn HEAD, no.gitmodulessection, or a pin that is not 40 hex: thelegsNEXT is the wrong command, or leaves the row red. - #432: A family member URL in
.gitmodulesthat is not UTF-8: the doctor ends in a traceback with no verdict. - #439: A registered-kind file at a path no check reads (such as
contracts/project.yamlin an assembly root): the doctor counts it validated and answersCOMPLIANT. - #447: A validator killed by a signal, or one that exits with nothing printed: the doctor reports a finding about the project, not a crash.
- #448: A
WARNINGfrom the project's ownvalidate-manifest.py, such as an unreachable referent chain: themanifestrow saysok, and the warning is not shown. - An ordinary git repository:
NOT A SHAPE ROOTnames only theadopt-project.py plancommand, though AGENTS.md says to report both ways in (#545). - A machine that is not ready: the
machinerow quotes the preflight's(see above)with nothing above it, because the preflight's lines are kept only in--json(#549). - The README's
validate-pins.pyparagraph: it no longer says that a digest that recomputes differently is the findingneutral-pin-digest-mismatch, exit 1. - A merge left unfinished inside a leg: the
mergerow reads only the root's own index, so it is not reported. - A hand-edited
@<sha>in a workflow, or a leg pin whose digest no longer matches:validate-pins.pyends the finding at THE LOCKSTEP RULE and names no command (#539). - A passing
validate-pins.pythat prints a note, such asneutral-pin-source-collides: thepinsrow saysokand drops the note. - A NEXT argument that begins with
=, such as a branch named=x: the doctor leaves it bare, and zsh expands a bare=word(#532). - A conflicted shape pin whose conflicted copies neither side edited: each copy goes to a person to merge by hand, and keeping both sides of a hunk, then
--accept-local, pins a stale line as the project's own edit (#547). - A leg pin broken in the bump's own commit: the
manifest kindsNEXT restores the pin from before the bump, and the doctor then readsDRIFTED. Nothing says to commit it and run thepinsrow'sbump-leg.pyline (#547). - An unregistered kind no gate here runs, or a
family.yamlbeside an assembly root: themanifest kindsNEXT is prose, withfamily.pycommands in backticks that an interactive zsh would run if the line were pasted (#548). - The test that holds every NEXT to a bare command checks each string constant alone, so it would miss a
#tail split across a+, or one with no blank after the#. - A git repository with no
origin: the doctor's adoption NEXT carries no--org, so pasted, it is refusedadopt-no-org(#545). openRepoShape --doctor --placement-plan a.yaml --placement-plan b.yaml: only the last plan is written, without a word (#546).- A relative
--placement-plan <file>given from inside the repository: it writes that file there, thoughopenRepoShape's usage and refusal lines, the README and the handbook say the doctor writes nothing into the repository (#546). - The kept clone's last line: it says any NEXT above runs from the clone, but the
pinsrow's NEXT names no file in it, and theplacementrow's relative--placement-plan, run from the clone, writes the plan into the directory the line calls safe to delete (#546). - A TERM sent to the installed doctor's shell alone, mid-run: it deletes the kept clone under the still-running doctor.
- An interrupted
openRepoShape --doctor(INT, TERM, HUP): it deletes its clone, but no test holds that. - The installed doctor's NEXT lines: only a drifted project's are tested. A plain repository's were run by hand.
- A checkout of the standard whose own
contracts/repository-naming.yamldoes not parse: a compliant project readsINVALID (naming), exit 1, and thenamingrow saysthe naming policy is not satisfied (exit 2)over aREFUSED yaml-unparsablethat is the standard's file, not the project's.
The test suite, CI and the docs
- #415: A Windows root whose path holds a space: no test runs the PowerShell form of the leg NEXT line, and its quoting test passes only because temporary paths need no quoting.
- #429: Looking up
apply --add's refusals in the README or the handbook:update-add-symlinkandupdate-add-not-a-directoryare not listed. - #430: A directory the user cannot search, on Python 3.14: the shipped tools treat it as absent, and skip it or misreport it instead of refusing.
- A test fixture that clones a just-pushed local remote by plain path (about a dozen of them,
siblings.py, fourgit submodule update --initruns in two test files, and the recursive clone in the adopt quoting test): it can race git's background maintenance. None has failed in CI (#544). - An unreadable root pin in the doctor's tests: one test still accepts exit 3, where only exit 1 (
INVALID) is correct (#544). - Three test modules run the family template's scripts, two by loading them and
tests/test_cli_reference.pythrough--helpchildren that do not setPYTHONDONTWRITEBYTECODE: they leave.pycfiles in the template tree, so a test that reads every template file could pass or fail by order (#544). - README's by-hand version of what setup runs: its dry run omits the real run's
--visibilityand--elected-by, so the plan it shows can name a different elector than the real run writes (#550). - The adopt suites run without
--work-dirand set noTMPDIRof their own, so every test run leavesopenreposhape-adopt-*directories in the system temp folder (#526). - No test holds that a bare or absent
source:orlegs:in a plan is no type finding: two mutants survive that file's tests (#544).
Full list
- Merged pull requests: https://github.com/opensoft/openRepoShape/pulls?q=is%3Apr+is%3Amerged
- The
v1.0.0milestone: https://github.com/opensoft/openRepoShape/milestone/1