Repository navigation
scaffold-project.py writes --elected-by, --reference and --name between literal double quotes with no escaping and asks none of them whether a file can hold it, so a line break leaves a project.yaml its own validators refuse, a backslash is recorded as another character (CORP\nick as CORP, a line feed, ick), and a byte that is not UTF-8 dies in write_lf after the legs are pushed #282
Description
(lane openRepoShape-2, 2026-10-11T23:46:28Z)
Noted by the writer of PR #270 (which closes the adopt half) and reproduced by this lane on main c3f4987 (git 2.43.0, Python 3.12.3), with local bare remotes only and no network. Pre-existing. Not claimed.
What is wrong. scaffold-project.py takes --elected-by, --reference and --name as typed and puts each into templates/assembly-root/project.yaml between literal double quotes (name: "{{PROJECT_NAME}}", reference: "{{REFERENCE}}", elected_by: "{{ELECTED_BY}}"). Nothing escapes the value, and nothing asks whether a file can hold it. --dry-run took each input I tried (exit 0), because it returns before anything is written. The elector's fallback, GIT_AUTHOR_NAME or git config user.name, takes the same road. What the project then does with the file:
| value | scaffold | what lands | what then happens |
|---|---|---|---|
a line break: LF, CR LF, U+0085 or U+2028 in --elected-by, --reference or --name |
exit 0, all three repositories pushed | the quoted scalar runs over two lines (elected_by: "Ada / Lovelace") |
validate-manifest.py, validate-repository-naming.py and scripts/bootstrap.py exit 2 yaml-unparsable (bootstrap's remediation is about submodules); shape-doctor.py --root calls the new root "not a shape root" and its NEXT is adopt-project.py plan --source <the new root> |
a backslash: --elected-by 'CORP\nick' (also through GIT_AUTHOR_NAME), --reference 'C:\docs\notes\new.md' |
exit 0, pushed | the backslash is written as typed | the project's own reader decodes \n and \t as escapes: CORP\nick reads back as CORP, a line feed, ick, and the Windows path as C:\docs, a line feed, otes, a line feed, ew.md. validate-manifest.py says manifest ok; README.md says CORP\nick; PyYAML refuses the path (found unknown escape character 'd') |
a double quote: Joe "JJ" Smith |
exit 0, pushed | elected_by: "Joe "JJ" Smith" |
the project's reader takes the first and last quote and returns the text; PyYAML raises ParserError, so any other tool reading project.yaml refuses it |
a byte that is not UTF-8 ($'Jos\xe9', a lone surrogate once Python decodes argv) in --elected-by or --reference |
exit 1, a traceback: UnicodeEncodeError: 'utf-8' codec can't encode character '\udce9' in write_lf |
both legs pushed, the assembly root created and empty | the corrected re-run is refused scaffold-remote-exists on the empty root; with --reuse-empty-repo it is refused again on Atlas-spec ("the two legs must not exist yet ... Delete them") |
the same byte in --name |
exit 1, the same traceback | all three repositories created and empty, nothing pushed | the same two refusals |
| a control character: U+0001, ESC, DEL | exit 0, pushed | written raw into project.yaml |
PyYAML raises ReaderError: special characters are not allowed; the project's reader and validators pass it |
| a bidi control: U+202E, U+2066, U+2069 | exit 0, pushed | raw in project.yaml:46 and in README.md:9 |
nothing objects; it is the question #270's review raises for adopt (unwritable_reason accepts Cf) |
Not a defect, and so not in this issue: : , # and ' inside --elected-by (the quotes protect them, both readers read them back exactly), and a Persian name joined with U+200C. A NUL cannot reach the tool at all: argv refuses it (ValueError: embedded null byte). Git accepts every value that gets as far as a commit; none of these reaches a git command line except --name, in a commit subject.
SHAPE=<openRepoShape checkout>
export GIT_AUTHOR_NAME=T GIT_AUTHOR_EMAIL=t@example.invalid GIT_COMMITTER_NAME=T GIT_COMMITTER_EMAIL=t@example.invalid LC_ALL=C.UTF-8
scaffold() { # scaffold LABEL FLAG VALUE: one scaffold into local bare repositories, then the project's own gate
TMP=$(mktemp -d); label=$1; flag=$2; value=$3
who=(--elected-by 'T H'); [ "$flag" = --elected-by ] && who=()
python3 "$SHAPE/scaffold-project.py" --org testorg --project Atlas --elected-on 2026-09-02 "${who[@]}" \
"$flag" "$value" --local-remote-dir "$TMP/remotes" --work-dir "$TMP/work" > "$TMP/out" 2> "$TMP/err"
rc=$?
echo "== $label: scaffold exit $rc, commits pushed: $(for r in Atlas Atlas-spec Atlas-code; do printf '%s=%s ' $r "$(git -C "$TMP/remotes/$r.git" rev-list --count --all)"; done)"
[ $rc = 0 ] || { tail -1 "$TMP/err" | cut -c1-110; return; }
cd "$TMP/work/Atlas" || return
python3 scripts/validate-manifest.py > "$TMP/gate" 2>&1; echo " validate-manifest.py exit $?: $(head -1 "$TMP/gate" | sed "s#$TMP#<TMP>#g" | cut -c1-110)"
python3 - "$flag" <<'PY'
import sys; sys.path.insert(0, "scripts")
from repo_shape import parse_yaml
key = sys.argv[1].lstrip("-").replace("-", "_")
text = open("project.yaml", encoding="utf-8").read()
line = [l for l in text.splitlines() if l.startswith(key + ":")]
try:
value = parse_yaml(text)[key]
print(f" project.yaml holds {line!r}\n the project's reader gives {value!r}, with {value.count(chr(10))} line feed(s)")
except Exception as exc:
print(f" project.yaml holds {line!r}\n the project's reader raises {type(exc).__name__}: {exc}")
PY
python3 "$SHAPE/shape-doctor.py" --root . 2>&1 | grep -a -E "^kind|^NOT A SHAPE ROOT|^ *NEXT +python3 .*adopt" | sed "s#$TMP#<TMP>#g;s#$SHAPE#<SHAPE>#g" | cut -c1-130 | sed 's/^/ doctor: /'
cd - > /dev/null
}
scaffold 'a line break in --elected-by' --elected-by $'Ada\nLovelace'
scaffold 'DOMAIN\user in --elected-by' --elected-by 'CORP\nick'
scaffold 'a Windows path in --reference' --reference 'C:\docs\notes\new.md'
scaffold 'a byte that is not UTF-8 in --reference' --reference $'ref-\xe9'
scaffold 'a byte that is not UTF-8 in --name' --name $'Jos\xe9'Observed on main c3f4987 (<TMP> and <SHAPE> for paths):
== a line break in --elected-by: scaffold exit 0, commits pushed: Atlas=1 Atlas-spec=1 Atlas-code=1
validate-manifest.py exit 2: REFUSED yaml-unparsable: <TMP>/work/Atlas/project.yaml: line 47: expected `key: value`, got 'Lovelace"'
project.yaml holds ['elected_by: "Ada']
the project's reader raises YamlError: line 47: expected `key: value`, got 'Lovelace"'
doctor: kind not a shape root
doctor: NEXT python3 <SHAPE>/adopt-project.py plan --source <TMP>/work/Atlas --project Atlas
doctor: NOT A SHAPE ROOT (exit 2)
== DOMAIN\user in --elected-by: scaffold exit 0, commits pushed: Atlas=1 Atlas-spec=1 Atlas-code=1
validate-manifest.py exit 0: manifest ok: Atlas (atlas), 3 legs
project.yaml holds ['elected_by: "CORP\\nick"']
the project's reader gives 'CORP\nick', with 1 line feed(s)
== a Windows path in --reference: scaffold exit 0, commits pushed: Atlas=1 Atlas-spec=1 Atlas-code=1
validate-manifest.py exit 0: manifest ok: Atlas (atlas), 3 legs
project.yaml holds ['reference: "C:\\docs\\notes\\new.md"']
the project's reader gives 'C:\\docs\notes\new.md', with 2 line feed(s)
== a byte that is not UTF-8 in --reference: scaffold exit 1, commits pushed: Atlas=0 Atlas-spec=1 Atlas-code=1
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce9' in position 436: surrogates not allowed
== a byte that is not UTF-8 in --name: scaffold exit 1, commits pushed: Atlas=0 Atlas-spec=0 Atlas-code=0
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce9' in position 31: surrogates not allowed
Against GitHub the same phases run (_create_remotes makes all three repositories before _seed_legs renders anything), so the last two rows leave real repositories behind (read from code; not run, since a scaffold that is not a dry run must not reach GitHub from a review).
What should happen. Two changes, one per half of the gap.
- The three values are asked whether a file can hold them, before anything exists and under
--dry-runtoo: a byte that is not UTF-8, a line break and a control character are refused by name, in_identitybesideelection_date. PR adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270 adds the question asshape_materialize.unwritable_reason(aed9190) foradopt-project.py; the scaffold should ask the same one, so the two tools refuse the same values in the same words. It does not cover the next point: at aed9190 it returns None for",\and a bidi control. - The three quoted lines are filled with the value escaped for a double-quoted scalar, so a backslash and a quote read back exactly:
repo_shape.double_quotedis the one writer here that does it, and it is the one adopt's plan already uses. README.md and the twoAGENTS.mdlines are Markdown and keep the raw text. The templates already quote these three lines, which is the convention templates/assembly-root/project.yaml and the two leg pins write eight text placeholders bare (id,elected_on,visibility,tracking_branch, the two legpaths andsubmodule_paths), so a checked value YAML reads as another type (007,2026,true,null) is misread by the project or refused by the tool; quoting them reads every one back exactly and lets numeric branch names adopt #259 proposes for the other placeholders; what they lack is the escape.
Tests, each both through --dry-run and through a real scaffold into --local-remote-dir followed by the project's validate-manifest.py: a value holding \n, \t, " and C:\docs\new.md reads back through parse_yaml as the typed text; a line break, a lone surrogate and a control character are refused as one named refusal with no repository created; the same three through the GIT_AUTHOR_NAME fallback.
Where (main c3f4987):
scaffold-project.py:487-510(_elector: the value as typed, refused only when empty; theGIT_AUTHOR_NAMEandgit config user.namefallback at:495-503);:525(display = args.name or project) and:528(reference = args.reference or default_reference(...)) in_identity, which asks none of them anything;:1075,:1079,:1080put them into the substitution table as text.scaffold-project.py:1104(--dry-runreturns here),:1125_create_remotes,:1128_seed_legs(renderstemplates/spec-rootandcode-root, wherePROJECT_NAMEis inAGENTS.md:1),:1133_build_assembly_rootand:888materialize_assembly_root(whereELECTED_BYandREFERENCEare rendered): the order that leaves the repositories behind.templates/assembly-root/project.yaml:32,:42,:46(the three quoted lines) andtemplates/assembly-root/README.md:1,:9,:10.scripts/shape_materialize.py:1045(render, a plainstr.replace) and:70-84(write_lf, strict UTF-8).scripts/repo_shape.py:269(DOUBLE_QUOTED_ESCAPES),:337(_unescape, which decodes\nand\tin the values above),:365(_scalar, which tests only the first and last character for a quote) and:284(double_quoted, the escaping writer).
Related.
- adopt-project.py and scaffold-project.py substitute the template placeholders one after another, so a free-text value holding another placeholder's token is expanded (
--elected-by '{{SPEC_PATH}}'is recorded aselected_by: "spec") or, after the legs are pushed, refused astemplate-unsubstitutedwith "a defect in the substitution table of the tool you ran" #260: the same three values holding another placeholder's token ({{NOPE}}), refused after the legs are pushed. It touchesrender, so the fix for this one should land beside it. - adopt-project.py and scaffold-project.py accept values the new project's own validate-manifest.py refuses (a blank
--elected-byor--reference,--elected-on 2026-W41-6or20261011on Python 3.11+,--org test+org, a plan'svisibility: secret), soadoption verifiedor a scaffold exit 0 is followed by a failed first bootstrap; andplanadmits a 251-255-character--projectthatcheckrefuses #261: the same values when blank (--elected-by ' '), and--elected-onin forms the validator refuses. Not this issue's inputs. - adopt-project.py: since #247 a byte that is not UTF-8 in
--elected-by,--referenceor the--sourcedirectory passesplanandcheck, andexecutepushes both legs before it dies withUnicodeEncodeErrorinwrite_lf#255 and PR adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270: the adopt half of the writability question, for a byte that is not UTF-8 and a line break. They do not name the scaffold, and adopt-project.py: a valueexecutewrites into a file is asked whether a file can hold it, inplan, incheckand inexecutebefore any leg exists, so a byte that is not UTF-8 in--elected-by,--referenceor a local--sourcedirectory no longer pushes both legs beforewrite_lfraises (#255) #270 does not quote. - templates/assembly-root/project.yaml and the two leg pins write eight text placeholders bare (
id,elected_on,visibility,tracking_branch, the two legpaths andsubmodule_paths), so a checked value YAML reads as another type (007,2026,true,null) is misread by the project or refused by the tool; quoting them reads every one back exactly and lets numeric branch names adopt #259: quoting the other placeholders; these three are already quoted. adopt-project.pyrenders the same three template lines through the samerender(adopt-project.py and scaffold-project.py substitute the template placeholders one after another, so a free-text value holding another placeholder's token is expanded (--elected-by '{{SPEC_PATH}}'is recorded aselected_by: "spec") or, after the legs are pushed, refused astemplate-unsubstitutedwith "a defect in the substitution table of the tool you ran" #260's transcript shows itsexecutewritingelected_by: "spec"for--elected-by '{{SPEC_PATH}}'), so the escaping half applies to it as well; it was not run here.
Not claimed.
🤖 Generated with Claude Code
Activity
brettheap commentedon Oct 12, 2026
CLAIMED — lane openRepoShape-5, session e62c2fd7-176d-429d-89fb-0653dd08ad5f@Eagle, 2026-10-12T11:02:53Z, for #282
Logged in
lanes/log/openRepoShape-5.mdatf26c632829a0c1e9f7ad5085971bf26ecf2b0741.The three reads (lane-collision-protocol Rule 1):
scaffold-project.py: `--elected-by`, `--reference`, `--name` and the …
tests/test_scaffold_manifest_values.py: an `--elected-by` of a TAB, `…
Merge origin/main 92e29d9 into fix/adopt-root-kept-symlinks: #190's r…
scaffold-project.py and adopt-project.py ask the project's YAML reade…
repo_shape.py, validate-manifest.py: a bracket in a plain flow scalar…
bump-leg.py, scaffold-project.py and family.py: `git status --porcela…
family.py and scaffold-project.py: `add --local-remote-dir` refuses a…