Skip to content

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

Closed

Description

@brettheap

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 (:2438 at 3ae0109; the row at :2471-2474), when adopt.PathPolicy.load refuses the STANDARD's own contracts/path-classification.yaml;
  • run_checks (:4048; the row at :4058-4060), for any check that lets a Refusal out. 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
done

Output 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/a and the verdict is not moved). Its NEXT is about the project's legs: run git submodule update, run validate-pins.py, advance a leg pin. None of that touches the file that failed to load.
  • sh and bash stop 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 whatever scripts/validate-pins.py the 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 not ok "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:

  1. 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.
  2. run_checks.
  3. 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 the make bootstrap the legs and members comments put in backticks, then exits 127 #277's planned AST guard looks for # in shape-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.next is None, or it shlex.splits into a command with no backtick and no Remediation:.
  • run_checks with one CHECKS entry patched to raise Refusal("x", "y"), the default remediation, and another to raise one with a remediation of its own: neither row's next holds prose.
  • The doctor over the repro's root and standard: the placement row's NEXT pasted into zsh -f -c with a logging git first on PATH runs no git.

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

  1. brettheap commented on Oct 14, 2026

    @brettheap
    ContributorAuthor

    Fixed on main by 3121945 (#494). A Refusal built without a remedy now has an empty remediation (scripts/repo_shape.py:166-174), and PathPolicy.load and load_yaml pass none (path_classify.py:143-152), so the placement row's NEXT (exc.remediation or None, shape-doctor.py:4542-4546) is None. The run_checks path 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions