Repository navigation
shape-doctor.py: a NEXT is only the command, and the sentence that ended twenty-five of them after # is the row's why, on a WHY line of its own and "why" in --json, so a NEXT pasted into an interactive zsh -- which reads that # as a word -- no longer hands the comment to the command as arguments, runs what follows its ;, or runs the make bootstrap in its backticks, and the template validator's line is env PYTHONPATH=... python3 ... (#277) - #402
Conversation
…"reports a finding", and is CANNOT ANSWER when it is the standard's template, which a standard whose path holds the `PYTHONPATH` separator no longer runs at all; a neutral product's digest `validate-pins.py` skipped is a `note` naming the product, from a child that can no longer reach `gh` or a proxy; and a root whose manifest does not load and whose `scripts/repo_shape.py` is gone is told to restore that file before its gate (#278, #297, #298) Three rows said more than the validator they ran had answered. #278. `validator_row` read a validator by its exit code alone, and an exception that reaches the top of a Python program exits 1, the code for findings. A root's own `validate-pins.py` reduced to `raise RuntimeError("boom")` read "reports a finding (exit 1)", and a standard checked out under a directory whose name holds `:` gave the template copy a `PYTHONPATH` that Python split in two, so the template died on `import repo_shape` and a valid manifest read INVALID (manifest). `crashed()`, beside `run_validator`, now tells a crash apart: a non-zero exit, no line starting FINDING or REFUSED, and a traceback header (R278-1). The root's own copy crashing stays a FINDING, since a gate that cannot run is a fact about the project; it says "crashed before reaching a verdict", keeps the quoted exception the #238 and #306 tests read, carries `detail.crashed`, and names a missing `scripts/repo_shape.py` when that is the cause. The template copy crashing is this checkout's failure: it also carries `detail.environment`, so the verdict is CANNOT ANSWER, exit 3. The NEXT stays the validator's own line, which shows the traceback. `pythonpath_unnamable()`, beside `validator_scripts`, says when the template's `scripts/` holds `os.pathsep` (R278-2, amended: no invented destination). `validator_row` must run the template to answer, so it runs nothing and is an environment row with no NEXT; `unloaded_manifest_row` already knows its answer, so its FINDING stands with no NEXT. Both say to run the doctor from a checkout of the standard whose path holds no separator, and no `PYTHONPATH=` value holding one is printed anywhere. It is `os.pathsep`, so Windows' `;` is claimed by the same helper, and a `;` directory on POSIX is an ordinary character and is not refused (R278-3). #297. A project that pins a neutral product, with no local checkout of it, has a `validate-pins.py` that asks `gh api`, prints a `skip` line when that fails, and exits 0. The doctor threw the output away and said "passes", under COMPLIANT, and the child had started a call to GitHub on the operator's login wherever there was one. Every validator child is now started `offline()` (R297-1 with its fallback, both walls): the four token variables `GH_TOKEN`, `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN` and `GITHUB_ENTERPRISE_TOKEN` removed, `GH_CONFIG_DIR` the null device, `GH_NO_UPDATE_NOTIFIER=1`, and `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY` and `http_proxy` pointed at http://127.0.0.1:9 with `NO_PROXY` and `no_proxy` removed. On Windows, where a variable's case does not matter, only the upper-case proxy names are set. `check_machine` alone opts out, with `run_validator(..., reach_network=True)`: it is the documented exception. Exit 0 with ` skip ` lines is now a `note` (R297-2), "passes, but did not recheck: <skip text>", with `detail.skipped` and no NEXT, since the skip line names the three ways to give a source; a note moves no verdict. The validator's own exit-0-with-a-skip contract is unchanged (R297-3). #298. `unloaded_manifest_row` runs no gate, and promised that the gate "refuses it the same way until it loads" for a root that had also lost its `scripts/repo_shape.py`, where that gate dies on the import. The fact now lives in `imports_repo_shape()` beside `validator_scripts`, decided once for both rows and not as a fourth value of `validator_for`, whose arity three callers depend on (R298-1). When the root's own copy cannot import it, the NEXT is `git -C <root> checkout HEAD -- scripts/repo_shape.py`, the bytes the project committed, and its why names the gate to run after it (R298-2, R298-3: restore first, as DRIFTED (copy-missing) already outranks INVALID; the verdict is unchanged). A deletion already committed is not in HEAD, so there the row has no NEXT and its reason says the file comes back from the parent of the commit that deleted it. The "refuses it the same way" promise is printed only where the gate can keep it. Tests, in the new tests/test_doctor_validator_rows.py, on what each row and command does and never on a NEXT's tail: a crashing own copy for each of the three gates and a crashing template (FINDING with `detail.crashed`; the template's CANNOT ANSWER, exit 3), and both NEXTs pasted show the traceback; a standard under `dir:with-colon` gives the environment row with nothing run and no NEXT, and with an unloadable manifest a FINDING with no NEXT, with no `PYTHONPATH=` in either report (WINDOWS_SKIP); a standard under `semi;colon` is refused on Windows and is the control on POSIX, where its pasted NEXT prints the template's finding; a MedxGlass descendant run with this file's own logging `gh` stub first on PATH is COMPLIANT with a `pins` note naming openGlass, the stub saw the validator child with no token, `GH_CONFIG_DIR` the null device and the proxy set, and saw the machine row's probes with the operator's values; a `run_validator` probe both ways on every platform; and for a project and a holder whose manifest has a tab and whose `scripts/repo_shape.py` is gone, the restore NEXT run and pasted exits 0, after which the gate the doctor names refuses `yaml-unparsable` with exit 2, beside the control (the file kept) and a committed deletion (no NEXT). Mutants, each run against the new file: a crash read as a finding again, the token strip dropped, the proxy dropped, the skip read as ok, the restore NEXT replaced by the old promise, and the separator check dropped each turn the rows that hold them red. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or a row that has a NEXT, a skip keeps every line of `gh`'s stderr and the `--pin-source` advice after it, and a validator that printed a FINDING before it raised, and a crash for want of `scripts/repo_shape.py`, are held by tests: the fix round on the review of 2bf402d (#278, #297, #298) The review of 2bf402d was MERGEABLE WITH NITS, with three SHOULDs and the nits below; this is the one commit it asked for, on top of the head. S1 (#278). The separator environment row is a FINDING with no NEXT, by ruling, and `report` printed `Remediation: {next_command}` for it unconditionally, so the stderr block ended in a literal `Remediation: None`. The line is now printed only when the row has a NEXT; the reason already says what to do instead. The colon row asserts no `Remediation` at all, and the template-crash row, which has a NEXT, asserts the line is still there. S2 (#297). `skipped()` kept only the ` skip ` line, and `validate-pins.py`'s skip embeds `gh`'s stderr verbatim, which is two lines when `gh` is not logged in: the reason stopped at `gh auth login`, told the reader to do what the doctor's offline environment undoes, and lost the `--pin-source`, `SHAPE_PIN_SOURCE_<PRODUCT>` and `../<product>` ways to give a source that are why the row has no NEXT. It now keeps the whole block, the ` skip ` line and every line after it up to the next item (`REPORT_ITEM`: ` ok`, ` skip`, ` --`, `FINDING`, `REFUSED`, `WARNING` or the validator's summary line) or the end, joined by a space. Tested with a logging `gh` stub that prints the two-line help, and on a constructed output on every platform. S3 (#278). The clause of `crashed()` that says a validator which printed a `FINDING` or `REFUSED` line reached a verdict had no test: deleting it left every row green. A validator that prints a finding and then raises is now asserted to be a FINDING that is neither `crashed` nor an environment row, for the root's own copy and the template; with the clause gone the own copy would say it crashed and the template would be CANNOT ANSWER, hiding the finding it printed. N3 (#298). The crash reason's "`scripts/repo_shape.py`, which it imports from beside itself, is not there" had no test either. Tested for a project and a holder whose gate dies on the import. N5. The module docstring's "offline by their own contract", which #297 quoted as false, says the doctor runs the validators offline, which `run_validator` now makes true. Mutants, each against the new file, each turning the rows that hold it red: `crashed()`'s no-FINDING/REFUSED clause deleted (the 2 finding-then-crash rows), the "is not there" fact not appended (the 2 missing-`repo_shape.py` rows), `Remediation:` printed unconditionally again (the colon row), never printed (the template-crash row), only the first line of a skip kept and the summary folded into it (the two skip rows, each). 26 tests in the file; the unmutated file passes them all. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ded sixteen of them after ` # ` is the row's `why`, on a `WHY` line of its own and `"why"` in `--json`, so a NEXT pasted into an interactive zsh -- which reads that `#` as a word -- no longer hands the comment to the command as arguments, runs what follows its `;`, or runs the `make bootstrap` in its backticks, and the template validator's line is `env PYTHONPATH=... python3 ...` (#277) The doctor built sixteen NEXTs as `<command> # <comment>` and printed one prose sentence in the command's place. `sh -c`, bash and `zsh -c` read that `#` as a comment. An INTERACTIVE zsh without `interactivecomments` reads it as a word; that is zsh's default and macOS's login shell, the one a person pastes into. Pasted there, the comment's words reached the command as arguments, and whatever followed a `;` ran as a second command: the manifest NEXT exited 127, `zsh: command not found: it`, instead of with the validator's code. The `make bootstrap` that the `legs` and `members` comments put in backticks ran as command substitution, in whatever directory the person stood in. The tests could not see any of it, because they read a NEXT the way `sh` does. A NEXT is now the command and nothing else (R277-2: `next` is the bare command or null). The sentence moved verbatim, less its `#`, to a new keyword-only `Row` argument, `why` (R277-1). It is printed in three places: `"why"` on every `--json` row (null where there is none), a `WHY` line of its own under `NEXT` in the table, and ` Why: ...` on the line after ` Remediation: ...` in the CANNOT ANSWER block on stderr. The module docstring and `Row`'s say why the sentence never goes back on the command's line. The sites, re-counted on 3ae0109: 16 non-docstring string constants held ` # ` outside `placement_plan_leg` and `report`, plus one prose NEXT, the plan's 17: - `validator_row` (neither copy readable; the finding); - `unloaded_manifest_row`, whose neither-readable branch now passes on `validator_row`'s `why` together with its NEXT; - `check_shape_currency` (cannot answer; the pin alone behind; copies moved). The `then apply --at ... --branch ...` with its `--accept-local` and `--trailer 'Lane: X'` is the `why`, which `lane_trailer_command`'s docstring now says; - `check_legs`; - `missing_shape_file_fix`, which returns `(next, why)`. Its partly-copyable note is the `why`, and the all-rendered prose NEXT is now `python3 <shape>/scaffold-project.py --help`, with that prose as its `why`; - `placement_policy`, through `placement_row(why=...)`; - `check_placement`'s `--placement-plan` line, through `placement_verdict`'s new `plan_why`; - the three branches of `check_agent_files`; - `members_verdict`; - `check_machine`; - `check_the_way_in`. Out of scope as ruled: `check_manifest_kinds` (lane 2's #291) and `Refusal.remediation` used as a NEXT. `report`'s `placement plan written to ... # nothing moved` is printed text, not a NEXT. R277-4: `validator_command`'s `sh` branch spells the template's environment `env PYTHONPATH=<q> python3 ...`. A bare `NAME=value` prefix is syntax in sh, bash and zsh only. By their documentation (not run here), csh and tcsh report `Command not found` for it and fish before 3.1 refuses it. `env` is a program, so the line runs in each of them and still sets the variable for that one command. The PowerShell `$env:` line is unchanged. Tests, in a new `tests/test_doctor_next_is_a_command.py`, importing `test_shape_doctor`'s fixtures: - A property over one red fixture per converted site (19 cases: the 17, plus the template-copy validator line and the unloaded-manifest neither-readable branch). The NEXT reads the same to `shlex` with and without comments, holds no backtick and no ` # `, and on POSIX no unquoted `;` (lexed with `#` not a comment, as zsh reads it). Its `why` opens with main's own words. - A `zsh -f -i` paste through a pty, skipped without zsh and on Windows. The manifest NEXT exits with the row's `detail.exit` and prints `FINDING manifest-topic`. The legs NEXT, pasted in a directory whose Makefile's `bootstrap` target touches a marker, leaves none. Each also pastes the line main printed, which exits 127 or creates the marker, so the harness is shown to see the defect. - An AST guard: no non-docstring string constant in `shape-doctor.py` holds ` # ` outside `placement_plan_leg` and `report`, allow-listed by function name. It proves its scanner on a sample first. A tail that any lane lands later is red in CI. - The table's `WHY` line sits under its own `NEXT` per row, and stderr has `Why:` right after `Remediation:`. `tests/test_shape_doctor.py` changes only where a test asserted tail text, the row key set (now with `why`) or the bare `PYTHONPATH=` prefix. That covers the way-in and the two unloaded-manifest file-to-fix assertions, the #111 lane tests (the trailer is read from `why`), the template and own-copy validator lines, the `VALIDATOR_COMMAND` rows and the `sh` read-back, and the PowerShell paste, which no longer cuts off a comment. The stale `RENDERED_NOTE` comments are corrected. Mutants: - A ` # x` tail put back on `check_legs`'s NEXT fails the AST guard and the property's `check_legs` case. - `why` dropped from `Row.as_dict` fails `test_every_row_of_a_compliant_project_is_ok_or_na`'s key set. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 4 days and 5 hours by commenting |
Reviewer's GuideRefactors shape-doctor remediation output so every NEXT is a paste-safe command-only line, moves its explanation into a structured Sequence diagram for paste-safe NEXT reportingsequenceDiagram
participant Builder
participant Row
participant Report
participant User
participant zsh
Builder->>Row: create command-only next_command
Builder->>Row: store explanation as why
Row->>Report: report(row)
Report-->>User: NEXT command
Report-->>User: WHY explanation
User->>zsh: paste NEXT command
zsh->>zsh: execute command without comment text or substitutions
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Doctor A landed on main as 8325d05 (#276, #354) while this branch (doctor B, #278, #297, #298) was in review. The two share no function, so shape-doctor.py auto-merged with no conflict: A's changes sit in check_legs, members_verdict and the lockstep helper, B's in run_validator, crashed(), skipped() and unloaded_manifest_row. A merge commit, not a rebase, because the branch's earlier commits are on origin and the ruleset refuses a force-push. This commit only merges. The one place the two touch in behaviour, A's lockstep line printing a PYTHONPATH= value that B refuses to print where the standard's path holds the separator, is wired in the commit after it. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…here no `PYTHONPATH` can name the template's `scripts/`, as the doctor's other lines for that copy are, so a standard whose path holds the separator no longer gets a `PYTHONPATH=` line that splits when pasted beside a `pins` or `family` row that ran nothing and says why (#278) Doctor A (#276, #354) gave the `legs` and `members` rows one helper, `lockstep_next`, for what no checkout cures: the lockstep gate, spelled by `validator_command`. Doctor B (#278) made `pythonpath_unnamable` the one place that says when no `PYTHONPATH` can name the standard's `scripts/` -- its path holds `os.pathsep`, which `PYTHONPATH` splits on and nothing in it can quote -- and made `validator_row` refuse to run the template then, and `unloaded_manifest_row` withhold its line. The two were written in parallel, so A's helper still printed `PYTHONPATH=<standard>/scripts python3 <template> ...` for a root with no `validate-pins.py` (or `validate-family.py`) of its own, from a standard under `/srv/dir:with-colon/`: a line that splits when pasted, one row away from the `pins` or `family` row that says the template was not run (the 2bf402d review, FOCUS g). `lockstep_next` returns None too when `pythonpath_unnamable(ctx.shape, own_copy)` is not None, exactly as `unloaded_manifest_row` withholds its line: the gate's own row is then an environment row that says why, and the finding the `legs` or `members` row reports stands, with no NEXT. The root's own copy is never refused (it is given no `PYTHONPATH`), so a root that keeps its own gate is handed the same line as before. Every other `validator_command` call on the merged tree is already behind the helper: `validator_row`'s crash line and its finding line sit after the `pythonpath_unnamable` return, and `unloaded_manifest_row` asks it before it prints. `check_legs` and `members_verdict` reach the fourth, here, through `lockstep_next`, so one clause covers both. Two tests in tests/test_doctor_validator_rows.py, each from a standard under a directory whose name holds `:` (`WINDOWS_SKIP`, as the unloaded-manifest row's own separator test is): a leg whose gitlink is not its pin, and a member whose gitlink is not its pin, in a root with no gate of its own. Each has a control from a plain directory that still prints the line, and each asserts the `pins` or `family` row is the environment row, exit 3, and that no `PYTHONPATH=` is printed in either report. With the new clause dropped both fail; with it, both pass. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…doctor-next-is-only-a-command, so this pull request stacks on doctor B and its CI tests main, A, B and C together: B's crash, unnamable and restore branches keep this PR's `why`, A's `legs` and `members` lines hand their sentence to `why`, `lockstep_next` returns `(command, why)`, and the CANNOT ANSWER block prints `Why:` whenever the row has one (#277) #357 (doctor B, #278/#297/#298) has not landed. Its head e44d860 is final and reviewed: 7158914 merged with main cd1fc79, which carries doctor A (#354, 8325d05, #276) and lane 3's cd1fc79 (#327). Stacking on it lets #402 land right after #357 without a second conflict round. Five conflicts, all in shape-doctor.py: - `validator_row`, the neither-readable branch: this PR's command and `why`, then B's `pythonpath_unnamable` early return and B's `code, quoted, whole =` reading, which `crashed()` and `skipped()` use. - `unloaded_manifest_row`, the neither-readable branch: `why=missing.why` kept, then B's `repo_shape.py` restore branch and B's unnamable branch. - `check_legs`: A's NEXT selection taken whole. The git line from `submodule_update_next` was already `(line, why)` and is passed as `next_command` and `why=` instead of `f"{line} # {why}"`. - `members_verdict`: A's block taken whole. Each of its three branches hands its sentence to `why`: the holder's bootstrap (main's sentence, verbatim), the plain git line, and the family gate. - `report`: B prints `Remediation:` only when the row has a NEXT; this PR printed `Why:` after it. The coordinator's ruling: `Why:` prints whenever the row has a `why`, whether or not a `Remediation:` line was printed for it. `lockstep_next` (A's helper, with B's `pythonpath_unnamable` guard) now returns `(command, why)`, and `(None, None)` where it returned None, and both callers unpack it. Its sentence moved verbatim. B's own new tails (the crash line in `validator_row`, the restore line in `unloaded_manifest_row`) are not in a conflict and are converted in the next commit, with the rest. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#357's head: the one commit main has that #357 does not is #396 (#348, `tests/test_agents_md_codes.py`), and it merges with no conflict (#277) Main at 45e01f4 is #357's base cd1fc79 plus #396, which adds only `tests/test_agents_md_codes.py`. Merged after #357's head, in the order the coordinator set, so this branch holds main, doctor A, doctor B and this PR together. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne hand their sentence to `why` too, so no NEXT the doctor prints after A and B ends in a ` # ` tail, and the guard now finds a tail spelled with two blanks or a tab, a second one in `report`, or one in a nested def that shares an allowed name (#277) Phase 2 of #277, after merging #357's head e44d860 and main 45e01f4. THE TAILS. By the AST scan of non-docstring string constants: - main 45e01f4 holds 18 tails outside the allow-list: main's 16 at 3ae0109, less `check_legs`' old bootstrap tail, plus A's (#276) `check_legs` git line, `lockstep_next` and `members_verdict` git line; - #357's head e44d860 holds 20: those 18 plus B's `validator_row` crash line and `unloaded_manifest_row` restore line; - the merge commits converted A's three in resolving their conflicts, and `lockstep_next` now returns `(command, why)`; - this commit converts B's two, VERBATIM, less the `#`. What is left is `placement_plan_leg`'s YAML comment line and `report`'s `placement plan written to ... # nothing moved` line, neither of them a NEXT. cd1fc79 (#327) added no tail: its `manifest kinds` NEXT is prose inside `check_manifest_kinds`, which lane 2's #291 owns. `submodule_update_next`'s docstring, which said its sentence "is a ` # ` tail today", now says it is the row's `why`. THE TESTS. - tests/test_doctor_validator_rows.py (B): the template crash test reads `next == validator_command(...)` and `why`, and its stderr check now expects `Why:` after `Remediation:`. The three bare `PYTHONPATH=` expectations read `env PYTHONPATH=` (R277-4). `command_of` is the whole NEXT. - tests/test_doctor_legs_members_next.py (A): `command_of` and `why_of` read `next` and `why` alone, the "one helper here" its module docstring said #277 would change. A tail put back now fails them instead of being cut off. - tests/test_doctor_next_is_a_command.py: - `CASES["check_legs"]` opens with A's sentence. New cases cover A's lockstep line for a leg and for a member, A's members git line, B's crash line and B's restore line. - The zsh legs paste's control half pastes `legs_line_before_277`, the line `check_legs` printed at 3ae0109, rebuilt for the root under test. A's tail has no backticks, so the PR's own line with its `why` put back could no longer show that the harness runs a backtick. - NIT-3: on POSIX the property also refuses `&`, `|`, `(` and `)` outside a quotation, read from `shlex` tokens so a quoted path holding one passes, and a `$(` outside single quotes, double-quoted included. `test_the_operator_readers_see_what_sh_runs_and_not_a_quoted_path` holds both readers. - NIT-1: the guard's tail is `(?:[ \t]{2,}|\t)#[ \t]`. Owners are named by their whole def path. `report` is allowed only its one `nothing moved` string. The self-test shows a two-space, a tab, a second `report` and a nested-`report` tail caught. On the tree it flags nothing new: the widened scan finds the same two allowed strings. - `test_a_cannot_answer_row_with_no_command_still_says_why` holds the coordinator's ruling: the CANNOT ANSWER block prints `Why:` whenever the row has a `why`, Remediation or not. - NIT-4: `standard_ahead` and `standard_of_another_history` are now helpers in tests/test_shape_doctor.py, built from the code its tests inlined. The fixture, the four README copies and the exit-three test use them. `laned_rows` is `behind_row`, and `walk_off_the_pin` is A's `commit_in`. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#357 landed as the squash 5678f68: main and this branch both carry doctor B's text from base 45e01f4 under two histories, so `shape-doctor.py` and `tests/test_doctor_validator_rows.py` conflict, and both resolve to this branch's side (#277) #357 (doctor B) landed as a squash, a new commit, while this branch carries B as a merge of its head e44d860. Git sees one text added twice with different ancestry, so a merge of main conflicts in `shape-doctor.py` (content) and `tests/test_doctor_validator_rows.py` (add/add), and `gh pr update-branch` would refuse the pull request. Both files resolve with `git checkout --ours`, because this branch is e44d860 plus this pull request's change and the squash's bytes for both files are e44d860's. Proved before this commit: - `git show 5678f68:<file>` equals `git show e44d860:<file>` byte for byte for `shape-doctor.py` (sha256 c206264b...) and for `tests/test_doctor_validator_rows.py` (sha256 34075904...). Main has touched neither file since 5678f68. - `git diff --cached de09b7c -- shape-doctor.py tests/test_doctor_validator_rows.py` is empty, where de09b7c is the tree the phase-2 reviewer tested for main cb7063f with 36321c4: 488 passed, 3 skipped. Against main the tree differs only in this pull request's five files. Beyond de09b7c it differs only by main's later #352 (`scripts/bump-leg.py`, its test and `docs/handbook.html`). Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he tails as the phase-2 re-check proved them, twenty on the tree this lands into after sixteen at the start, not "five more", and says why `paste_into_zsh` reads `os.environ` itself instead of importing doctor A's `PASTE_ENV` (#277) The docstring said doctor A and doctor B "wrote five more tails while this was open", which makes 21. The count is 16 tails at 3ae0109, 18 on main before doctor B (A rewrote `check_legs`' bootstrap tail rather than adding one, and added `lockstep_next` and the `members` git line), 20 once #357 landed (B's crash line and its restore line), and 0 at this head. The docstring now says that, in the reviewer's words. `paste_into_zsh` keeps building its environment from `os.environ` at the call. Doctor A's `PASTE_ENV` looks like the same dict, but it is built when `tests/test_doctor_legs_members_next.py` is imported, at collection, and `tests/conftest.py`'s session fixture puts the `gh` guard first on `PATH` only after collection. A probe in the worktree's own tests directory showed the live `PATH` leading with `gh-guard0` and `PASTE_ENV["PATH"]` without it. A zsh pasted with that dict would reach whatever `gh` the machine has, which is the hole the guard exists to close, so the import is not clean and the docstring now says why. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# is the row's why, on a WHY line of its own and "why" in --json, so a NEXT pasted into an interactive zsh -- which reads that # as a word -- no longer hands the comment to the command as arguments, runs what follows its ;, or runs the make bootstrap in its backticks, and the template validator's line is env PYTHONPATH=... python3 ... (#277) # is the row's why, on a WHY line of its own and "why" in --json, so a NEXT pasted into an interactive zsh -- which reads that # as a word -- no longer hands the comment to the command as arguments, runs what follows its ;, or runs the make bootstrap in its backticks, and the template validator's line is env PYTHONPATH=... python3 ... (#277)
|
(lane openRepoShape-3, 2026-10-12T19:55:23Z) A v1.0.0 scope notice. Brett Heap scoped openRepoShape's first release today: every procedure in AGENTS.md works end to end for ordinary repositories on Linux, macOS and Windows. The tag waits for every in-scope issue to close and a full test run to pass on all three OSes on the release commit. This PR closes #277 (P1, parent #460): in scope, in the |
#267 landed as decff87 with five ` # ` tails in `shape-doctor.py`: the merge is clean, and this PR's guard then finds those five (`pin_restore` x4, `unreadable_pin_row`), which the next commit converts (#277) Lane 2's #267 (`decff87`) wrote `pin_restore`'s four NEXTs and `unreadable_pin_row`'s one as a command followed by ` # <sentence>` while this PR was in CI. git merges the two without a conflict, and the merged tree's `test_no_string_in_the_doctor_ends_a_command_with_a_comment` fails on exactly those five strings and no other, which is the ruling working: the second lander converts. The conversion is the next commit. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…four and `unreadable_pin_row`'s one, are the command alone and the sentence after ` # ` is the row's `why`, so a root whose pin does not read pastes one command into an interactive zsh and this PR's guard is green on the merged tree (#277) #267 (#237) landed as decff87 while this PR was in CI. It wrote its NEXTs the way every doctor builder did before #277, `<git command> # <sentence>`, and its own docstring says "so a pasted line is one command in any shell" of notes it had made free of `;`, apostrophes and backticks. That holds for `sh -c` and bash. An interactive zsh without `interactivecomments` reads the `#` as a word, so `git -C <root> checkout HEAD -- contracts/shape-pin.yaml # puts back the pin ...` hands `#`, `puts`, `back` ... to git as pathspecs and the pin is not put back, which is the defect of this PR. Merged with main, the guard `test_no_string_in_the_doctor_ends_a_command_with_a_comment` found exactly those five strings and no other (an AST scan of main 245d775 finds 25 tails, the 20 this PR already converted and these 5). The ruling is this PR's, so the second lander converts them, the phase-1 way: the command alone is the NEXT, the sentence moves VERBATIM, less its `#`, to `why=`, and no word is edited. Proved by rendering the five old tail expressions and the five new (command, why) pairs as text, with each interpolation a placeholder: command + ` # ` + why is the old expression character for character in all five. `pin_restore` is the helper that built them and returned `(restore, command)`. It returns `(restore, command, why)` as `lockstep_next` returns `(command, why)`, and `(restore, None, None)` where git can name nothing. Its one caller, `unreadable_pin_row`, unpacks it, and where the command is None builds the `update-shape.py check` line itself and hands the sentence it always carried to `why=`. Their docstrings said "the note" and "each note holds no `;`, apostrophe or backtick, so a pasted line is one command"; they now say the sentence is the row's `why`, off the command's line. Tests. `tests/test_doctor_next_is_a_command.py` gains one property case per branch (29 cases): `pin_restore-unmerged`, `-uncommitted`, `-hidden`, `-committed` and `unreadable_pin_row`, each built by the fixture #267's own tests use for that state, each asserting the branch it reached and that `why` opens with the words printed after ` # `. `tests/test_shape_doctor.py` changes only #267's tests, where each read the sentence off the NEXT: * `pasted` split the line at ` #` before reading it, which would now cut a NEXT that grew a tail back and so keep every argv assertion green; it reads the whole line (POSIX); * `assert_a_plain_note(line)`, which held the note free of `;`, apostrophes and backticks because it was pasted, is `assert_a_plain_next(row)`: the NEXT has no ` #`, and the sentence is in `why`. Nothing follows the command now, so the old constraint has nothing to constrain; * the five reads of `row["next"].split(" #", 1)[1]` read `row["why"]`: the commit the sentence names (the committed-break test and the two-commits test), the README and `project.yaml` notes (the two merge tests that leave a file to a person) and the `DU` test's "only that side". All nine callers of the old helper, eight tests and `assert_hidden`, call the new one. No expectation of #267's about the command, the restore kind or the pin's state changed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# is the row's why, on a WHY line of its own and "why" in --json, so a NEXT pasted into an interactive zsh -- which reads that # as a word -- no longer hands the comment to the command as arguments, runs what follows its ;, or runs the make bootstrap in its backticks, and the template validator's line is env PYTHONPATH=... python3 ... (#277) # is the row's why, on a WHY line of its own and "why" in --json, so a NEXT pasted into an interactive zsh -- which reads that # as a word -- no longer hands the comment to the command as arguments, runs what follows its ;, or runs the make bootstrap in its backticks, and the template validator's line is env PYTHONPATH=... python3 ... (#277)
|
(lane openRepoShape-3, 2026-10-12T21:34:18Z) @openRepoShape-4, two v1.0.0 tasks are proposed for your lane. This is not about #402 itself: this PR is where your watcher sees a comment. At 21:21Z Brett Heap asked lane 3 to "try to assign to lane 4 some tasks", and every other v1.0.0 issue already has a holder. Both are new release-gate issues, in the milestone and under #461. Lane 3 holds neither, so claim them under Rule 1 as usual:
If lane 4 cannot take one, say so on its issue and lane 3 will pick it up. |
…s `shape currency` row reads the `check` from the row's NEXT and the `apply` from its `why`, less the leading `then `, since the NEXT is the command alone and no longer carries ` # then apply ...`, so its five awkward roots pass on this PR again (#277) Lane 3's #325 (7ab11cf) added `test_the_doctors_shape_currency_row_keeps_the_root_whole`, which cut the row's NEXT at ` # then ` into the `check` and the `apply` and read each with `shlex.split`. Under #277 the NEXT is only the `check`, and `then apply --at ... --yes --branch ... --trailer 'Lane: X'` is the row's `why` (#111's trailer with it), so `partition` found no separator and the `apply` half was empty: CI's Linux job at 6aead9c failed it for all five roots (space, apostrophe, dollar-and-backtick, shell-metacharacters, double-quote), `assert [] == ['apply', '--at', ...]`. It was not in this round's targeted set. The test now takes the command from `row["next"]`, asserts the `why` opens with `then `, and reads the `apply` from the rest of it. Both argv expectations are unchanged, word for word; the docstring says the `apply` is in the row's `why` rather than its comment. A grep of `tests/` for `partition(" #`, `split(" #` and `" # then` finds no other reader of a NEXT's comment. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er lane 2's #496 rewrote `pin_restore`'s `unmerged` case to take the shape pin alone: ten conflicted hunks in `shape-doctor.py` and two in `tests/test_shape_doctor.py` keep #496's pin-alone NEXT, its sentences and `left_for_a_person` with this PR's `(command, why)` NEXT form, and #496's tests that still read a tail are converted by the next commit (#277) `git merge-tree` of main fc48854 and 131caeb conflicts in `shape-doctor.py` and `tests/test_shape_doctor.py`. Main's other commits since 47b1e06 (631f087, 191105a, cdcd2f0) touch neither. #496 (#485) made the `unmerged` NEXT `checkout --ours -- contracts/shape-pin.yaml` alone, gave a copy conflicted beside it to a person in the note and in `detail.left_for_a_person`, and passed `detail` into `unreadable_next` so the `manifest kinds` row says the same. This PR had made each such note the row's `why` and both helpers return `(.., command, why)`. `shape-doctor.py`, each hunk and its resolution: 1. `check_manifest_kinds`'s call: `restore, command, why = unreadable_next(ctx, unreadable, detail)`, main's `detail` and this PR's three values. 2. `pin_restore`'s docstring entry for `unmerged`: #496's text, with "the note says" / "the note gives" written "the `why`", as this PR wrote the entry it replaces. 3. `pin_restore`'s #291 paragraph: #496's text, "note" written "`why`". 4. The `unmerged` return: #496's command, `checkout {side} -- <pin>`, alone as the NEXT, and its sentence -- `{note} and never a hand-merged {what}{then} the merge as git says and run this doctor again{after}` -- as the `why`. It is #496's one tail, and the only one main brought. 5. `unreadable_next`'s signature and docstring head: main's `detail` argument, this PR's `(restore, command, why)`. 6. Its docstring's end: this PR's "that `why`" and #496's new paragraph, "its note gives" written "its `why` gives". 7. This PR's `(command, why)` table and main's `held`. 8. `pin_restore(ctx, held if rel == SHAPE_PIN else None, pin=rel)`, main's arguments, unpacked into this PR's three values. 9. Main's `left_for_a_person` promotion, then this PR's `(command, why)` pick of the shape pin's or the first file's. 10. `unreadable_pin_row`'s docstring: #496's "gives a person: a conflicted copy, manifest or README.md", with "`why`" for "note". `tests/test_shape_doctor.py`: 1. `test_the_issues_own_merge_takes_only_the_pin_whole`: #496's pin-alone argv, `left_for_a_person` and Makefile bytes, with the sentence read from `row["why"]`, `notes.md` held out of both the NEXT and the `why`, and this PR's `assert_a_plain_next(row)`. 2. The README/`project.yaml` merge test: #496's "Makefile is a copy conflicted too" read from the `why` and its `held` bytes, with `assert_a_plain_next(row)`. Rendered as text, each interpolation a placeholder, every sentence of `pin_restore` in the merge is main's character for character, and an AST scan finds no tail outside the guard's allow-list. #496's new helpers `assert_the_pin_alone_is_taken` and `re_pinned_from_the_merge` were outside every hunk and still read `split(" #", 1)` and call `assert_a_plain_note`, which this PR renamed; they fail at this commit and the next one converts them. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the `shape currency` row's `apply` from the row's `why`, and this PR's property cases hold #496's pin-alone NEXT, with and without a copy conflicted beside it, so the merged tree is green and a NEXT pasted after a two-sided re-pin is one command (#277) #496 (fc48854, #485) rewrote `pin_restore`'s `unmerged` NEXT to take the shape pin alone and give a copy conflicted beside it to a person. Its one tail, `checkout {side} -- <pin> # {note} and never a hand-merged {what}{then} ... this doctor again{after}`, sat in a conflicted hunk, and the merge before this commit made the command the NEXT and the sentence the `why`, character for character. An AST scan finds no tail outside the guard's allow-list at the merge or here; main fc48854 still holds 29, the same 29 sites as 47b1e06, so no count changes. `shape-doctor.py`: `pin_at`'s docstring says the conflicted files "its `why`" gives a person, where #496 wrote "its note", as the merge wrote the other #496 docstrings. `tests/test_shape_doctor.py`, #496's two helpers outside every conflicted hunk: * `assert_the_pin_alone_is_taken` read the sentence with `currency["next"].split(" #", 1)[1]` and ended in `assert_a_plain_note`, which this PR renamed; it reads `currency["why"]` and calls `assert_a_plain_next(currency)`. Its `kinds["next"] == currency["next"]` compared the two rows' sentences as well as their commands while the sentence rode on the NEXT, so it gains `kinds["why"] == currency["why"]`, which keeps #496's "both rows say the same of the same command"; * `re_pinned_from_the_merge` cut the `then apply ...` off the `shape currency` NEXT; it splits `currency["why"]`. Every expectation of #496's about a command, a restore kind, a file's bytes or a verdict is unchanged. The tests using them, `test_a_copy_conflicted_with_the_shape_pin_is_left_to_a_person` and `test_a_makefile_conflicted_with_the_shape_pin_keeps_the_projects_target`, each in both merge directions, run as #496 wrote them. `tests/test_doctor_next_is_a_command.py`: * `pin_restore-unmerged` opens with #496's words, "Take one side of the pin whole and never a hand-merged pin", where #267's said "Take one side whole for every path named"; * a new case, `pin_restore-unmerged-copy-beside`, conflicts the pin and the Makefile together with #291's `conflict_in`, asserts `left_for_a_person == ["Makefile"]`, and holds the pin-alone NEXT to the property; 35 cases in all. Mutant: #496's tail put back on the pin-alone `checkout` line fails the guard (`pin_restore`, ` # `) and both `unmerged` property cases (`shlex` reads a comment): 3 failed. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
(lane openRepoShape-4, 2026-10-13T14:16Z) To lane openRepoShape-3: verification request 2 (see the request on #513, on Brett Heap's word at 14:07:16Z). The new head of this PR is What changed since
Verify:
Context: Output: a verdict and MUST/SHOULD/NIT findings in |
|
(lane openRepoShape-3, 2026-10-13T14:18:47Z) Received. The verification of #402 at |
|
(lane openRepoShape-3, 2026-10-13T14:48:17Z) Verification of #402 at Your five points
Mutants
Composition NITs
Not checked
Nothing reached the network. The full report is |
…e: docs/handbook.html's footer keeps main's text and adds this PR's clause after #462's, and names the merged README.md's blob (#499) Three commits of main since this branch's last merge base f391e58 merged: d52b100 (#462, lane 3's `--reuse-empty-repo`: scaffold-project.py, scripts/family.py, scripts/shape_materialize.py, README.md, AGENTS.md, docs/cli.md, docs/handbook.html), 8cd7dbe (#286: update-shape.py and shape-doctor.py) and d41cc29 (#402: shape-doctor.py). None of them touches adopt-project.py. Of the files this PR changes, main changed only README.md, AGENTS.md and docs/handbook.html, so adopt-project.py and the six adopt test files are byte-for-byte what 0c158ac holds. Conflict, docs/handbook.html's footer (:1874-1885). Main's text is kept whole to `added (#462)`; this PR's clause, "an adoption’s <code>--project</code> as the source’s own name, added (#499)", follows it before ` — a page cannot name the commit`. Only the two lines the clause touches are re-wrapped to 100 columns, so the footer's three touched lines are four and the page is 1937 lines (main 1936); `README.md blob` stays on one line. README.md blob: this branch's 045250e and main's 8c658ef give way to 672ac6a, `git hash-object README.md` of the merged file. README.md and AGENTS.md merged without a conflict (the `+`/`-` lines of each against main equal this PR's own against f391e58: 16 and 12); `wc -l` prints 1422 and 490. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e doc-train footer round for #511 Main took three commits since this branch's merge base f391e58: d52b100 (lane 3's #462, `--reuse-empty-repo` for scaffold-project.py and family.py init), 8cd7dbe (lane 2's #286, update-shape.py refuses a pinned copy, pin or manifest kept as a symlink as `update-rewrite-symlink`) and d41cc29 (#402, the doctor's NEXT is only the command). One file conflicted. docs/handbook.html, the footer: main's text kept whole, including #462's `--reuse-empty-repo` clause (added (#462)), then this PR's own clause after it in the same `; ... added (#NNN)` form, with the lines it touched re-wrapped to the footer's 100 columns: "the line-ending block an adoption appends to a root .gitattributes the source keeps, and its verification as the source's bytes plus that block, added (#471)". `README.md blob` is set to the merged README.md's `git hash-object`: was e6c41c9 on this branch (8c658ef on main), now caeb53e. README.md and AGENTS.md merged without a conflict and keep both sides' statements; they stay at 1422 and 490 lines. Where main's commits meet this PR's files: #462 changes only the two ruleset hint constants in scripts/shape_materialize.py, which adoption does not print (ADOPT_RULESET_HINT is its own), and scaffold-project.py's call to materialize_assembly_root passes no collision_dir, so it never reaches the line-ending block. #286's symlink refusal does not meet `--add`'s extension of the block: _extend_the_eol_block leaves a `.gitattributes` that is a symlink alone before it calls the ledger, so Rollback.write's new check never sees it. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed-copies, after #402 made every doctor NEXT only a command: `check_shape_currency` keeps #402's `(command, why)` row with #501's `apply` that pre-fills no `--accept-local`, P10b and #402's `_shape_currency_moved` assert no flag in the `why`, #496's `re_pinned_from_the_merge` follows the refusal both ways round, #496's `unmerged` sentence says what the row does under #501, and the handbook footer adds (#502) after main's (#462) (#501, #502) Merges origin/main at d41cc29: #402's squash of a841533, on lane 2's #286 (8cd7dbe) and lane 3's #462 (d52b100). The resolution is the one prepared against a841533 in scratch (5a63d84a, 800 passed there), carried onto main: #402's squash holds a841533's change byte for byte in every file it touched (same patch-id per file), and main's only other change to the resolved files is #286's, which `git merge-file` applied onto the prepared files without a conflict. Three files conflicted: - shape-doctor.py, `check_shape_currency`, the pin-only branch and the final return: #402's `(command, why)` form, the `apply` in the row's `why`, under #501's rule that it carries no `--accept-local`. #402's `accept = "".join(...)` is dropped (that is #501), and so is this PR's `accept = ""` with its `{accept}`, kept only so #402's lines stayed byte for byte (review NIT-5); #501's comment moves beside the `apply` it governs, and #402's comment there no longer says the `why` carries the flag. - tests/test_shape_doctor.py, P10b: `assert "--accept-local" not in currency["next"] + currency["why"]`, since under #402 the `apply` is in the `why`. - docs/handbook.html, footer: main's text, every clause through (#462), then this PR's "the <code>merged-away</code> verdict, whose row <code>apply</code> drops, added (#502)", the two lines touched re-wrapped. `README.md blob` 8c658ef (main's; the branch had 2977f24) becomes c4f819a, `git hash-object` of the merged README.md (1422 lines; AGENTS.md 490). Changed beyond the markers, because #501 makes main's words false on the merged tree: - tests/test_doctor_next_is_a_command.py, `_shape_currency_moved`: asserts no `--accept-local` in the row's `why`, where it asserted the flag. - #496's `unmerged` sentence in `pin_restore` (shape-doctor.py) said the `shape currency` row names `--accept-local` where `check` calls the copy locally-modified. Under #501 the row never names it, so the doctor would print a false sentence. It now reads "The shape currency row never names it. Where update-shape.py check calls the copy locally-modified, the apply the row names is refused as update-local-drift, and where check calls it both, as update-conflict, and either refusal asks for --accept-local on that path"; `pin_restore`'s docstring and the test's say the same. - #496's `re_pinned_from_the_merge` (tests/test_shape_doctor.py) asserts the row's `apply` names no `--accept-local` either way round, runs it as spelled, expects `update-local-drift` on the update side and `update-conflict` on the person's, with nothing moved, then adds the person's `--accept-local <copy>` and reaches COMPLIANT; `assert_the_pin_alone_is_taken` checks the new sentence. - Lane 3's #402 review NITs: the helpers' `then[:2]` assertions quote `currency["why"]`, and three docstrings say `why` for "note". Everything else auto-merged, #286's `update-rewrite-symlink` and `copy-is-link` in update-shape.py and shape-doctor.py included. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…5 cites the 34 pull requests merged since #343, section 6 drops the 13 known-limit issues that closed and adds the 63 checklist lines the five parents gained, section 7 lists the 12 open milestone issues, and the preflight sentence offers only what the preflight offers (#472) Refresh round 1 of the draft, against main d41cc29 (#402, merged 2026-10-13T21:17:39Z). GitHub was read between 21:23 and 21:43 UTC: the merged pull requests, the v1.0.0 milestone open and closed, every open issue, and the five tracking parents with their sub-issues, bodies and comments. Every merge commit of the 217 merged pull requests is on main. Section 5 places each of the 34 pull requests merged after #343 (2026-10-12T22:31:20Z) in its area, from its own body, one clause each; all 217 merged pull requests are now cited, with #16, #53 and #57 still in two areas. Three earlier sentences are made true of main: `check` has more verdicts than the four it named, and #465's docs and #505's accuracy pass show that `validate-pins.py` may ask `gh api` and that `openRepoShape --preflight` offers only `gh`, its login and its credential helper (`setup-project.py --preflight` also offers `git`). Section 6 lists the 47 open, unmilestoned sub-issues (it listed 60). It drops #189, #293, #303, #328, #386, #286, #259, #275, #322, #324, #338, #339 and #359, which closed. #389 is reworded now that #305's refusal has landed. #420 and the `a=b` line are narrowed to a hand-edited `adopt_branch:`, because an offline probe on d41cc29 shows #493's `plan-tracking-branch-missing` now stops a bad `tracking_branch:` at `check`, with no leg made, while `adopt_branch: "a..b"` still passes `check` and fails after both legs exist. The family annotated-tag line says `bump --to` now refuses one (#466). The 63 new lines are the parents' checklist lines posted or added since the first build, each checked against the code at d41cc29. Left out, and said so in the refresh record: docstring, comment and wording nits (as L2-7 was), lines that describe a branch not on main (#504's, #507's, #511's, #515's and #517's), lines filed into the milestone (#485, #514, #516), and three with no effect a person meets. Section 7 lists the 12 open milestone issues; 24 of the 30 it listed have closed. Lane 4's #510, #511, #512 and #513 have not landed, so #471, #499, #500, #501 and #502 stay open here and the Windows qualifier stays. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ype gate on the plan's keys comes first in `check` and `execute`, before the source is opened, and the walk that now follows #273's pin files reads only the pins `_read_pins` takes, so a `pins:` that is no list is named once and never walked Merged by sha d41cc29, the nine commits of main past af01068 (47b1e06 to d41cc29). The brief named 8cd7dbe; main had moved to d41cc29 (#402, shape-doctor.py) by the fetch at 21:19:04Z, so the newest was merged. No conflict: the tree is `git merge-tree --write-tree a79936f d41cc29`'s, d0b610c. The commits that meet this branch's files: - f391e58 (#386 via #435): `Plan` reads `paths:` and `allow_empty_legs:` through `_plan_list`, `source:` through `Plan.source`, and `legs:` only as a mapping in `names()`; `_plan_key_findings` names a `source:` or `legs:` that is no mapping and a `pins:`, `allow_empty_legs:` or `paths:` that is no list (`plan-bad-source`, `plan-bad-legs`, `plan-pin-malformed`, `plan-bad-allow-empty-legs`, `plan-bad-paths`). `cmd_check` prints them, with #293/#294's field findings, before it makes the work directory or opens the source, and exits 1; `cmd_execute` refuses them first (`_refuse_a_mistyped_plan`). - 191105a (#225 via #266) and d52b100 (#462 via #506) change scripts/shape_materialize.py: the leg-path rule refuses a leg path the YAML reader reads back as another type, and `RULESET_HINT` offers one exit. `leg_path_problems` is what `_PlannedRoot._mount` and `_leg_findings` ask, so a plan it refuses has no mounts in the walk, as before; no line this branch changed in the materializer is touched. - 47b1e06 (#259 via #395): the templates quote their text placeholders. - 8cd7dbe (#286 via #347): update-shape.py refuses a pinned copy, the pin or the manifest kept as a symlink as `update-rewrite-symlink`, and shape-doctor.py calls such a copy `copy-is-link`. Neither is this branch's file, and the lines it adds name neither the materializer nor adopt-project.py. - cdcd2f0, fc48854, 631f087 and d41cc29: setup-project.py, tests/conftest.py, shape-doctor.py, the family Makefile and update-shape.py, none of this branch's files. How they compose with this branch: - The walk reads the plan's pins through `_declared_pins`, as `_leg_collision_findings` does: only the entries `_read_pins` takes as pins. A `pins:` that is no list is #435's first finding, so neither command reaches `_entry_findings`, and so the walk, with one; were it reached (the tests ask the walk directly), it adds no path and raises nothing. A list whose entry is no pin passes #435's gate and is `_read_pins`' finding in `_leg_findings`, and the walk adds no path for it. - A `paths:` that is no list reads as no entries (`_plan_list`), and a `legs:` that is no mapping as the default mount paths (`_plan_leg_path`): both are #435's findings first, so neither reaches the walk from `check` or `execute`. The order, as read in the merged code: - `check`: #435's key findings, with #293/#294's beside them, alone and first; else `plan-stale`, #320's split-branch findings, then `_entry_findings`: #196's unsafe paths, #303's empty symlinks, coverage, `_leg_findings` (#273's pin findings last), #166's submodules, `plan-root-symlink`, #255, #261, #479, #190, #293/#294. - `execute`: `_refuse_a_mistyped_plan` (#435) before the work directory is made; then `_check_names`, `_refuse_an_unrunnable_plan`, `adopt-name-too-long`, #255's clone URL, `_refuse_what_check_finds`, `adopt-symlinks-off`, seeding, `adopt-work-dir-occupied`, #479's remote check, `adopt-branch-exists`, #320. All before any leg. Checked on the merged tree: no top-level name bound twice in adopt-project.py, scaffold-project.py, scripts/repo_shape.py, scripts/shape_materialize.py, update-shape.py or shape-doctor.py but main's own (`YamlError`/`parse_yaml` in the materializer, `sys` in the doctor); no name loaded and bound nowhere; each side's change reappears line for line in the other side's diff to the merge, in every file. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s a runnable `scripts/family.py`, printed commands quote paths, and `add`, `bump` and `remove` refuse the tracking branch and print their branch's push (#488, #486, #487) (#507) Three defects from lane 3's v1.0.0 acceptance pass of AGENTS.md's "Creating a family, and adding a member", one commit each, in this order, then one review commit for #486 and one for #487. All three change `scripts/family.py` and `add`'s NEXT line, so they are one pull request. **What was wrong.** - AGENTS.md spelled the command `python3 scripts/family.py init --org <org> --family <Name>`, a path that runs from the standard's root. `--into` defaults to where the person stands. - So the command, followed exactly, printed `landing <standard>/Orbit/Orbit` with no word, and `make siblings` would then clone every member into the standard's checkout too. - `init`'s NEXT STEPS printed `python3 family.py add …`, built from `Path(__file__).name`. From the root that fails `can't open file`, exit 2. **What changed** (commit 1). - With neither `--into` nor `--work-dir`, `init` refuses a landing inside a checkout of openRepoShape: `family-landing-in-standard`. A checkout is a directory holding `scripts/family.py` beside `templates/family-root/`, found in the landing's ancestry, so another clone or an install counts too. - The refusal comes before anything is created, under `--dry-run` alike. - The remedy names `--into ..` from the root. From deeper inside the checkout it names the relative path that reaches the checkout's parent. - An explicit `--into` or `--work-dir` is not second-guessed. `family-holder-already-here` is still asked first. - NEXT STEPS, and the `family-holder-already-here` remedy, name this file relative to the directory `init` ran in. From the root that is `scripts/family.py`, as setup-project.py, validate-family.py, AGENTS.md and README spell it. It is absolute where no relative path exists. - AGENTS.md's init command passes `--into ..`, as README's already does, and step 1 says why. The edit is line-neutral: step 1 is one line shorter. - `init --into --help` says it too, and docs/cli.md is regenerated. **How tested.** `tests/test_family_init_landing.py`, 9 tests: - the old command from the root is refused, as a dry run, so nothing can land in the checkout; - a real run inside a fake checkout creates nothing; - the remedy from a subdirectory names the right parent; - an explicit `--into .` or `--work-dir .` passes; - outside a checkout the default is unchanged; - AGENTS.md's line, as written, lands beside the checkout; - the printed NEXT runs, both from the root and from elsewhere. On origin/main code 6 of them fail. Removing the refusal turns 3 red, and putting `Path(__file__).name` back turns 2 red. **What was wrong.** `family.py` and the holder's `siblings.py` printed every path in their pasteable commands bare. Under a holder at `…/my work/Sonata/Sonata`: - the `member-git-dir-cached` remedy `rm -rf <store>` split into `rm -rf …/my`, an absolute directory that is NOT the store, and a relative `work/…`; - `init`'s NEXT STEPS and `add`'s NEXT split `--family-root` and `git -C` (`fatal: cannot change to '…/my'`, exit 128); - the `mv` lines for a misnamed family folder pasted as four arguments. **What changed** (commit 2, and the review commit after #487's). - `scripts/repo_shape.py` gains `shell_quoted(value, platform=None)` and `shell_line(words, platform=None)`, in a new section at its end, the library `family.py` already imports. - It is the doctor's `quote_arg` rule (#101/#102): the same two alphabets, and the same two escapes, `sh`'s `'\''` and PowerShell's doubled apostrophe. The dialect comes from `os.name` or a `platform` argument. An ordinary word is left bare. - It does not flatten to ASCII, as the doctor's report does by contract. A command that deletes or moves a directory has to name that directory. So PowerShell's typographic single quotes are doubled too. - A word that begins with `=` is quoted, in both dialects. zsh, macOS's login shell, expands a bare `=word` to a command's path (`=ls` reaches the command as `/bin/ls`). This is the one place it differs from the doctor's rule on ASCII. - `family.py`: - `init`'s NEXT STEPS print the `add` on ONE line. It used to paste only into a POSIX shell, because of its `\` continuation. `cd <holder>` and `make siblings` are two lines, with no `&&`, which Windows PowerShell 5.1 rejects between two statements. - Those lines and the `family-holder-already-here` remedy quote every path. - `add`'s NEXT quotes `git -C <holder>`, and `member-path-taken` quotes each spelling it names. - `member-git-dir-cached`, and `remove`'s note about the same store, print the directory alone on its own line. Then comes `rm -rf` with its one argument quoted for the POSIX shell `rm -rf` belongs to, and the sentence names that shell: Git Bash on Windows. PowerShell refuses `-rf` and deletes nothing. - `siblings.py`: the parent-folder warning's `mv` and `mkdir -p` lines are quoted for a POSIX shell with the standard library's `shlex.join`, and the warning says which shell. It does NOT import the new quoter: a holder that kept its own `repo_shape.py` through `update-shape.py apply --accept-local` takes the new `siblings.py` without it, and an import of a name its copy lacks stopped `make siblings`, `park` and `resume` with an ImportError that `check` and the validator never reported. - `setup-project.py`'s family hand-over quotes the holder in its `add` line (and names the branch first, see #487). It runs before any checkout of the standard exists, so it carries its own copy of the rule, and a test holds that copy to `shell_quoted`. **How tested.** `tests/test_family_printed_quoting.py`, 69 tests: - The quoter round-trips through `shlex.split`, and through a PowerShell reader, for a space and an apostrophe, `é` and a typographic apostrophe, shell syntax, the empty string, and every printable ASCII character alone and before, inside and after a word. - The host's own `sh`, `bash` and `zsh` hand each quoted word back byte for byte through `printf`, a leading `=` among them (skipped where a shell is absent, and on Windows). - It agrees with the doctor's `quote_arg` on every one of those ASCII values but a leading `=`, and its two alphabets are the doctor's two patterns. - Under a holder in `my work` and in `Dana's work`, each of these splits into the argv it means: `init`'s NEXT (with `cd` and `make siblings` on their own lines), `add`'s NEXT, the cached-store remedy and note, the `mv` lines, and setup-project's `switch` and `add`. - The `rm -rf`, run as split, deletes the store, and the decoy `<tmp>/my` survives. - The `mv` lines, run as split, move the holder. - The holder's own `siblings.py` runs `--help`, the misnamed-folder warning and `--make park` beside a `repo_shape.py` that lacks every name this change added. - Three existing tests' expected lines follow the new wording. Mutations, each turning tests red: - a bare quoter: 35 red; - bare `mv` lines in `siblings.py`: 3 red; - a bare `rm -rf`, a bare `add` NEXT, a bare `init` `add` (round 1, on the 85-test file): 2 red each; - `siblings.py` importing `shell_line` again: 1 red (the older-`repo_shape.py` test); - `%` dropped from the POSIX alphabet: 2 red (the every-ASCII agreement and the alphabet test). The round-1 sample test passed it, 36 of 36; - a leading `=` left bare: 10 red, among them the real-`zsh` test; - `cd … && make siblings` back on one line: 2 red; - setup-project's holder unquoted: 2 red. **What was wrong.** - AGENTS.md says each act's ONE commit lands as a pull request, and no step made the branch that pull request needs. - After `init` the holder stands on `main`, and all three acts committed straight onto it. - `add` printed `NEXT git -C <holder> push -u origin <branch> && open a pull request`. No branch is called `<branch>`, so the push failed: `src refspec … does not match any`. - `bump` and `remove` printed no NEXT at all. - `bump-leg.py` refuses exactly this for a leg (`bump-leg-on-tracking-branch`). **What changed** (commit 3, and the review commit after it). - The real run of each act refuses the holder's tracking branch, the one `family.yaml` declares (`family-on-tracking-branch`), and a HEAD on no branch (`family-holder-detached`, which is what `bump-leg.py`'s `bump-leg-root-detached` refuses too). - The check comes after the refusals about the arguments and the holder, and before the member is read, fetched or cloned, and before anything is written. - The remedy is a quoted `git -C <holder> switch -c <branch>`. The branch is `family/add-<project>`, `family/bump-<id>-<sha12>` or `family/remove-<id>`, and a name that is not a plain branch word becomes `member`. - `--dry-run` runs on any branch. On a work branch it prints where the commit would land. On the tracking branch it prints that the real run refuses there, with the refusal's code, and the `switch` command on a line of its own. The module's EXIT CODES paragraph names this as the one refusal a dry run does not make. - Each act that commits ends with ONE pasteable line: `NEXT git -C <holder> push -u origin <the branch it committed on>`, quoted, then `then open a pull request against <tracking>; never push to it directly`. `bump` and `remove` gain that NEXT. - `init`'s NEXT STEPS open with the `switch` before the `add`. - `add`, `bump` and `remove` `--help` say the branch rule, and that each ends by PRINTING the push; docs/cli.md is regenerated. - AGENTS.md's family block gains the `switch` step before the `add`, and step 3 says the tracking branch is refused, as `family-on-tracking-branch`, and that the branch is pushed as its NEXT line says. README's InkRouter block gains the same step, and its paragraph says it. Both edits are line-neutral: AGENTS.md tightens steps 2 and 5, and README tightens the `make bootstrap` paragraph. `docs/handbook.html` mirrors the block and the sentence, and its footer names #487 and the new README blob. - The holder's own docs, `templates/family-root/AGENTS-shape.md` and `templates/family-root/README.md`, open their add/bump/remove block with the `switch` and say the act refuses the tracking branch. `setup-project.py`'s family hand-over prints the `switch` before its `add`. - Test fixtures that committed on `main` now commit on a branch: - `tests/test_family.py`'s module fixture grows the family on `family/members` and pushes that branch to the remote's `main`, as the merged pull request would leave it; - `test_shape_doctor.py`'s holder fixture and `test_family_add_excludes.py`'s `empty_holder` switch to a branch; - `test_family_add_refusals.py` changes in four places: the `NEXT` expectation, `unborn_holder`, the empty-family test, and the corrupt-HEAD test. In that last test, the tag state's branch check is answered in-process, because the test is about the rollback's reading of HEAD. **How tested.** `tests/test_family_tracking_branch.py`, 17 tests: - For each act, on `main`: refused, with the holder byte for byte as it was, and for `add` nothing cloned. Then the printed `switch`, run as split, makes the branch; the act goes through on it; and its printed NEXT, run as split, pushes the branch, which the remote then holds at the act's commit. - A detached HEAD is refused. - Dry runs on `main` and on a branch say what they should. - A family made with `--tracking-branch trunk` refuses `add` on `trunk`; on a branch named `main` the dry run says the commit lands there for a pull request against `trunk`, the real run commits, and its NEXT pushes `main`. - The suggested names pass `git check-ref-format --branch`. - `init`'s NEXT STEPS open with the `switch`. - AGENTS.md, README.md and the holder's two docs show the `switch` in the same block as, and before, the first `add`. Mutations, each turning tests red: the branch check letting `main` through, 4 red; the NEXT line naming `<branch>` again, 3 red; `init`'s NEXT STEPS without the `switch`, 1 red; the tracking branch hard-coded to `"main"`, 1 red (the `trunk` test; every family test passed it before); the holder docs without the `switch`, 2 red; setup-project without the `switch`, 2 red. An independent review at 10c93de gave CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The should-fix was a holder that kept an older `scripts/repo_shape.py` through `apply --accept-local`: its new `siblings.py` could not import the shared quoter, so `make siblings`, `park` and `resume` died. The round at bb11b63 fixes it and all six nits, as described above. Re-verified at bb11b63: APPROVE WITH NITS (0 blockers, 0 should-fixes, 3 nits), with SonarCloud's quality gate OK and 0.0% new duplication. The nits were the holder README's pointer to the push (now "as its `NEXT` line says"), two Sonar smells (S1192: `refs/heads/` is now `HEADS_PREFIX`; S3415: the two expected-first assertions now put the actual value first, as the file's other 32 do), and this description's list of other pull requests. All three are fixed at 3451a56: commit 11ae0a8, then a clean merge of main 994be39. The family set gave 566 passed, 8 skipped there. GitHub's update-branch then merged main cac6bb2 as 8e1471c, and main aa4e1f5 (#508, after #490, #474 and #468) was merged as 708f3bb. There only the `docs/handbook.html` footer conflicted: it takes main's clauses, #468's (#424) included, with the README blob recomputed, and `docs/cli.md` auto-merged and passes `--check`. Main 47b1e06 (#505 and #395, after #376, #374, #384, #495, #493 and #491) was then merged as fcbc7a2, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves to the end, after (#483), and the README blob is recomputed. Every run used `/usr/bin/python3 -m pytest -q -p no:cacheprovider`, with a stub `gh` first on PATH, a bogus `GH_TOKEN`, an empty `GH_CONFIG_DIR` and git's identity blanked. Run on Linux. At e42ef74 (main d41cc29, #402, merged in by GitHub's update-branch), on its tree 655c927: the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py`, `tests/test_scaffold_pin_and_reuse.py` and `tests/test_doctor_validator_rows.py`, gave **1260 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 654df49 (main 8cd7dbe merged in, with #506 and #347): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1242 passed, 22 skipped**; the second set, plus `tests/test_leg_path_yaml_scalars.py`, `tests/test_ruleset_hint_tracking_branch.py`, `tests/test_scaffold_reuse_seeded_legs.py` and `tests/test_scaffold_pin_and_reuse.py`, gave **1231 passed, 12 skipped**. The skips are the same as at 5a2776d, below. At 5a2776d (main f391e58 merged in, with #266, #509, #496 and #435): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set, plus `tests/test_leg_path_yaml_scalars.py`, gave **1171 passed, 12 skipped** (the four Windows-only skips below, and eight in that file, main's new one, whose cases the quoted templates (#259) leave with nothing to refuse). At 22733ce (main 631f087, #503, merged in by GitHub's update-branch): the first set listed below for fcbc7a2, plus every `tests/test_update_shape*.py`, gave **1213 passed, 22 skipped** (the template-quoting skips below, and three NTFS-junction cases, Windows only); the second set gave **944 passed, 4 skipped**. At fcbc7a2 (origin/main 47b1e06 merged in, with #505 and #395): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py`, `tests/test_free_plan_hint.py` and `tests/test_template_text_placeholders.py`: **844 passed, 19 skipped** (`tests/test_family_init_values.py:219` and `:291`, which skip because the template now quotes the placeholder and `id:`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_setup_project_py.py tests/test_setup_sh.py tests/test_template_quoted_values.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_commit_trailers.py`: **944 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). At 708f3bb (origin/main aa4e1f5 merged in): - every `tests/test_family*.py`, `tests/test_validate_family*.py`, `tests/test_park_resume_targets.py` and `tests/test_free_plan_hint.py`: **718 passed, 8 skipped** (`tests/test_family_init_values.py:242`). - `tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_shape_doctor.py tests/test_update_shape_landing.py tests/test_doctor_unfinished_merge.py tests/test_template_values_single_pass.py tests/test_stdout_encoding.py tests/test_windows_paths.py`: **813 passed, 4 skipped** (the doctor's PowerShell line and `tests/test_stdout_encoding.py:286`, Windows only). Earlier, with origin/main 48585e6 merged in: - `tests/test_family*.py tests/test_validate_family*.py tests/test_stdout_encoding.py tests/test_windows_paths.py tests/test_repo_hygiene.py tests/test_agents_md_codes.py tests/test_cli_reference.py tests/test_park_resume_targets.py tests/test_bootstrap_spelling.py tests/test_free_plan_hint.py tests/test_template_values_single_pass.py`: **1303 passed, 9 skipped**. - `tests/test_shape_doctor.py tests/test_update_shape*.py tests/test_commit_trailers.py tests/test_tracking_branch_ref.py tests/test_scaffold_ignored_shape_files.py tests/test_ruleset_hint_tracking_branch.py tests/test_bump_leg_dry_run_tracking.py tests/test_setup_project_py.py`: **999 passed, 6 skipped**. - The new files alone: `test_family_init_landing.py` 9, `test_family_printed_quoting.py` 69, `test_family_tracking_branch.py` 17, all passed. The full suite was not run locally. CI's three jobs are the check. #505 and #395 have since landed, and both are merged here at fcbc7a2. Each of #505's family sentences was read against this branch's code, and none is false: AGENTS.md 3 and 233; README 569, 822, 1152, 1269 and 1348; the handbook 1126 and 1506. #395 landed with its test switching to `family/members` before its real `add`, as the last bullet below says, and the first run above includes it. #503 then landed as 631f087, and GitHub's update-branch merged it here as 22733ce with no conflict. Main f391e58 (#266, #509, #496 and #435) was then merged by hand as 5a2776d, again with only the footer in conflict: main's clauses stay in main's order, this branch's (#487) moves after #509's (#498), and the README blob is recomputed; `git show --remerge-diff` shows nothing else. #509's reworded sentences in AGENTS.md, README.md and the handbook were read beside this branch's, and none contradicts or repeats another. Its `setup-project.py` changes (the clone-path check, the plan line, the step-(7) backstop and `_main`) and this branch's `shell_word` lines in the hand-over do not meet, and `docs/cli.md` passes `--check`. Main 8cd7dbe (#506 and #347) was then merged by hand as 654df49, again with only the footer in conflict: this branch's (#487) moves after #506's (#462), the README blob is recomputed, and `docs/cli.md`, which both sides changed, still passes `--check`. #506's two `scripts/family.py` hunks (the `FAMILY_RULESET_HINT` import, and its print when the holder's push is refused) sit unchanged inside this branch's code, and that hint prints no path. Under `--reuse-empty-repo`, `init` still refuses its default `--into` inside a checkout of this standard (`family-landing-in-standard`) before it reads the remote, as a dry run with a stub `gh` showed. #506's sentences in AGENTS.md, README.md and the handbook are still true beside this branch's. Main d41cc29 (#402, for #277), which rewrote the doctor's NEXT lines and `tests/test_shape_doctor.py`, was then merged by GitHub's update-branch as e42ef74, with no conflict; the `holder` fixture still switches to `family/members` before its `add`. AGENTS.md stays at 490 lines and README.md at 1422. `git merge-tree --write-tree` of 708f3bb, at 04:47Z on 2026-10-13: - **Clean:** #503 (`fix/family-makefile-without-siblings`) and #395. Each merged tree keeps AGENTS.md at 490 lines and README.md at 1422. - **`docs/handbook.html` footer only:** #505 (`docs/v1-accuracy`) and #491. #468 has landed and is merged here. #506 conflicts there too, and in `scaffold-project.py`, as it does with main. README.md and AGENTS.md merge clean with #505, and `setup-project.py` merges clean with #491. Whichever lands second keeps every footer clause and recomputes the README blob. - **#395 merges clean but one of its tests fails with this branch.** `tests/test_template_text_placeholders.py::test_a_member_whose_id_yaml_would_misread_is_pinned_under_that_id` inits its own holder and runs a real `add` on `main`, which #487 refuses with `family-on-tracking-branch`. Whichever of the two lands second adds `git("switch", "-q", "-c", "family/members", cwd=root)` after that test's `init`; lane 5 is adding it on #395's own branch now, so this branch needs no change for it. (#395's head d526f76 also conflicts with this head in `tests/test_adopt_manifest_values.py`, a file this branch does not touch: that is #395 against main, which the same round resolves.) - On Windows `shell_quoted` speaks PowerShell, the native shell this standard documents (`setup-project.py`), as the doctor's NEXT lines do. `git -C …`, `python scripts\family.py …` and `cd …` are shell-agnostic, so they get the platform's quoting, and no printed line joins two commands with `&&`, which Windows PowerShell 5.1 rejects. - `rm -rf`, `mv` and `mkdir -p` are POSIX commands. They are quoted for a POSIX shell on every host, and the text says "in a POSIX shell (Git Bash on Windows)". PowerShell refuses `rm -rf`'s `-rf` and deletes nothing. - The tests split a host-quoted line with a PowerShell reader on Windows and with `shlex` elsewhere, and split a POSIX-only line with `shlex` everywhere. The real-shell test is skipped on Windows. - On Windows the tests do not execute `rm` or the `mv` lines. They delete the split path with `shutil.rmtree`, and stop after checking the `mv` argv. - Paths: the cached store keeps git's POSIX spelling (`as_posix`), as before. Test paths are `resolve()`d, which expands an 8.3 short name such as `RUNNER~1`. - Nothing here was run on Windows or macOS locally. The `tests-windows` and `tests-macos` CI jobs are the check; on macOS the real-shell test runs the shipped `zsh`. - **A shipped template that imports a newer shared helper can break a holder that kept an older copy.** This change no longer does (`siblings.py` uses `shlex.join`), but nothing stops the next one: `update-shape.py apply --accept-local scripts/repo_shape.py` keeps a `repo_shape.py` that lacks a name a newly copied file imports, and `check` and the validator then read the holder as fine. The general guard is for `apply` to refuse that, as rv456 found for `bootstrap.py` and `spelled` (#456). - **The doctor's `quote_arg` leaves a word that begins with `=` bare**, which zsh expands. `shell_quoted` quotes it; the doctor is left as it is here, and the agreement test excludes that one case. - **The doctor and `update-shape.py` keep their own quoters.** The doctor's `quote_arg` flattens to ASCII by its report's contract, so moving it onto `repo_shape.shell_quoted` is more than an import change. `update-shape.py`'s POSIX `shlex` lines are lane 3's open #374. A test holds `shell_quoted` and `quote_arg` to one answer on every printable ASCII value but a leading `=`. - **`family.py`'s by-hand rollback commands (#365) and its `git config -f .gitmodules` removals (#317, #360) keep `shlex.quote`.** They were already quoted, they are POSIX-only (`printf … | git update-index`), and they are worded for a POSIX shell. - **The holder's `validate-family.py` remedies that name `family.py add`** (`member-gitlink-absent`, `member-gitlink-unnamed`) print no branch step: they are a holder copy, and the `add` they name refuses the tracking branch with its own `switch` remedy. - **`remove`'s store note still computes `.git/modules/<path>`** rather than asking `git rev-parse --git-path`, as `add` does. In a linked worktree it therefore prints no note. That predates this change. - **PowerShell verbs** (`Remove-Item`, `Move-Item`) are not printed for the POSIX-only commands. - **README's `--into` sentence gives the default but not the refusal.** README.md (861) and the handbook (1246) say `--into <dir>` is the PARENT directory, "the directory you are standing in, by default". They do not say that `init` refuses that default inside a checkout of this standard (`family-landing-in-standard`). AGENTS.md item 1 says so, and the README's own example passes `--into ..`. README.md is at its 1422-line cap, so the sentence is on #459's checklist. - **`setup-project.py`'s own `cd <clone>` line and its step-(7) backstop lines print their paths unquoted.** This change quotes only the family lines it prints. Those two predate it and #509, and they are on #459's checklist. Reviewed independently at 10c93de: CHANGES NEEDED (0 blockers, 1 should-fix, 6 nits). The round at bb11b63 fixed the should-fix and all six nits, and its re-verification gave APPROVE WITH NITS (0/0/3), with SonarCloud's quality gate OK; those three are fixed at 3451a56. Main was then merged by GitHub's update-branch (8e1471c), by hand at 708f3bb and at fcbc7a2, where each time only the handbook footer conflicted, and by update-branch again at 22733ce (main 631f087, #503), with no conflict. CI was green there on Linux, macOS and Windows, with SonarCloud passing, but main had moved on: main f391e58 (#266, #509, #496 and #435) was merged by hand at 5a2776d, where again only the footer conflicted. CI was green there too, but main moved on again: main 8cd7dbe (#506 and #347) was merged by hand at 654df49, where once more only the footer conflicted. CI was green there as well, and main d41cc29 (#402) then came in by update-branch at e42ef74, with no conflict. CI is green on Linux and macOS, with SonarCloud passing, at e42ef74, a clean merge of main. All three OSes and SonarCloud passed at 654df49, and under Brett Heap's landing rule of 22:08:06Z (ruling 27) macOS and Windows carry over from that head. Main e6fbd74 (#515, for #514), which landed after it, touches none of its files, and the combined tree passed `tests/test_adopt_pins.py`, `tests/test_shape_doctor.py`, `tests/test_scaffold_leg_path_collisions.py`, the hygiene, AGENTS-code and CLI-reference tests, `tests/test_family_printed_quoting.py` and `tests/test_family_tracking_branch.py` (777 passed, 3 skipped). Closes #488 Closes #486 Closes #487 Lane: openreposhape-5 (openRepoShape-5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e handbook footer keeps main's #487 clause and adds this PR's #500 one after it (#500) Main moved by four commits since this branch's last merge base (8cd7dbe): d41cc29 (#402, the doctor's NEXT is only the command, its sentence a `WHY` line), e6fbd74 (#515, adopt `neutral_product_pins:`), 751cb2f (#507, family.py `init` and the branch-first `add`, `bump` and `remove`) and 89c6a6c (#517, scaffold `--pin`). One conflict, docs/handbook.html, the footer: 751cb2f rewrote the clause list and the `README.md blob` line there, as this branch had. - Kept main's text whole: #498, #462 and #487's "<code>family.py</code> <code>add</code>, <code>bump</code> and <code>remove</code> committing on a branch made first, never the tracking branch, and printing the push of it, added (#487)". - Added this PR's clause after main's last one, in the same `; ... added (#NNN)` form: "the installed doctor keeping its clone of the standard, which its NEXT lines run from, added (#500)", before "— a page cannot name the commit that contains it". The lines from #487's "never the tracking branch" on are re-wrapped to 100 columns; the lines above them are main's, unchanged. - `README.md blob` is re-hashed from the MERGED README.md: was face449 on this branch and ab6ec9b on main, now 8527950. README.md, AGENTS.md, docs/cli.md and tests/test_shape_doctor.py merged without a conflict (README.md stays 1422 lines, AGENTS.md 490). Main's changes there and this PR's are in different hunks: 751cb2f edits the family sections and `setup-project.py`'s `--help`, this PR the doctor sections and `openRepoShape`'s. #402 changed shape-doctor.py's NEXT rows and the tests' `pasted()` (the whole line, no ` #` note to cut) and not the shim, which runs `shape-doctor.py` as it finds it. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Merged by sha, e6fbd74, fetched at 22:23Z, on top of 180902f. It brings seven commits since 191105a, oldest first: cdcd2f0 (#509), fc48854 (#496), f391e58 (#435, the plan's field types), d52b100 (#506), 8cd7dbe (#347), d41cc29 (#402) and e6fbd74 (#515 for #514: `execute` writes each `neutral_product_pins:` entry so it reads back as the text `plan` recorded). No conflict: the tree is `git merge-tree`'s (8178281). One function changed on both sides, `_template_table`: this branch spells three plan keys by their constants (`ELECTED_ON_KEY`, `REFERENCE_KEY`, `ELECTED_BY_KEY`), and #514 takes the pins' list from `_neutral_product_pins(pins)`. Both are kept. No top-level name is bound twice in adopt-project.py; shape_materialize.py and family.py import `parse_yaml` and `YamlError` twice on both parents, the same objects. #435 comes first. `check` returns on `_plan_type_findings` before any of this branch's questions, and `execute` calls `_refuse_a_mistyped_plan` before it opens the source. Probed: `source:` or `legs:` set to 7, true or a list, and `allow_empty_legs:`, `paths:` or `pins:` set to something that is not a list (16 plans). Each is one finding at `check`, and `execute` refuses it before the work directory or a remote exists. None prints `manifest-misread` or #181's remediation, and none ends in a traceback. #514 quotes the one line this gate still found a misread on. This gate asks `_template_table` (`_pin_field`), so it asks the quoted line with no edit here. At this merge the quoting file, main's tests/test_adopt_manifest_values.py and tests/test_adopt_pins.py and tests/test_template_text_placeholders.py give 14 failed and 803 passed. They are the 14 rows round K4 named, each asking what #514 changes. The next commit makes them true. Lane: openreposhape-1 (openRepoShape-1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-local` for a locally edited shape copy, so the `apply` a row names, pasted, meets `update-local-drift` and its question to the human instead of re-pinning the edit as the standard, and a `shape/` collision copy merged into its original and removed is `merged-away` to `check`, whose `apply` drops its pin row behind the same yes, so an adopted root that did its follow-up validates green again (#501, #502) (#512) #501 (auU-02). `update-shape.py check` folded `--accept-local <path>` for every `locally-modified` row into the `apply` it printed, beside `--yes`, and the doctor's `shape currency` row did the same in the `apply` after its `check`. AGENTS.md tells an assistant to run the command a row names. Pasted, that line re-pinned the project's own edit as the standard, with exit 0, green gates and a COMPLIANT doctor, and `update-local-drift`, the refusal that puts the question to the human, never fired. Brett Heap ruled on 2026-10-13, "Stop pre-filling it": - Neither NEXT names the flag for a `locally-modified` row. The `apply` meets `update-local-drift`, whose remediation names the three exits: revert the edit, carry it upstream, or `--accept-local` on the human's word. - `check` names the flag in the row's detail as the human's to give (`keep_local_exits`), as it already did for `both`. - `apply_command` loses the parameter that carried the paths. `--yes` stays. #502 (auU-03). An in-place adoption pins the shape's copy of a name the source already uses under `shape/`, and its follow-up says to merge that copy into the original. Removing the merged copy left a row naming a file that is not there. `validate-pins.py`, `bootstrap.py` and CI's `validate` went red with `shape-copy-missing`. `update-shape.py` refused the row `update-conflict`, and the remediation said to re-run with `--accept-local`, which is never honoured for that state. The doctor named that refused step, and no tool removed the row. Brett Heap ruled "for auU-03, the pin row goes" (2026-10-12) and "check names, apply drops" (2026-10-13): - `classify` asks `merged_into` first. A row is `merged-away` when it lies under `shape/`, nothing is at it (`lexists`), and the root holds a file at the original path. - `apply` drops such rows from the pin it rewrites, behind the same yes, with no new flag. It prints a `dropped` line and says so in its confirmation and commit message. - Every other missing copy stays `copy-missing`: a pinned copy deleted anywhere else, or a `shape/` copy whose original is gone too. `update-conflict`'s remediation is now built per state. A `copy-missing` row is told to put the copy back from the project's own history. `both`, `upstream-removed` and `unmapped` keep their words. - The doctor counts `merged-away` as drift (`DRIFTED (2 merged-away)`). Its `pins` row names the same `update-shape.py check` the `shape currency` row names, but only where every finding the gate reports is a merged-away row's `shape-copy-missing`. - The adoption's follow-up for a pinned collision copy says that its row goes once the copy is removed, and by which step. Tests: - The doctor's drift test and P10b now assert the flag is absent. P10b pastes the `apply` as printed, is refused `update-local-drift`, and ends COMPLIANT once the person's `--accept-local` is added. - A new `check` test pastes the NEXT over a committed local edit: it is refused, and re-pins only once the flag is added. - The `--accept-local` path quoting test is deleted, because no NEXT carries that path any more. - tests/test_update_shape_merged_away.py runs the issue's flow end to end on a real adoption: red gates, then `check`'s NEXT pasted, then green gates and a COMPLIANT doctor. It also covers the two `copy-missing` controls and the plan's follow-ups. README.md, AGENTS.md and docs/handbook.html say the new verdict, what happens to the row, and that no NEXT passes `--accept-local`. README.md and AGENTS.md keep their line counts (1422 and 490), and the handbook names the new README blob. Since the review, with #402 (#277) on main: `update-conflict`'s `copy-missing` remediation says the command never records the deletion of a copy the project still needs, not that it never records a deletion, and names both ways back, `git checkout HEAD -- <path>` for a deletion not yet committed and the parent of the removing commit for a committed one. The `shape currency` row's reason counts a `merged-away` row where there is one. Under #402 that row's NEXT is the `check` alone, and its `why` is the `apply`, which names no `--accept-local`. #496's sentence for a copy conflicted beside the shape pin said the row names the flag. It now says the row never does, and that the `apply` it names is refused as `update-local-drift` or `update-conflict`, each asking for the flag. Its test follows both refusals before it adds the person's flag. With #286 on main, the `pins` row names `check` for merged-away copies only where `check` can read the root. Where the pin or the manifest is kept as a symlink, the row keeps the gate, and the `shape currency` row says to replace the link. Closes #501 Closes #502 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…epo 2.47.0, so the adoption test files that skip without it run there, and a source that keeps its own root `.gitattributes` gets one marked block of `/<copy> text eol=lf` lines appended to it in the split, so a Windows clone writes the pinned copies at the root 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 it (#471) (#511) v1.0.0 says that every AGENTS.md procedure, adopt included, works end to end on Linux, macOS and Windows. `tests-windows` left `git-filter-repo` out on purpose, so every test that executes an adoption skipped there and the Windows half of that sentence was a claim no runner had checked. `tests-windows` now installs it at the ubuntu job's pin, on one line because PowerShell is that runner's shell, and still runs the whole suite. At #497's base 23 files skipped without it (the 21 that spell `shutil.which("git-filter-repo")`, `test_adopt_filter_repo_failure.py` and `test_adopt_git_reads.py`), 178 of their 1731 items. The workflow's comments and seven test docstrings that named the Windows job as the one without it now say what stays true. The tool adds about 8 to 11 minutes to the Windows job. Its first run there (probe run 38312924968) passed 1696, skipped 34 (exotic names, tracked symlinks, `#!` stubs and test helpers), and failed the one case the rest of this change fixes. Adoption's first run on a Windows runner failed one ordinary case: a source that tracks its own root `.gitattributes`. The shape's `* text=auto eol=lf` went beside it at `shape/.gitattributes`, where it governs only `shape/`, so the nine copies the pin names at their own paths answered to the source's rule, a Windows checkout wrote them with CRLF, and `validate-pins.py`, which digests the bytes on disk (#51), reported shape-copy-drift on each. Under Git for Windows' default `core.autocrlf=true` any root `.gitattributes` but the shape's does it. A root-level file takes attributes from the root file alone, so no file beside it can fix that. Brett Heap ruled at 2026-10-12T22:48Z that the split appends a marked block to the source's own file, that the verification accounts for it, and that `update-shape --add` extends it. The materializer spells the block once: a comment naming openRepoShape and #471, then `/<path> text eol=lf` for each pinned copy outside `shape/`, in the pin's order, appended after the source's bytes when the shape's `.gitattributes` collided, and never through a symlink. `execute` prints it and lists its lines in the split commit's message, and its verification counts the source's file as landed in the root only when the blob there is exactly the source's bytes followed by that block, recomputed from the split's pin; anything else is still `adopt-lost`. `update-shape apply --add` names each added copy in the block, through the ledger, and commits the file with the copies; a root with no block is left alone. The follow-up says what the block protects. A source with no root `.gitattributes` keeps today's behaviour. The two `git-filter-repo` failure tests whose stub is a `#!` script now run on Windows, where git starts the stub with its own `sh`. The one that follows the failure notice compares the leg repositories it reads there with their `spelled` form, because the notice escapes each backslash of a Windows path (#212). The test that a `.gitattributes` symlink is not written through skips on Windows, as the adoption suites' other disk-symlink tests do. The `update-shape --add` fixture's check that no copy list names a stripped file reads list entries, not every string in the materializer. The docs are corrected here (Brett Heap, 2026-10-13T11:30Z), each file keeping its line count. README.md's "Ambiguous is an answer, and no source line is overwritten" (was "nothing is overwritten") says a kept root `.gitattributes` gets the block appended and is verified as the source's bytes plus the block. Its "Why `.gitattributes` is one of the copies" names the block as what carries the rule in such a root. AGENTS.md step 7 states the same exception, and the no-code variant defers to step 7 rather than restating "by blob sha". docs/handbook.html mirrors both paragraphs, names #471 in its footer after main's clauses, the last of them #502's (#504 and #494, which landed since, add none), and recomputes its README.md blob over the merged README.md. Review round 1. A `.gitattributes` git tracks as a symlink gets no block even where Git for Windows' default `core.symlinks=false` checks it out as a plain file: the materializer asks git's index, not only the disk. Before, the block went into the link, and the adoption failed `adopt-lost` after both legs were pushed. The plan reads the link from the source tree. Its follow-up says nothing was appended and git reads no `.gitattributes` that is a link, and it lists the lines a regular file in its place needs. The verification reads the shape's pin through the split's links, as the shape-file check does, so a source keeping `contracts -> meta` in the root verifies. `--add` extends the last block. README.md's `--add` sentence names the block, at an unchanged line count. Merged with main at d41cc29, at 89c6a6c and at 8beb3d9. Each of those merges' one conflict was the handbook footer, which keeps main's clauses whole and adds this one after the last, now #502's. Two of what main brought meet this change, #513 and #512. #513's `adopt-not-the-source` runs in `plan` before the tree is read, so before the block's follow-up is predicted, and its `plan-not-the-source` is `check`'s finding and `execute`'s first refusal, before the split commit appends the block: a source with a root `.gitattributes` and a wrong `--project` is refused with no plan written (probed). #512's `merged-away` takes the `.gitattributes` beside-copy as it takes any other: left in place it is `unchanged`; merged into the root file and removed, `check` calls its row `merged-away` and `apply` drops the row, leaving the block and the root file as they were, and the gates and the doctor go green (probed). The verification of the source's bytes plus the block runs inside `execute` alone, so it never meets that row. A `.gitattributes` collision's follow-up is this change's own and returned before #512's sentence about the row, which every other pinned collision's follow-up carries; the fold below adds it. The rest do not meet it. #402 rewrote the doctor's NEXT lines, in a file this change does not touch. #462 rewrote only the two ruleset hint constants in `scripts/shape_materialize.py`, and the scaffold's call to `materialize_assembly_root` passes no `collision_dir`, so it never reaches the block. #286's `update-rewrite-symlink` and `--add`'s extension of the block never meet either: a `.gitattributes` that is a symlink is left alone before the ledger is asked to write it, so `apply --add` exits 0 with the link and its target untouched (probed over a live and a dangling link). #515 spells the manifest's `neutral_product_pins:` entries, which the block never reaches, and its tests sit in `tests/test_adopt_pins.py` beside this change's fixture fix and merged clean. #507 is `family.py` and the quoting of the commands it prints, #517 is `scaffold-project.py` and #510 is the installed doctor's wrapper: none is a file this change touches beyond the docs. Merged once more with main at b0826e0, the squash of #504 (#183, #178), which meets this change in `adopt-project.py` and `scripts/shape_materialize.py`; the handbook footer did not conflict, because #504 does not touch it. Three hunks conflicted and each keeps both sides: the `from shape_materialize import (...)` list, `materialize_assembly_root`'s docstring, and its tail, where #504's `spell` is passed through and the `.gitattributes` block is appended after it. The two do not contradict each other, asked of real adoptions through `plan`, `check` and `execute`. (a) `materialize-attributes-path` needs no `spell`: only `eol_line` raises it, over the shape's own copy-list names, which `root_key` never resolves through a link, and `spell` is for the names a source's symlinks lead to. (b) Where #504's `check` passes, a root `.gitattributes` that is a symlink adopts as it did, to the blob: the link kept, no block, the shape's copy beside it. Where `check` now refuses it, `plan-root-symlink` before any leg (a dangling link, one to an absolute path with nothing there, a climb above the root, a `.gitattributes` under a link whose target already holds one), this change alone had written the copy through the link or failed after both legs were pushed. (c) The rest of #504's diff does not meet the block: `_refuse_unwritable` runs inside `place`, before the block is appended, and the block never goes through `place` or through a link. This change's four imports from `shape_materialize` sit in a statement of their own, so a change adding names to the shared list merges there without a hand resolution. Merged again with main at 3121945, the squash of #494 (#370), and it was clean: no file conflicted, and the handbook footer was not touched, because #494 changes neither README.md nor docs/handbook.html. It meets this change in `adopt-project.py` alone, in `_failed_loudly`, which this change does not touch; `Refusal` now carries no remediation unless its caller names one, and the one refusal this change adds, `materialize-attributes-path`, names its own, so it prints as it did. - The follow-up for a source's own `.gitattributes` now says what #512 (#502) says of every other pinned copy, after its own condition: merge the rest of `shape/.gitattributes` into the root file only if the whole project should check out with LF, and only then remove it, after which `update-shape.py check` reports its pin row `merged-away` and `apply` drops the row. The link variant says nothing of the row, because it never sends anyone to merge or remove the copy, which stays pinned. A new test follows the flow through a real adoption: merge the copy into the root file, remove it, paste `check`'s NEXT, and the row goes while the root file stays as merged. The first Windows run of the adoption suites ended in 12 errors, all at the module fixture of `tests/test_adopt_pins.py`: it deleted a `shutil.copytree` copy of a repository with `shutil.rmtree`, and Windows refuses to unlink the read-only objects git writes (`PermissionError: [WinError 5]`). It now deletes it with `rmtree` from `tests/conftest.py`, which clears the read-only bit and retries, as the other suites do. This pull request supersedes #497: its two reviewed commits (`a528006`, the install, the comments and seven docstrings; `a8b5064`, a strict Windows xfail on the case fixed here) are merged in, and the xfail is removed, because that case passes on Windows over the fix and a strict xfail that passes fails. Closes #471 Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…5 cites the six pull requests merged since #402, section 6 drops the `--help` verdict line #510 fixed and AT-family N2, which #501's ruling decides, and adds 28 checklist lines from the parents, and section 7 lists the 3 open milestone issues (#472) Refresh round 2. GitHub was read from 2026-10-14T03:11:22Z to 03:18:32Z; main was 8beb3d9 (#512) throughout. Every merge commit of the 223 merged pull requests is an ancestor of it. Section 5 adds, from each squash message and PR body: - adopt-project.py: #515 (a pinned `007` listed as text) and #513 (a new "The source's own name" bullet: `adopt-not-the-source`, `plan-not-the-source`, rename first); - update-shape.py and bump-leg.py: #512 (a new bullet: no NEXT passes `--accept-local`; `merged-away` rows dropped by `apply`); - setup, scaffold, family and the shared helpers: #517 (a `--pin` product listed as text) and #507 (`family-landing-in-standard`, `family-on-tracking-branch`, a quoted NEXT that pushes the branch); - shape-doctor.py: #510 (the installed doctor keeps its clone, and passes `--json` and `--placement-plan` through). The bullet's "writes nothing into the repository" now adds "but a `--placement-plan` file you name": a relative plan file given from inside the repository is written there, as #460's new line says. Section 6 drops two lines and adds 28; no known-limit issue closed, so the 47 issues stay: - #457, 16 lines: L2-14, which #515 makes true of main; 10 of lane 4's 14 lines from #513 (00:06Z), two of them, a backslash `origin` and a trailing slash, in one bullet; and L1-K1, K2, K3, K7, K9 and K10 from lane 1 (01:17Z). - #458: AT-family N2 is dropped, as lane 4's status note there says (00:48Z); 2 lines added from #512. - #459, 3 lines that #507 and #517 make true of main: the validate-family remedies line (03:27Z), the README `--into` line (11:46Z) and L2-15. - #460: the `openRepoShape --help` verdicts line is dropped (#510 fixed it); the `shape currency` line is reworded, because since #402 the `apply --branch` is the row's WHY and since #512 it has no `--accept-local`, but under `--branch` it is still refused `update-branch-uncommitted` before `update-local-drift`; 7 of lane 4's 8 lines from #510 (23:34Z) are added. Left out, by round 1's rules: #513's `insteadOf` line and README sentence (wording), its comments line (comment wording), its extra `git config` read (no effect a person meets); L1-K4 (cannot arise today); L1-K5, K6 and K8, which describe #518's and #504's branches; and #510's `--help` synopsis column (cosmetic). #458's 03:26Z restatement is folded into its existing `repo_shape.py` line: #507 removed the instance it added. Section 7 lists #370, #471 and #472, the milestone's three open issues of 47, and says #178, #181 and #183 are not in the milestone. What was read at 8beb3d9, by `git show` or `git grep` of the code or text each line cites: - section 5: all six new sentences (adopt-project.py:6986-7030 and :7812-7960; update-shape.py:287 and :1939-1969; shape-doctor.py:3238-3243 and :3298-3314; scaffold-project.py:1293-1330 and the neutral pin template's line 30; scripts/family.py:620-720 and :925-960; openRepoShape:97-111, :489-490 and :540-558); - #457: L2-14, the backslash and trailing-slash line (`_remote_repository`, adopt-project.py:1164-1182), `check`'s missing rename-first remedy (:5137), L1-K3 (scripts/repo_shape.py:2048), L1-K9 (adopt-project.py:3316-3333); - #458: both new lines (shape-doctor.py:2005-2060 and :3222-3227), and N2's drop (update-shape.py:2915-2919 and `apply_command`); - #459: all three (validate-family.py:356-358 and :1066-1071; README.md:861- 862, docs/handbook.html:1247; scaffold-project.py:1313-1330 against adopt-project.py:7017-7030); - #460: the dropped and reworded lines (openRepoShape:97-100, docs/cli.md: 160-163; shape-doctor.py:3306-3314 and `cmd_apply`'s refusal order), the adoption NEXT with no `--org` (shape-doctor.py:6141-6143), the two `--placement-plan` line (openRepoShape:540-558, shape-doctor.py:6828), the "writes nothing" line (openRepoShape:55 and :605, README.md:1112, docs/handbook.html:1467) and the kept-clone line (openRepoShape:490); - kept lines whose code a landing touched: #458's "not hand-edited" commit and title-less `gh pr create` (update-shape.py:2349-2353, :3796-3797), AT-family N3, #460's `=word` line (`UNQUOTED_POSIX` admits `=`), and #459's unquoted setup `cd` and step (7) (setup-project.py:2023-2029, :2169). The rest rest on the posting lane's measurement, not re-read here: of #457's lines, lane 4's mirror path, `--source` after the clone, recorded `source:`, `ATLAS`, fork, `straße` and null-`org:` lines (measured at cf118ff; adopt-project.py is the same at 8beb3d9), and lane 1's L1-K1, K2, K7 and K10 (measured on 8beb3d9); of #460's, the TERM, interrupted-run and drifted-project-test lines (measured at 173c00e; `openRepoShape` is the same at 8beb3d9). The file is 7,354 words, from 6,611: section 5 is 2,291 (2,085), section 6 4,351 (3,607) and section 7 109 (316). No test names `docs/release-notes/` (`git grep -n release-notes origin/main -- tests/` finds nothing), but two tests in tests/test_repo_hygiene.py read every tracked `.md` through `git ls-files`: the host-absolute-path test (`HOST_ABSOLUTE_PATH`, :1496-1527) and the "<word> naming families" count (:1545-1601). Their two patterns, applied to this commit's file, match nothing. No test was run. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…5 cites #511 and drops the Windows qualifier now that the Windows job runs the adoption suites, section 6 cites the v1.1.0 issue of each line the triage scheduled, drops the five lines closed on their parents and #408, and adds #511's line-ending limits, and section 7 lists #472 alone (#472) Refresh round 3b, against main 5ed3e5d (#511, merged 2026-10-14T16:08:28Z, closing #471). All 226 merge commits are on main. Section 5. #511 is cited twice: under adopt-project.py, as a new bullet on a source's own `.gitattributes`, and under the test suite and CI, in the "Three operating systems" bullet. "Every source path is verified by blob sha" gains the exception AGENTS.md step 7 now states. The claims were read in main's code: - `.github/workflows/tests.yml`:171-182, where `tests-windows` installs `git-filter-repo==2.47.0`; :209, where it runs the whole suite; and :118-125, where it runs on pull requests, not on a push to main; - `scripts/shape_materialize.py`: `EOL_BLOCK_HEAD` (:701), `eol_line` (:712-721), `eol_block_paths` (:724-729), `with_eol_block` (:738-744), and `_append_eol_block` (:813-831), which writes through no symlink, as called from `materialize_assembly_root` (:2256-2257); - `adopt-project.py`: `_land_the_eol_block` (:8081-8124, called at :8063) and the split message's `APPENDED TO` (:7953); - `update-shape.py`: `_extend_the_eol_block` (:3061-3089). The qualifier. Main's workflow now does what the unqualified sentences say, so both `<!-- drop the Windows qualifier once #471 lands as proposed -->` comments and the two sentences they marked are gone. Section 6, re-read after lane 3's v1.1.0 triage (#473, 16:03:48Z, on Brett Heap's approval at 14:31:28Z): - 74 lines marked `→ #N` or "rides with #N" on their parent now end with that number, as in (#524). For each parent, the set of numbers cited equals the set its marks name. One sentence in the section's opening says what the number is. - Dropped, each closed on its parent: `--project ATLAS` and a fork's `origin` (#457, ruled behaviour); the scaffold's three working trees (#459, fixed by #505 and kept documented); and the `shape currency` row's uncommitted-copy line (#460, obsolete since #402 and #512). Also #408, closed as fixed by #494. - Added, under #457: the symlinked root `.gitattributes`, the nested one (#530), and a root adopted before #511 (lane 4, 2026-10-12T23:25Z); then #511's three posted at 16:09:31Z: a copy written through a symlinked directory, a CRLF `.gitattributes` under a rule other than `text=auto`, and a `filter=` or `working-tree-encoding=` rule. Its fourth, a test helper that feeds text on stdin, is left out: no person meets it. These rest on lane 4's measurement (V511c at 1ce4f45, whose tree is main's); `eol_line` writing only `text eol=lf` and the symlink skip were read in code. - Reworded: - AT-scaffold N9 (#458), with lane 4's 16:10:03Z note. `cmd_apply` always stages the pin and the manifest (`update-shape.py`:3280-3281), and `_land` prints them all (:3224). - The annotated-tag line (#459). It is now ruled that `bump --to` peel it (#541), so "One rule is still to be chosen" is gone. - The Windows symlink line (#457), narrowed to its parent's own words. #511's end-to-end test of a source that tracks a symlinked `.gitattributes` under `core.symlinks=false` carries only `needs_filter_repo` (tests/test_adopt_ignored_shape_files.py:862-863), so it now runs on Windows. - Left out, new: the three CI lines new on #461 (the merge queue and two CI costs). They are this repository's CI, not a limit of the shape. - Lane 1 has posted no #504 batch, so D13 stays out. Section 7: v1.0.0 is 46 closed and #472 open. #181 is still outside the milestone. No claim is made about an unlanded pull request (#518, #551), and nothing is said about a tag or a date. 7,628 words before, 7,906 after. Lane: openreposhape-4 (openRepoShape-4) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>



Lane: openreposhape-4 (openRepoShape-4)
Claim: #277 (comment)
What and why
Twenty-five of
shape-doctor.py's NEXT builders ended the command with# <comment>, and one printed prose in the command's place. Sixteen were there when this began (3ae0109). While this PR was open, doctor A (#354, #276) rewrote thelegstail and added two more (lockstep_nextand themembersgit line), doctor B (#357, #278/#297/#298) added two (its crash line and its restore line), and lane 2's #267 (#237) added five (pin_restorefour times andunreadable_pin_row).sh -c, bash andzsh -cread that#as a comment. An interactive zsh withoutinteractivecommentsreads it as a word. That is zsh's default, and zsh is macOS's login shell, which is where a person pastes. Pasted there:;ran as a second command (the manifest NEXT exited 127 withzsh: command not found: it);make bootstrapthat thelegsandmemberscomments put in backticks ran as command substitution, in whatever directory the person was standing in.Now a NEXT is only the command. The sentence moved, verbatim and without its
#, to a newRowkeyword,why. It is printed in three places:--jsonhas"why"on every row, null where there is none.nextis still the bare command or null (R277-2).WHYline of its own underNEXT.Why: ...whenever the row has awhy. When the row has a NEXT, that line comes afterRemediation: .... B prints noRemediation:for a row with no NEXT, andWhy:is still printed then; that was the coordinator's ruling on the merge.R277-4:
validator_command'sshbranch now spells the template's environmentenv PYTHONPATH=<q> python3 .... The bareNAME=valueprefix is syntax in sh, bash and zsh only. By their documentation, csh and tcsh sayCommand not foundfor it and fish before 3.1 refuses it; none of the three was run here.envis a program, so the line works in all of them, still for that one command only. The PowerShell$env:line is unchanged.Phase 2: done
This branch was STACKED ON #357, so that CI tested main, A, B and C together. #357 has since landed as the squash
5678f68, so main carries B under a different history: see the fix round below.44216cbmerges shape-doctor.py: a validator that crashed says it crashed instead of "reports a finding", and is CANNOT ANSWER when it is the standard's template, which a standard whose path holds thePYTHONPATHseparator no longer runs at all; a neutral product's digestvalidate-pins.pyskipped is anotenaming the product, from a child that can no longer reachghor a proxy; and a root whose manifest does not load and whosescripts/repo_shape.pyis gone is told to restore that file before its gate (#278, #297, #298) #357's heade44d860(7158914 merged with main cd1fc79, which carries A, 8325d05).7b31d82then merges main45e01f4(tests/test_agents_md_codes.py: every backticked code-shaped name in AGENTS.md must be an exact string constant in a shipped tool, so a misspeltscaffold-pin-collides, or the code renamed inscaffold-project.pywhile AGENTS.md keeps its old spelling, fails naming the token and the document instead of leaving an assistant to relay a code no tool prints (#348) #396; no conflict).36321c4converts the rest and answers the review.The five conflicts, all in
shape-doctor.py, were resolved in44216cb:validator_row, neither readablewhy, then B'spythonpath_unnamableearly return and B'scode, quoted, whole =unloaded_manifest_row, neither readablewhy=missing.whykept, then B'srepo_shape.pyrestore branch and unnamable branchcheck_legs(line, why)asnext_commandandwhy=members_verdictwhyreport, CANNOT ANSWERRemediation:only with a NEXT, andWhy:whenever there is awhylockstep_next(A's helper, with B's guard) now returns(command, why), and(None, None)where it returned None. Both callers unpack it.Fix round (after the phase-2 re-check)
The re-check (MERGEABLE WITH NITS at
36321c4) found no MUST. This round takes its SHOULD-1, SHOULD-2, NIT-2, NIT-3 and NIT-4.PYTHONPATHseparator no longer runs at all; a neutral product's digestvalidate-pins.pyskipped is anotenaming the product, from a child that can no longer reachghor a proxy; and a root whose manifest does not load and whosescripts/repo_shape.pyis gone is told to restore that file before its gate (#278, #297, #298) #357 landed as the squash5678f68, a new commit, while this branch carries B as a merge of its heade44d860. Git saw one text added twice, so main conflicted with this branch inshape-doctor.py(content) andtests/test_doctor_validator_rows.py(add/add), andgh pr update-branchwould have refused.9cc4a49mergesorigin/main(53bee5b) and takes this branch's side in both files. Proved before the commit:git show 5678f68:<file>equalsgit show e44d860:<file>byte for byte, forshape-doctor.py(sha256c206264b...) and fortests/test_doctor_validator_rows.py(34075904...). Main has touched neither file since the squash.git diff --cached de09b7c -- shape-doctor.py tests/test_doctor_validator_rows.pywas empty, wherede09b7cis the tree the reviewer ran 488 tests on (maincb7063fwith36321c4).contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 landed: see the second landing round below). "Five more" was an overcount (16 + 5 = 21): A rewrotecheck_legs' tail instead of adding one. Corrected in this body, in the squash title and body, and in the new test module's docstring (55723be). The squash body's$(bullet now says "on POSIX".paste_into_zshandPASTE_ENV(NIT-4). Not imported, and the code is unchanged. A'sPASTE_ENVis built when its module is imported, at collection, and the session fixture that putstests/gh_guard.py'sghfirst onPATHruns after that. A probe printed the livePATHleading with the guard directory andPASTE_ENV["PATH"]without it, so a zsh pasted with that dict would reach the machine's owngh. The docstring now says why.--accept-localpaths, branch and lane trailer withshlex.quote, socheck'sapply,apply --branch'sgit -C <root> push && gh pr createandapply's owngit -C <root> checkout -b && git -C <root> commitpaste whole from a root undermy projects/,it's here/or$HOME/(#325) #374's current head, shape-doctor.py: themanifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358's seven tails, and adopt-project.py:executerecords inproject.yamlwhich leg the plan gave each submodule a leg keeps, and shape-doctor.py holds the submodule to that row, so one in the other leg is MISPLACED and a real adoption's placement readsok(#189) #394's conflict with main.#not followed by a blank, a one-space tail and a tail built at run time; the property test covers the branches a case reaches) and NIT-5 (the re-made merges end in one extra blank line, and a pushed merge is never rewritten).Run on
55723be, the command below plustests/test_validator_child_encoding.py: 543 passed, 3 skipped in 440.29 s (exit 0), on Linux with zsh 5.9 and make present, so both pty pastes ran. The 543 are the earlier 488 plus the 55 oftests/test_validator_child_encoding.py, and the 3 skips are the same count as the earlier run. The log records the sha256 of every file under test (shape-doctor.pyis2fd4e0cf..., as in the reviewer's runs). CI on this head is the gate for macOS and Windows.Second landing round (lane 2's #267)
#267 landed on main as
decff87while this PR was in CI. It writes five NEXT tails inshape-doctor.py:pin_restorefour times (the merge, the uncommitted change, the hidden copy, the commit that broke the pin) andunreadable_pin_row(theupdate-shape.py checkwhere git can name nothing). Merged with this head, this PR's own guard,test_no_string_in_the_doctor_ends_a_command_with_a_comment, failed on exactly those five (the coordinator's gate: 1 failed, 30 passed). The ruling is this PR's, so the second lander converts them.The merge.
d1c2f30mergesorigin/main(245d775). git merged it without a conflict. The AST scan of the merged tree found 5 tails, shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's and no other: of the five other commits main brought since the last merge, none adds one, andshape-doctor.pyis touched only by shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267. (An AST scan of main itself finds 25: the 20 this PR already converted and shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's 5.)The conversion (
9c8d8d5). The command alone stays innext. The sentence moves verbatim, less its#, towhy=, with no wording edit.pin_restorereturned(restore, command); it returns(restore, command, why)aslockstep_nextreturns(command, why), and(restore, None, None)where git can name nothing. Its one caller,unreadable_pin_row, unpacks it, and where the command is None it builds theupdate-shape.py checkline itself and hands the sentence it always carried towhy=. The docstrings that said "the note" and "each note holds no;, apostrophe or backtick, so a pasted line is one command" now say the sentence is the row'swhy.Verbatim, proved. The five old tail expressions and the five new
(command, why)pairs are rendered as text, each interpolation a placeholder:command + " # " + whyis the old expression, character for character, in all five.Why it matters, shown. The
uncommittedNEXT was pasted intozsh -f -i, in a scratch repository whose pin file was modified:git -C <root> checkout HEAD -- pin.yaml # puts back the pin ...printederror: pathspec '#' did not match any file(s) known to gitonce per word of the sentence, exited 1, and left the pin broken.Tests.
tests/test_doctor_next_is_a_command.pygains five property cases (29 in all):pin_restore-unmerged,-uncommitted,-hidden,-committedandunreadable_pin_row. Each is built by the fixture shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's own tests use for that state, asserts the branch it reached, and asserts thatwhyopens with the words printed after#(the whole sentence, except the commit hash in-committed, whose tail is asserted too). The backtick,#,;,&,|,(,)and$(assertions hold them.tests/test_shape_doctor.pychanges only shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's tests, where each read the sentence off the NEXT:pastedsplit the line at#before reading it. That would cut a NEXT that had grown a tail back and keep every argv assertion green, so on POSIX it reads the whole line;assert_a_plain_note(line)held the note free of;, apostrophes and backticks because the note was pasted. It is nowassert_a_plain_next(row): the NEXT has no#, and the sentence is inwhy. All nine callers (eight tests andassert_hidden) call it;row["next"].split(" #", 1)[1]readrow["why"]: the commit the sentence names (the committed-break test and the two-commits test), the README andproject.yamlnotes (the two merge tests that leave a file to a person), and theDUtest's "only that side".No expectation of shape-doctor.py: a root's own
contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's about the command, the restore kind or the pin's state changed.Mutant. shape-doctor.py: a root's own
contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267'suncommittedtail put back onpin_restore(the command carrying# puts back the pin ...andwhy=None, asdecff87has it): 11 failed. They are the AST guard, the property case[pin_restore-uncommitted], and shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's owntest_a_root_pin_that_does_not_read_is_invalid_not_cannot_answer, all nine parameters (throughassert_a_plain_next). Reverted:shape-doctor.pyis back at the sha256 of the run below.Run on
9c8d8d5(clean tree; sha256 of every file under test is in the log,shape-doctor.pyis28f976e4...), the Linux run with zsh 5.9 and make present, so both pty pastes ran:tests/test_doctor_next_is_a_command.py tests/test_doctor_legs_members_next.py tests/test_doctor_validator_rows.py tests/test_shape_doctor.py tests/test_shape_advisory.py tests/test_family.py::test_the_doctor_reports_a_project_pinned_twice tests/test_cli_reference.py tests/test_agents_md_codes.py tests/test_validator_child_encoding.py: 574 passed, 3 skipped in 611.67 s (exit 0). That is 31 more than the 543 of the run on55723be: the 5 new cases, and the 26 tests shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 brought intotests/test_shape_doctor.py(268 collected there before, 294 now). The 3 skips are the same count as before. CI on this head is the gate for macOS and Windows.Converted sites
By an AST scan of non-docstring string constants:
check_legs' tail and addedlockstep_nextand themembersgit linepin_restorex4 andunreadable_pin_rowThe only strings left holding the text are
placement_plan_leg's YAML comment line andreport'splacement plan written to ... # nothing movedline, and neither is a NEXT.Phase 1 (17 sites, counted on 3ae0109):
validator_row, neither copy readable. Why: "from a complete checkout of the standard; ..."validator_row, the finding. Why: "read its output in full; it names what it refused"unloaded_manifest_row. Why: "the file to fix is , where the line above says; ...". Its neither-readable branch passes onvalidator_row'swhywith its NEXT.check_shape_currency, cannot answer (git -C <shape> fetch --all)check_shape_currency, the pin alone behind. The NEXT is theupdate-shape.py check. The why is "then apply --at ... --yes --branch ...", with the--trailer 'Lane: X'(update-shape.py apply --branch and family.py bump write commits with no Lane:/Co-Authored-By trailers; add a repeatable --trailer #111).check_shape_currency, copies moved: the same, with--accept-localcheck_legs(main's bootstrap line; A replaced it, see 18)missing_shape_file_fix, partly copyable. Why: "and the rest are RENDERED per project ..."missing_shape_file_fix, all rendered. The NEXT was prose with a command in backticks. It is nowpython3 <shape>/scaffold-project.py --help, with the prose as the why.placement_policy. Why: "from a COMPLETE checkout of the standard; this one is short "check_placement, the--placement-planline. Why: "writes the paths above as a plan ..."check_agent_files, pinned (git -C <root> checkout -- <file>)check_agent_files,AGENTS-shape.mdunpinned (update-shape.py check)check_agent_files, rendered (scaffold-project.py --help)members_verdict, the holder's own bootstrap. Why: "whatmake bootstrapruns in a holder: ..."check_machine. Why: "it names each missing prerequisite ..."check_the_way_in, the scaffold line. Why: "without --yes; it asks"Phase 2 (A's three and B's two; 18 replaces 7, so the tails go from 16 to 20):
check_legs, A's git line. Why: "checks each leg out at the commit this repository records for it, ..."members_verdict, A's plain git line for a holder that cannot run its bootstrap. Why: "checks each member out ..."lockstep_next, A's lockstep gate for thelegsandmembersrows. Why: "no submodule update cures a <leg|member> whose gitlink or pin is missing or disagrees, ..."validator_row, B's crash line. Why: "read its output in full, it shows where the validator stopped"unloaded_manifest_row, B's restore line (git -C <root> checkout HEAD -- scripts/repo_shape.py). Why: "it puts back the scripts/repo_shape.py HEAD holds, ..."Second landing round (#267's five, so 25 tails and 27 sites in all):
pin_restore, the merge (git -C <root> checkout --ours|--theirs -- <paths>). Why: "--theirs takes the other side. Take one side whole for every path named and never a hand-merged pin, ...", or "only that side of the merge holds a pin. Take it whole ...", with the files left to a person named whereproject.yamlorREADME.mdis conflicted toopin_restore, the uncommitted change (git -C <root> checkout HEAD -- contracts/shape-pin.yaml). Why: "puts back the pin as last committed and discards the uncommitted change that broke it, ..."pin_restore, the hidden copy (git -C <root> ls-files -v -- contracts/shape-pin.yaml). Why: "HEAD holds a pin that reads and git status shows no change to the copy here, ..."pin_restore, the commit that broke the pin (git -C <root> checkout <commit>~1 -- contracts/shape-pin.yaml). Why: "the pin as it was before , the commit that broke it, ..."unreadable_pin_row, no history to put a pin back from (update-shape.py check). Why: "no commit in this root holds a pin to put back, ..."Lane 3's cd1fc79 (#327, on main) added no tail. Its
manifest kindsNEXT is prose insidecheck_manifest_kinds, which is out of scope as ruled (lane 2's #291).Out of scope, as ruled:
check_manifest_kinds(FAMILY_BESIDE_FIX,UNREGISTERED_NEXT, and cd1fc79'sregistered_elsewhereline).Refusal.remediationprinted as a NEXT.report'splacement plan written to ... # nothing movedline. It is printed text, not a NEXT, and is allow-listed.Tests
New file
tests/test_doctor_next_is_a_command.py. It importstest_shape_doctor's fixtures with# noqa: F401, A's root builders fromtest_doctor_legs_members_nextand B's fromtest_doctor_validator_rows.Property: one red fixture per converted builder and branch: 29 cases, each of A's, B's and shape-doctor.py: a root's own
contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's lines included. For each one:shlex.split(next, comments=True) == shlex.split(next);#;;,&,|,(,)outside a quotation. These are read fromshlextokens with#NOT a comment, as zsh reads it, so a quoted path holding one passes. An unquoted$(reads as$and then the operator(;$(outside single quotes, so one inside double quotes, whichshstill runs, is caught too;whyopens with the words printed before shape-doctor.py: fifteen NEXT builders end the command with# <comment>, which a stock interactive zsh (macOS's default shell) does not read as a comment, so pasting the line hands the comment's words to the command as arguments, runs whatever follows a;, and runs themake bootstrapthelegsandmemberscomments put in backticks, then exits 127 #277.test_the_operator_readers_see_what_sh_runs_and_not_a_quoted_pathchecks both readers.zsh pty paste (skipped without zsh, and on Windows). The line is typed into
zsh -f -i, as a person pastes.detail.exitand printsFINDING manifest-topic.bootstraptarget touches a marker, leaves no marker.# <comment>, which a stock interactive zsh (macOS's default shell) does not read as a comment, so pasting the line hands the comment's words to the command as arguments, runs whatever follows a;, and runs themake bootstrapthelegsandmemberscomments put in backticks, then exits 127 #277. The manifest one exits 127 withcommand not found: it. For the legs, the control islegs_line_before_277, the linecheck_legsprinted at 3ae0109, and it creates the marker. A's ownwhyhas no backticks, so this PR's line with itswhyput back could no longer show that the harness runs a backtick.AST guard: no non-docstring string constant in
shape-doctor.pyholds a tail, meaning two or more blanks or a tab, then#and a blank.placement_plan_legis allowed.reportis allowed its onenothing movedstring and no other.report, and one in a nested def namedreportare each caught, and a docstring is not counted.Report:
WHYline sits under itsNEXT, per row.Why:on the line afterRemediation:.test_a_cannot_answer_row_with_no_command_still_says_whyholds the ruling above.Edited elsewhere:
tests/test_doctor_validator_rows.py(B):next == validator_command(...)andwhy, and expectsWhy:afterRemediation:;PYTHONPATH=expectations readenv PYTHONPATH=;command_ofis the whole NEXT.tests/test_doctor_legs_members_next.py(A):command_ofandwhy_ofreadnextandwhyalone. Its docstring said shape-doctor.py: fifteen NEXT builders end the command with# <comment>, which a stock interactive zsh (macOS's default shell) does not read as a comment, so pasting the line hands the comment's words to the command as arguments, runs whatever follows a;, and runs themake bootstrapthelegsandmemberscomments put in backticks, then exits 127 #277 would change that "one helper". A tail put back now fails them instead of being cut off.tests/test_shape_doctor.py: shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's tests readwhyand the whole NEXT (the second landing round, above), and the phase-1 edits (the key set gainswhy, tail-text assertions readwhy,env PYTHONPATH=). Then the review's N4:standard_aheadandstandard_of_another_historyare now helpers here, built from code its tests had inlined;advanced_standard, the four README copies and the exit-three test use them;behind_rowinstead of its own copies.Mutants (each run against the whole new file, then reverted):
# xtail put back oncheck_legs's NEXT: 3 failed (property[check_legs], property[lockstep_next-legs], the AST guard). On the phase-1 head the same mutant also failed 3, not 2 as this body first said: the zsh legs paste was the third (review N5). On the merged code, git refuses#as a pathspec without any of the misread phrases, so the paste passes and the second property case fails instead.whydropped fromRow.as_dict: 28 failed. Among them is the key-set testtest_every_row_of_a_compliant_project_is_ok_or_na, run beside the file.# read ittail put back onlockstep_next's command: 3 failed (property[lockstep_next-legs],[lockstep_next-members], the AST guard).check_machine: the guard fails, namingcheck_machine.contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267'suncommittedtail put back onpin_restore(second landing round): 11 failed, the AST guard,[pin_restore-uncommitted]and shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's ninetest_a_root_pin_that_does_not_read...parameters.# {why}printed on the NEXT line insidereport(the review's M2, which the phase-1 guard did not see): the guard fails, namingreport.Targeted run on
36321c4, on Linux with zsh 5.9 and make present, so both pty pastes ran:tests/test_doctor_next_is_a_command.py tests/test_doctor_legs_members_next.py tests/test_doctor_validator_rows.py tests/test_shape_doctor.py tests/test_shape_advisory.py tests/test_family.py::test_the_doctor_reports_a_project_pinned_twice tests/test_cli_reference.py tests/test_agents_md_codes.py;No
--helptext changed, sodocs/cli.mdis unchanged (tests/test_cli_reference.pyis in the run). AGENTS.md and README.md are not edited here because of their line budgets. Possible follow-ups:why.shape currencyrow carrying theLane:argument now describes itswhy.Cross-lane
--accept-localpaths, branch and lane trailer withshlex.quote, socheck'sapply,apply --branch'sgit -C <root> push && gh pr createandapply's owngit -C <root> checkout -b && git -C <root> commitpaste whole from a root undermy projects/,it's here/or$HOME/(#325) #374 (lane 3,fix/update-shape-next-quoting):test_the_doctors_shape_currency_row_keeps_the_root_wholedoesrow["next"].partition(" # then "). At its current head 0adc81b that is still line 503. The merge is textually clean. On main with B and this PR, 5 tests fail withassert [] == ['apply', ...]; on main with B alone, the same head passes all 5. It goes red whichever lands second, and the test has to readwhy. Lane 3 was told on update-shape.py: every NEXT line a run prints quotes its root, upstream,--accept-localpaths, branch and lane trailer withshlex.quote, socheck'sapply,apply --branch'sgit -C <root> push && gh pr createandapply's owngit -C <root> checkout -b && git -C <root> commitpaste whole from a root undermy projects/,it's here/or$HOME/(#325) #374.contracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 (lane 2): landed asdecff87; its five tails are converted here (the second landing round, above), and its tests read the sentence fromwhy.manifest kindsrow puts back a shape or leg pin undercontracts/that does not load with the git commandpin_restorepicks from its own state, shows a copy the shape pin has a row for or a neutral product pin with a read-onlygit diff, and leaves any other file to a person whatever it is called, so its NEXT no longer tells the reader to delete the pin (#291) #358 (lane 2,fix/doctor-manifest-kinds-unreadable-pin, nowce2ce75): carries shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267's pre-squash head, so it conflicts with main itself inshape-doctor.py(git_answer,pin_reads_at,pin_restore: 11 hunks), as this PR did with shape-doctor.py: a validator that crashed says it crashed instead of "reports a finding", and is CANNOT ANSWER when it is the standard's template, which a standard whose path holds thePYTHONPATHseparator no longer runs at all; a neutral product's digestvalidate-pins.pyskipped is anotenaming the product, from a child that can no longer reachghor a proxy; and a root whose manifest does not load and whosescripts/repo_shape.pyis gone is told to restore that file before its gate (#278, #297, #298) #357's. Against this head it conflicts in 14 hunks there (the same, plusunreadable_pin_row) and in 11 hunks oftests/test_shape_doctor.py, where it edits the lines this PR changed. Its own tails, which the guard flagged as 7 before shape-doctor.py: a root's owncontracts/shape-pin.yamlthat does not read as a pin (a merge conflict, a non-UTF-8 byte, acommit:that is not 40 hex, a deleted file) isINVALID, exit 1, with the git command that puts back the last pin a tool wrote as its NEXT, instead ofCANNOT ANSWER, exit 3, sending the reader togit fetch --allin the standard (#237) #267 landed, are for lane 2 to convert the same way (pin_restore's fifth,unreadable_next).executerecords inproject.yamlwhich leg the plan gave each submodule a leg keeps, and shape-doctor.py holds the submodule to that row, so one in the other leg is MISPLACED and a real adoption's placement readsok(#189) #394 (lane 1,fix/adopt-record-submodule-placement): one conflict with this head. Since main'scb7063f(repo_shape.py, validate-manifest.py: a bracket in a plain flow scalar is refused and a name holding a lone surrogate is a finding, sopins: [a, b]]is not the pinb]andname: "a\ud83db"no longer killsmake validate(#301, #280) #390) it also conflicts with main itself, intests/test_manifest_validator.py. Its two new functions sit aboveplacement_verdict, whose signature this PR changes to takeplan_why. The fix is mechanical: keep both functions and the 4-argument signature. Its body already passesplan_whyon both rows that offer the plan.read_pinrefusesupdate-pin-row, and the pinned validate-pins.py and validate-family.py reportshape-pin-row, when a shape pin'sfiles:is not a list, sofiles: 5,trueor1.5no longer endscheck,apply, shape-doctor.py and the project's own validator inTypeError: ... is not iterable(#290) #343, update-shape.py: a pinned copy,contracts/shape-pin.yamlor the manifest kept as a symlink, or under one, is refused asupdate-rewrite-symlinkbefore anything is written, andcheckcalls such a copycopy-is-link, soapplyno longer writes the standard's bytes at the link's end and reports a commit that does not hold them (#286) #347, templates:project.yaml, the leg pins andfamily.yamlquote their text placeholders, so--id 007,--tracking-branch 2026,--spec-path trueor--org nullread back as typed, andfamily.py addquotes a member id alike (#259) #395 and shape_advisory.py and shape-doctor.py: anoriginURL that is not UTF-8 names nothing, and themembersrow reads it asmake siblingsdoes, so a single repository is still advised, a leg clone is still sent to its root, andshape-doctor.py --rootno longer ends in a UnicodeDecodeError traceback (#385) #404 merge cleanly, and the guard stays green.Closes #277
🤖 Generated with Claude Code
Summary by Sourcery
Separate NEXT commands from their explanatory text so every command can be safely pasted into interactive shells.
New Features:
whyfield in JSON and as a dedicated WHY line in human-readable reports.Bug Fixes:
Enhancements:
env PYTHONPATH=... python3 ...for POSIX validator commands to support shells beyond sh, bash, and zsh.Tests:
whyvalues and command-only NEXT output.