Repository navigation
shape-doctor.py puts a Refusal's remediation, a paragraph of prose that opens Remediation:, in a row's NEXT (placement_policy and run_checks), so a standard whose own contracts/path-classification.yaml does not load hands the project a NEXT about its legs and leg pins, which sh and bash reject as a syntax error and zsh, macOS's default shell, runs, executing git submodule update --init --recursive and python3 scripts/validate-pins.py in the reader's directory #408
Description
Lane: openreposhape-4 (openRepoShape-4)
Found by this lane's planner for the doctor cluster (#276, #277, #278, #297, #298), section 4, item 2: "Refusal.remediation printed as a NEXT: run_checks ..., placement_policy .... Prose in the command slot." The plan kept it out of #277's scope ("NOT Refusal.remediation used as a NEXT"). The #277 writer's brief in this lane says the same, so the fix in progress for #277 leaves it. #370 is about the TEXT of the default remediation in the family tools, not about the doctor putting a remediation where a command goes. Re-verified on main 3ae0109 with local bare remotes, an empty global git config, GIT_ALLOW_PROTOCOL=file and a gh that exits 97 (only the machine row calls it).
What is wrong. Two places in shape-doctor.py build a row whose next is exc.remediation or None:
placement_policy(:2438at 3ae0109; the row at:2471-2474), whenadopt.PathPolicy.loadrefuses the STANDARD's owncontracts/path-classification.yaml;run_checks(:4048; the row at:4058-4060), for any check that lets aRefusalout. That row is a FINDING whose NEXT is the refusal's remediation.
A remediation is prose for a human, printed after a refusal's line, and it opens with Remediation:. A Refusal built without one carries repo_shape.REMEDIATION, which is the assembly root's lockstep paragraph (#370). Neither is a command:
U=<openRepoShape checkout> # a clean checkout at 3ae0109
# Runs in bash on Linux (GNU coreutils). Nothing leaves the machine: every repository is a local bare one under $R, git
# reads an empty global config, GIT_ALLOW_PROTOCOL=file makes any accidental https/ssh clone fail, and a `gh` that exits
# 97 stands first on PATH.
R=$(mktemp -d); mkdir $R/bin; printf '#!/bin/sh\nexit 97\n' > $R/bin/gh; chmod +x $R/bin/gh; touch $R/gitconfig
export PATH=$R/bin:$PATH GIT_CONFIG_GLOBAL=$R/gitconfig GIT_CONFIG_NOSYSTEM=1 GIT_ALLOW_PROTOCOL=file \
GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t.invalid GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t.invalid
python3 $U/scaffold-project.py --org testorg --project Atlas --elected-by 'Test Human' --elected-on 2026-10-12 \
--local-remote-dir $R/remotes --work-dir $R/work >/dev/null
git clone -q --recurse-submodules $R/remotes/Atlas.git $R/Atlas
# a checkout of the standard whose own path policy does not load (a bad local edit, a conflicted pull)
git clone -q $U $R/std
printf 'kind: path-classification\nrules: [\n' > $R/std/contracts/path-classification.yaml
python3 $R/std/shape-doctor.py --root $R/Atlas --json > $R/d.json; echo "doctor: exit $?"
python3 - $R/d.json <<'PY'
import json, sys
row = [r for r in json.load(open(sys.argv[1]))["rows"] if r["id"] == "placement"][0]
print(row["status"], "|", row["reason"])
print("NEXT:", row["next"])
open(sys.argv[1] + ".next", "w").write(row["next"])
PY
# paste that NEXT, from inside the project, into three shells, with a `git` and a `python3` that only log
mkdir $R/stub; for t in git python3; do printf '#!/bin/sh\necho " ran: %s $*" >> %s/ran.log\n' $t $R > $R/stub/$t; chmod +x $R/stub/$t; done
cd $R/Atlas
for SH in sh bash zsh; do
: > $R/ran.log; PATH=$R/stub:$PATH $SH -c "$(cat $R/d.json.next)" >/dev/null 2>&1; echo "$SH: exit $?"; cat $R/ran.log
doneOutput at 3ae0109:
doctor: exit 0
n/a | the standard's own path policy could not be read: $R/std/contracts/path-classification.yaml: not a flow collection: '['
NEXT: Remediation: run `git submodule update --init --recursive`, then `python3 scripts/validate-pins.py`. If the PIN itself is stale, advance the gitlink, `contracts/<leg>-pin.yaml` and every workflow `@<sha>` that names the leg in ONE commit (see README, 'The lockstep invariant').
sh: exit 2
bash: exit 2
zsh: exit 1
ran: git submodule update --init --recursive
ran: python3 scripts/validate-pins.py
- What the row is about: the standard is short (the row is
n/aand the verdict is not moved). Its NEXT is about the project's legs: rungit submodule update, runvalidate-pins.py, advance a leg pin. None of that touches the file that failed to load. shandbashstop at a syntax error (the parenthesis), so nothing runs.- zsh, the default shell on macOS, runs both backticked commands before it gives up. In the reader's own project the first is a real
git submodule update --init --recursive, which moves every leg to its gitlink and leaves a commit made on a leg's detached HEAD in its reflog alone. The second runs whateverscripts/validate-pins.pythe current directory holds. Pasting the NEXT is what AGENTS.md tells an agent to do: "A FINDING names its fix, and you run THAT", and the README says every row that is notok"names the exact command that fixes it" (README.md:1135).
run_checks' clause was not reached from a repository. Ten damaged roots were tried, and each check caught its own refusal: a leg pin that is not UTF-8 or is a list, a workflow, AGENTS-shape.md or the naming copy not UTF-8, a committed .gitmodules git cannot parse, a holder whose family.yaml is a list, whose .gitmodules git cannot parse, or whose member pin is short. The clause is the doctor's net for a check that does not catch its refusal. It is shown here by reading, and a test can reach it by patching a check.
Why. Row.next is the slot for the exact next command, the one --json callers act on and AGENTS.md tells an agent to run. Refusal.remediation was written for a refusal's own stderr. Both places reused it because it was the only "what to do" text they had to hand.
What it should do. No row's NEXT is prose, and none holds a backtick, whatever reaches it. The decisions are the author's:
placement_policy.- NEXT
None, since the reason already names the file and the loader's words. - Or a command that fits a short standard. The branch just above it, for a policy file that is MISSING, prints
shape-doctor.py --root <root>"from a COMPLETE checkout of the standard". - Or the restore of the standard's own file (
git -C <standard> checkout -- contracts/path-classification.yaml), if the doctor should ever name a write in the standard.
- NEXT
run_checks.- NEXT
None. The remediation, if there is one, goes in the reason, or inwhyonce 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 lands. - Also: whether a refusal that escapes a check is the doctor failing to ask (
detail.environment, CANNOT ANSWER, exit 3) rather than a FINDING about the repository. TheOSErrorclause beside it makes the FINDING choice today. With no git on PATH, thelegsrow readsFINDING could not be run: [Errno 2] No such file or directory: 'git'and the verdictDRIFTED (1 leg(s) not at the pin), a statement about the project that comes from this machine (main 3ae0109, run beside The assembly root'sscripts/validate-pins.pyends in aFileNotFoundErrortraceback, exit 1, a finding's code, when git is not on PATH, becauserecorded_gitlinkreachesrepo_shape.git_outand nothing catchesOSError, where the file's own exit codes give 1 to drift and 2 to a question that could not be asked: the sibling of #368, whose fix is ruled intovalidate-family.pyonly #412).
- NEXT
- Holding it. 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's planned AST guard looks for#inshape-doctor.py's own string constants. A remediation arrives at run time from another module and holds no#, so the guard would not see this. A property over the rows the suite builds ("Remediation:" not in next, no backtick) would.
How to test.
placement_policy, in process, with a policy file that does not load:row.nextis None, or itshlex.splits into a command with no backtick and noRemediation:.run_checkswith oneCHECKSentry patched to raiseRefusal("x", "y"), the default remediation, and another to raise one with a remediation of its own: neither row'snextholds prose.- The doctor over the repro's root and standard: the
placementrow's NEXT pasted intozsh -f -cwith a logginggitfirst on PATH runs nogit.
Verified at main 3ae0109 (shape-doctor.py sha256 f1731ff4…).
Related: #370 (the default remediation's text, in the family tools), #277 (a NEXT is only a command; in progress in this lane, scope excludes this), #291 / lane 2's PR #358 (the manifest kinds row's prose NEXTs; FAMILY_BESIDE_FIX's backticks run family.py init and family.py add in zsh the same way), #410 (another row the doctor fills from what a child printed rather than from what it means).
Activity
brettheap commentedon Oct 14, 2026
Fixed on main by 3121945 (#494). A
Refusalbuilt without a remedy now has an empty remediation (scripts/repo_shape.py:166-174), andPathPolicy.loadandload_yamlpass none (path_classify.py:143-152), so the placement row's NEXT (exc.remediation or None, shape-doctor.py:4542-4546) is None. Therun_checkspath is unchanged but has no known reach; the guard test in #548 covers it. Found in the v1.1.0 triage of #460, 2026-10-14.docs/release-notes/v1.0.0.md: brought up to main 3121945, so section …
docs/release-notes/v1.0.0.md: brought up to main 5ed3e5d, so section …
shape-doctor.py: `manifest kinds` prints one command for its NEXT, th…