Skip to content

openRepoShape v1.0.0

Choose a tag to compare

@brettheap brettheap released this 14 Oct 23:17
· 387 commits to main since this release
bfed143

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.py on Windows. It runs a preflight, checks the names,
    shows the plan and asks for one typed yes.
  • Direct scaffold: scaffold-project.py. Add --pin open<Product>@<commit>
    for a declared descendant.
  • Adopt in place: adopt-project.py plan, then check, then execute.
  • Compliance check: openRepoShape --doctor [<path>] or
    shape-doctor.py --root <path>.
  • Stay current: update-shape.py check, then apply.
  • 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 park and make resume.
  • Each project's own gate: make bootstrap and make validate run pinned
    copies of the scripts, so the gate never needs this standard.

Requirements and platforms

  • git and 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-repo is needed for adoption only. make is optional.
  • openRepoShape --preflight checks the machine and creates nothing. On
    Windows, run py setup-project.py --preflight. It names what is missing,
    and offers to install gh, log it in and set up its credential helper,
    each behind a typed yes. setup-project.py --preflight also offers git.
  • Linux and macOS are supported, and the stock macOS /bin/bash 3.2 is
    enough. Native Windows uses setup-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-latest and
    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 bfed143e89f97d3480cf5373051cb1ace82db312 to setup.sh. Outside a
    checkout, setup.sh and setup-project.py take --shape-ref directly.

  • Existing project or family holder: run these from a checkout of the
    standard. check lists the files that change. apply says how many it
    will re-copy and asks Type 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 with git filter-repo, and the
    split lands as one pull request. Every source path is verified by blob
    sha, a kept root .gitattributes as 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 the xf-project-<id> topic is set
    (#5, #16, #17, #24).
  • Branches and pins. The tracking branch defaults to the source's default
    branch, and check and execute refuse one that is no branch of the
    source (plan-tracking-branch-missing). The topic is set before the pull
    request, and NEXT names scripts/bootstrap.py when the source kept its own
    Makefile (#493). plan --pin takes <product>@<commit>, and execute
    writes the pin file the scaffold writes, so the adopted root's
    validate-pins.py passes; a --pin-source that cannot answer is
    pin-source-unreadable (#508). A pinned product such as 007 is listed
    in project.yaml as text, not as the number 7 (#515).
  • The source's own name. plan refuses an --org and --project that
    name another repository than the source's, compared without case
    (adopt-not-the-source). check and execute refuse 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. plan writes a value bare only where the project's
    own YAML loader reads it back as that text, so --spec-path null,
    --id null and --tracking-branch true, on a source with a branch true,
    adopt as typed. Before, the first mounted the leg at spec, the second made
    the id northwind, 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-path finding, not the default spec. 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
    execute writes. The shipped templates quote those lines, so it names none
    today (#518).
  • Refused before any leg exists. execute refuses everything check
    finds before it creates a leg. That covers bad mount paths, root paths that
    clash with the shape's files, the source's own project.yaml kept in the
    root, a symlink with an empty target, values no file can hold
    (adopt-unwritable-value), and paths it cannot extract, which plan now
    asks about. A split branch an earlier run left, or a branch in its way, is
    adopt-branch-exists or plan-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. check follows 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-symlink finding, and execute refuses it before any leg
    exists. Where git checks a symlink out as a file, execute also 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=lf lines appended to it in
    the split, a line for each pinned copy outside shape/, 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, and update-shape.py apply --add extends the block (#511).
  • Submodules. .gitmodules stays in one leg with its submodules. An
    unreadable one is a plan-gitmodules-unreadable finding, 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-commit and others). execute
    records in project.yaml the 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 .gitignore matches them, and leg mounts use --force. Findings
    spell out control characters and bytes that are not UTF-8, in a
    hand-edited question: 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 with IS pushed is finished by hand with the command it prints,
    never by re-running (#371).

update-shape.py and bump-leg.py

  • Re-syncing copies. check gives each pinned file a verdict
    (unchanged, upstream-changed, locally-modified, both and others)
    and names files the standard has added (upstream-added). It exits 1 when
    there is work to do. apply re-copies and re-pins, and rolls back if the
    project's validators fail. --add and --accept-local each name one file
    and can be repeated. Family holders, and their README's Shape: 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. A shape/
    collision copy merged into its original and removed is merged-away, and
    apply drops its pin row (#512).
  • Landing. apply --branch takes --trailer lines, and a non-ASCII
    trailer is committed as typed under any locale. Before it writes,
    apply --branch refuses a pin, manifest or copy that differs from HEAD
    (update-branch-uncommitted). The gh pr create it 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
    --pr run 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; check calls
    such a copy copy-is-link). A shape pin whose files: is not a list is
    refused update-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-run shows old -> new and the trailers first. The
    dry run also runs on the tracking branch, and names the git switch -c the
    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 bootstrap moves 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.sh self-bootstraps from upstream with an explicit
    --org, so no fork is needed. It is a shim over setup-project.py, the
    native Windows way in. openRepoShape installs as a command. The preflight
    offers installs behind a typed yes. --local-remote-dir rehearses a run
    with nothing created on GitHub, and --family lands the clone in a family
    folder. setup.sh --help outside 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 its open<Product>, directly or through a recorded
    chain. The default reference: follows the election date. The Triad
    advisory speaks once and never blocks, and single-repository.yaml silences
    it. A --link-source value is PRODUCT=PATH only 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-branch git 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 007 or --tracking-branch 2026 reads back as typed, and a leg
    path any template line would misread is refused. A --pin product such
    as 007 is listed as text, so the root's own validate-pins.py passes
    (#185, #201, #234, #235, #256, #266, #271, #314, #316, #351, #355, #395, #409, #517).

  • Ruleset refusals. --reuse-empty-repo reuses 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, not main (#373, #506).

  • Each project's own gate. make bootstrap places each leg at its pin,
    and .gitattributes keeps LF line endings. AGENTS-shape.md carries the
    rules for assistants, and a <user>-wip template ships too. The validate
    workflow reads private legs with a GitHub App or SHAPE_LEGS_TOKEN, and
    carries on with a warning without one. validate-pins.py rechecks
    neutral-product pins from a local checkout where it can, else through
    gh api with 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 one workflow-unreadable refusal naming every such file.
    make bootstrap escapes what the console cannot print, so a name such as
    日本 under PYTHONIOENCODING=ascii no 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 siblings clones working copies beside it without moving anything.
    init asks the holder's own reader and validator about family.yaml
    before it creates anything. add, bump and remove have --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), and bump --to an annotated tag, a tree or a blob
    is refused member-to-not-a-commit. A repeated bump says nothing to do
    and exits 1. make siblings, park and resume refuse by name when
    scripts/siblings.py is missing. init refuses its default landing
    inside a checkout of the standard (family-landing-in-standard), and
    add, bump and remove refuse 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 park and make resume hand 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 and gh is 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 its REFUSED line 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.py still prints it. family.py add refuses a
    .gitmodules git cannot read as gitmodules-unreadable, as the holder's
    gate does (#494).

shape-doctor.py

  • One verdict. openRepoShape --doctor fetches nothing into the
    repository, and writes nothing there but a --placement-plan file 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. --json and --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, MISPLACED or INVALID: exit 1;
    • NOT A SHAPE ROOT: exit 2;
    • CANNOT ANSWER: exit 3.

    Only a FINDING moves the verdict. The machine check is now --preflight
    (#96).

  • Placement. The placement row checks each leg's paths against the
    adoption policy. --placement-plan writes 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 a WHY line of its own, and why in --json. They
    are quoted for sh or PowerShell, use Copy-Item -LiteralPath on Windows,
    and set the PYTHONPATH a validator needs. A gitlink committed by hand is
    offered both repairs, bump-leg.py --to the 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 was NOT A SHAPE ROOT or CANNOT ANSWER, and a root
    in the middle of a merge is INVALID (merge), exit 1, where it could read
    COMPLIANT. A misplaced family.yaml is a manifest kinds finding, 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 installs git-filter-repo,
    so the adoption suites run there too. Every push to main runs 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, and setup-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.html is a one-page reading of it. docs/cli.md is 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_ROOT rather
    than a fixed path. adopt-project.py plan --help says what --spec-path
    and --code-path are, their defaults, and the two refusals plan makes 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: check passes, and execute fails after both legs exist, with nothing on stderr.
  • #389: A plan that keeps the source's own project.yaml in the root: check and execute now 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: check and execute resolve 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 the NEXT: line prints fails.
  • #420: A hand-edited adopt_branch: that git refuses as a branch name, such as a..b: check says plan ok, and execute fails after both legs exist. A bad tracking_branch: is caught by check as plan-tracking-branch-missing.
  • #425: A source whose default branch name is not UTF-8: plan, check and execute end in a UnicodeDecodeError traceback.
  • #427: An origin whose organisation is not ASCII or holds a space: plan refuses unsafe-value: --org, a flag nobody passed.
  • #449: A hand edit that makes a neutral pin's commit: and tree_sha256: disagree: adoption verified, then the root's validate-pins.py fails neutral-pin-digest-mismatch.
  • #451: A --pin-source that 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: execute ends in a UnicodeEncodeError traceback 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: plan asks 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: check passes, and execute fails 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 as a=b: check says plan ok, and execute refuses unsafe-value before any leg exists (#520).
  • A hand-edited plan with a bad id: or org: beside a bad leg path: check and execute name different problems first. No leg is made either way.
  • A project adopted with a --spec-path other than spec: the root Makefile still exports CONTRACTS_DIR ?= $(CURDIR)/spec/contracts, so the code leg's tooling points at a directory that does not exist (#522).
  • A nested --code-path, such as legs/code beside legs/spec: the cross-leg follow-up names the spec leg ../legs/spec/… relative to the code leg, where ../spec/… is right (#522).
  • plan, check or execute run without --work-dir: each leaves an openreposhape-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 before check or execute: they refuse source-unclonable, with text about the repository's name. Passing --source again recovers (#526).
  • Any adoption: the follow-up plan prints, and the split commit lists, says to open the split as a pull request, which execute does itself against GitHub, and calls the extracted legs seeded (#523).
  • execute --local-remote-dir: it ends with NEXT: review the pull request, though none was opened (#523).
  • An answer in the plan with no resolution: beside it: check says plan 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).
  • plan with no --elected-by and no git config user.name: it is refused adopt-manifest-invalid, naming --elected-by, a flag nobody passed (#529).
  • A path the plan answered by hand, such as spec/examples/: the doctor's placement row keeps listing it as review_required, because execute records only the submodule answers (#525).
  • A local source or assembly that hides a branch with uploadpack.hideRefs or transfer.hideRefs: check does not see it, and execute --local-remote-dir pushes both legs before the split push fails (#520).
  • A submodule moved to the other leg on purpose after adoption: the doctor reads MISPLACED from then on, and nothing says how its submodules: 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, on or .5: execute lists 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>: plan refuses adopt-not-the-source even with the right --org, and says the name was read from the path (#521).
  • An origin that is a Windows path with backslashes, or a URL with a trailing slash: it names no repository, so --org and --project are not compared with it, and a mistyped --project plans with exit 0 (#521).
  • plan --source <org>/<repo> with another --project: adopt-not-the-source comes 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: check reports naming-unclassified without the rename-first remedy that execute gives.
  • An origin removed after plan, or execute --source a copy with no origin: the plan's recorded source: repository is not consulted, so a plan hand-edited to another name passes, and execute acts on that name.
  • A path origin such as …/straße/Atlas.git with --org strasse: the source check takes them for one repository, though GitHub folds ASCII case only.
  • A hand-edited org: that is null, deleted, false or 0, for a source whose repository is literally None/<name> (or False/, 0/): check says plan ok where a text org: finds plan-split-branch-taken. execute still refuses.
  • A file the root keeps at contracts/shape-pin.yaml, or a link there to a declared pin's file: check passes, execute fails adopt-shape-pin-unreadable after both legs are pushed, and the first bootstrap fails (#524).
  • A declared pin's file reached through a root-kept link, such as contracts -> governance with a governance/openglass-pin.yaml: check and execute pass, and the first bootstrap fails yaml-unparsable or neutral-pin-unreadable.
  • A link at a path the shape writes to a name the clone's .git does not hold under objects/, refs/, logs/ or modules/, such as CLAUDE.md -> .git/refs/remotes/origin/main: check passes, and execute writes the shape's file into the clone's .git and says adoption verified, or, under objects/, fails after both legs exist.
  • A link at a path the shape writes whose target climbs back out of .git or a directory in it, such as CLAUDE.md -> .git/refs/../HEAD: it is refused plan-root-symlink before any leg, saying .git/refs is 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's plan-root-symlink says where the link leads, though the clone holds it as a file, and its "change where it points" leads on to execute's adopt-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 refused plan-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, and check and execute pass.
  • A hand-edited pin whose product holds a tab: check reports two findings about the one value, plan-pin-malformed and plan-unwritable-value. No leg is made.
  • A root-kept link at a shape copy's path, such as shape/README.md -> ../README.md: check's plan-root-blocks-shape-copy says execute would write through it after both legs are pushed, but execute refuses it before any leg exists.
  • A --work-dir the user cannot write: check passes, and execute refuses extracting the spec leg after 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 .gitattributes is a symlink: the split appends no line-ending block, and git reads no .gitattributes through 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 as scripts/.gitattributes, that sets eol or -text for 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 and make bootstrap reports shape-copy-drift. update-shape.py apply --add only 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.py reports shape-copy-drift for that copy.
  • A source .gitattributes committed with CRLF line ends under a rule other than text=auto: the split renormalises it to LF, and execute fails adopt-lost: .gitattributes after both legs exist.
  • A source filter= or working-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, execute fails at git add -A after both legs exist.

update-shape.py and bump-leg.py

  • #422: A pinned file kept as a hard link: apply rewrites 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 and family.py init end in a UnicodeDecodeError traceback, not a refusal or warning.
  • #428: An apply whose rollback cannot put one file back (an unwritable copy, a full disk): an OSError traceback, with the remaining files left unrestored.
  • #431: apply --branch started from a directory git cannot open: a good branch name is refused as update-branch-invalid.
  • #437: A trailing comment on the pin's files: or the manifest's shape: line: apply refuses update-pin-unrecognised or update-manifest-no-shape, though check sent you there.
  • #438: A leg pin written as digests: {tree_sha256: …}: bump-leg.py --dry-run exits 0, and the real run refuses bump-leg-pin-unrecognised.
  • bump-leg.py --to an 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.py kept with apply --accept-local while taking the new bootstrap.py: make bootstrap ends in an ImportError, though apply's validators pass and check then reads everything unchanged (#536).
  • apply without --branch, moving the pin back to what HEAD already commits: it still prints a commit line, and pasted, git answers nothing to commit (#535).
  • A root under a path holding a space, such as My Projects/: the two switch -c remedies and the closing push line that bump-leg.py prints are unquoted, and paste as another command (#532).
  • A root or path holding a space or a shell character: update-shape.py's update-branch-exists remedy, its ADDED UPSTREAM --add list, its pass --add note and its update-local-drift remedy 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.py says make bootstrap re-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 bootstrap checks 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 branch line 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 bootstrap prints checked out, and then git said: though nothing was checked out (#533).
  • bump-leg.py and apply list the paths they staged, not the ones the commit changed: apply names project.yaml beside the pin even when only the pin moved, as when it drops a merged-away row. Repairing a gitlink moved by hand prints X -> X while the pin moves.
  • An update-conflict refusal: its remedy says to read the upstream bytes with git show <target>:<source path>, but nothing names the source path (#535).
  • A copy re-pinned with --accept-local: the commit apply writes still says the copies are not hand-edited (#535).
  • apply --branch without --pr: the gh pr create it prints carries no --title, --body or --fill, so gh asks for them at a prompt (#535).
  • Merged-away rows beside a pinned copy kept as a symlink: the doctor's pins row names update-shape.py check and says its apply drops their rows, though that apply is refused update-rewrite-symlink.
  • A root with copy-is-link, copy-missing, upstream-removed, unmapped or already-at-target rows: the doctor's shape currency counts 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's shape: key: the validators and check accept it, and apply refuses update-pin-unrecognised, or update-manifest-no-shape saying the manifest has no shape: block.
  • bump-leg.py --dry-run over a pin that its own validators already refuse, such as one with a stray tab line: it exits 0 and prints the old -> new line, and the real run exits 2 bump-leg-validators-red and rolls back.

setup, scaffold, family and the shared helpers

  • #362: A holder index holding Members/…, a case variant of members/, on macOS or Windows: family.py add cannot add any member.
  • #363: An uncommitted git rm -r Members on macOS or Windows: add commits the gitlink as Members/<P>, and fresh clones leave the member empty.
  • #364: An untracked Members/<P> directory: refused member-path-taken on 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 remove commits it with the member, and fresh clones leave every member empty.
  • #421: --tracking-branch +x: --dry-run passes, and the push fails after every remote exists.
  • #426: A git config user.name that is not UTF-8, with no --created-by: family.py init ends in a UnicodeDecodeError traceback.
  • #433: A --local-remote-dir path that is not UTF-8: the scaffold and family.py init end in a traceback after the bare remotes exist.
  • #434: A .github/workflows/ directory the user cannot list: validate-pins.py ends in a PermissionError traceback.
  • #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) where neutral_product_pins: or referent_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-source path 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 name holding a lone surrogate, or any run under PYTHONIOENCODING=ascii: make siblings, park and resume end in a UnicodeEncodeError traceback.
  • #455: A lone surrogate or NUL in tracking_branch or a leg's branch:: make bootstrap ends in a traceback, while validate-manifest.py says manifest ok.
  • family.py bump --to a commit whose project.yaml id another row already pins: bump commits, and the holder's validator then refuses member-duplicate-id (#541).
  • An annotated tag's own id: family.py add --at pins the commit it tags, while bump --to refuses it (member-to-not-a-commit). As ruled, bump --to is to peel it too (#541).
  • No git on PATH: the shipped bootstrap.py scripts end in a FileNotFoundError traceback.
  • A directory name that is not UTF-8: validate-repository-naming.py ends in a UnicodeEncodeError or UnicodeDecodeError traceback.
  • A holder member directory the user cannot enter: validate-family.py refuses git-not-runnable and says to put git on PATH (#539).
  • Re-running before the commit, after the git rm that member-gitlink-unnamed prints: the finding repeats, and the same command then fails.
  • A shape-pin files: row with no path:: the shipped validators report the path None as 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 of opensoft but 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 --pin passed 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 STEPS block prints just before setup runs those steps itself (#538).
  • --local-remote-dir: the clone in the scaffold's NEXT STEPS fails with transport 'file' not allowed, because it lacks the -c protocol.file.allow=always that setup passes (#538).
  • Step (7)'s by-hand recovery under --local-remote-dir: its recursive clone lacks -c protocol.file.allow=always too (#538).
  • A clone path or --into holding a space: setup's closing cd and step (7)'s by-hand recovery print the paths unquoted, and the shell splits them (#538).
  • -- --dry-run passed through setup: step (7)'s refusal says the three repositories WERE created when none were (#538).
  • A file standing where --family puts the clone's parent folder: step (3) passes, all three repositories are created, and the run ends in a FileExistsError traceback (#538).
  • scaffold-project.py --dry-run against remotes that already exist: it does not ask, so scaffold-remote-exists comes 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 gh can see a leg that git cannot read: the scaffold refuses with the generic a git or gh command failed, not scaffold-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 creates main at it, and says nothing about seed/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-source given as an absolute path: the committed pin file records the scaffolding machine's own directory (#540).
  • A lone shape-copy-drift: validate-pins.py follows it with THE LOCKSTEP RULE, about moving a gitlink, a pin and the workflows together (#539).
  • family.py init --dry-run against a holder repository that already exists: it exits 0, and the real run refuses family-remote-exists after leaving an empty <Family>/ folder (#537).
  • family.py bump --to a 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_branch is not its remote's default branch: make siblings clones the default branch, not the tracking branch the docs promise (#543).
  • family.py siblings: it runs the standard checkout's siblings.py, not the holder's pinned copy, so a holder pinned at an older shape runs other code than make siblings does, and the siblings-script-missing remedy says otherwise (#543).
  • make siblings in a holder whose folder is not named after the family: it clones the members, then prints mv lines for the holder alone, so a re-run after them clones the members again (#543).
  • The holder's validate-family.py remedies that say to run family.py add (member-gitlink-absent, member-gitlink-unnamed): they name no branch step, so the add is first refused family-on-tracking-branch, whose remedy names the git switch -c (#539).
  • The README's and the handbook's --into sentence: it gives the default, but not that init refuses that default inside a checkout of the standard (family-landing-in-standard).
  • A --pin product that begins with a digit or -, such as 3dGlass or 0x1F: the scaffold may write it bare where adopt-project.py quotes 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 .pth file in the operator's Python setup that reaches a repo_shape.py: the manifest row 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 legs or members NEXT moves it, so only the reflog keeps the commit.
  • #414: A mount with no .git, an unborn HEAD, no .gitmodules section, or a pin that is not 40 hex: the legs NEXT is the wrong command, or leaves the row red.
  • #432: A family member URL in .gitmodules that 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.yaml in an assembly root): the doctor counts it validated and answers COMPLIANT.
  • #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 WARNING from the project's own validate-manifest.py, such as an unreachable referent chain: the manifest row says ok, and the warning is not shown.
  • An ordinary git repository: NOT A SHAPE ROOT names only the adopt-project.py plan command, though AGENTS.md says to report both ways in (#545).
  • A machine that is not ready: the machine row 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.py paragraph: it no longer says that a digest that recomputes differently is the finding neutral-pin-digest-mismatch, exit 1.
  • A merge left unfinished inside a leg: the merge row 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.py ends the finding at THE LOCKSTEP RULE and names no command (#539).
  • A passing validate-pins.py that prints a note, such as neutral-pin-source-collides: the pins row says ok and 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 kinds NEXT restores the pin from before the bump, and the doctor then reads DRIFTED. Nothing says to commit it and run the pins row's bump-leg.py line (#547).
  • An unregistered kind no gate here runs, or a family.yaml beside an assembly root: the manifest kinds NEXT is prose, with family.py commands 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 refused adopt-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, though openRepoShape'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 pins row's NEXT names no file in it, and the placement row'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.yaml does not parse: a compliant project reads INVALID (naming), exit 1, and the naming row says the naming policy is not satisfied (exit 2) over a REFUSED yaml-unparsable that 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-symlink and update-add-not-a-directory are 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, four git submodule update --init runs 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.py through --help children that do not set PYTHONDONTWRITEBYTECODE: they leave .pyc files 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 --visibility and --elected-by, so the plan it shows can name a different elector than the real run writes (#550).
  • The adopt suites run without --work-dir and set no TMPDIR of their own, so every test run leaves openreposhape-adopt-* directories in the system temp folder (#526).
  • No test holds that a bare or absent source: or legs: in a plan is no type finding: two mutants survive that file's tests (#544).

Full list