You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Give the test suite explicit code, assembly, spec and dependency roots, so it runs on today's single repository and in the adopted triad, and refuses composed acceptance without all of them - #190
Part of #186.
T009/T010 of specs/004-migrate-to-triad/tasks.md (branch 004-migrate-to-triad, PR #187).
Head: b50f197 (4 commits on c4864ac; 8 files, all under tests/). The fourth commit, b50f197, fixes lane 3's finding (1); the reviewer's LAND on it is review 5471080687.
What changes
When the repository is adopted into the triad, the files split across three repositories:
README.md, AGENTS.md and CLAUDE.md move to the assembly root.
docs/ moves to the spec leg.
Every command, tests/, .gitmodules and contracts/openreposhape-pin.yaml stay in the code leg.
The dependencyupstream/openRepoShape stays nested in the code leg.
The suite assumed one tree. It now names four:
Root
Variable
Default (no variable)
code
OPENREPOTOOLS_CODE_ROOT
the tree the suite was loaded from
assembly
OPENREPOTOOLS_ASSEMBLY_ROOT
the nearest directory above the code root that carries contracts/code-pin.yaml and project.yaml, is a repository's top level, and records the code root as the gitlink its code pin names; absent on today's layout
spec
OPENREPOTOOLS_SPEC_ROOT
the assembly's spec leg (contracts/spec-pin.yaml's submodule_path) when it is checked out; absent otherwise
dependency
—
<code root>/upstream/openRepoShape, initialized or not
OPENREPOTOOLS_COMPOSED=1 is the composed switch. In either mode, the run is refused when a variable names the wrong kind of tree, or when the mode is anything other than 0 or 1.
Which root a file is read from (ROOTS.path_for(<rel>))
README.md, AGENTS.md and CLAUDE.md come from the assembly.
docs/…, openspec/…, specs/… and ideation/… come from the spec root.
Everything else comes from the code root.
Today's single tree answers for all three, because it carries openspec/ and specs/ beside the code. On today's layout, every reader reads exactly the file it always read.
Decision: .gitattributes stays in the code leg
The suite reads .gitattributes from the CODE root. Git never applies a superproject's .gitattributes inside a submodule. I measured this in the composed candidate:
README.md at the assembly is text: auto / eol: lf.
code/park and code/tests/run.sh are unspecified.
So the line-ending rule protects the bash files only through the code leg's own copy. Reading the assembly's copy would pass while the code leg went unprotected.
This matches T006: its leg patch code-0001 gives the code leg its own .gitattributes, and the assembly keeps its copy for its own files. Until code-0001 lands in the real leg, the leg's tests job is red on this one test, by design.
Follow-up for the plan's owner: the placement table in specs/004-migrate-to-triad/plan.md (§ Target placement) on main still puts .gitattributes at the assembly only. It needs amending to say the code leg keeps its own copy before the real split. A leg split exactly as the table says is red on this test; the independent review saw this in its C5 and C6 runs.
Length caps at an assembly root (T006)
File
Cap on today's layout
Cap when an assembly root is named
AGENTS.md
316 (unchanged)
340
README.md
486 (unchanged)
510
The higher caps allow exactly the 24 lines each that T006's assembly-0001 adds. Each cap has a dated entry. The entries cite specs/004-migrate-to-triad/root-guidance-2026-10-08.md, which is on #192's branch. Land #192 with or before this PR, or that citation points nowhere until it does.
Readers rewired
tests/test_repo_hygiene.py:
The 16 document tests read through doc(), and their per-surface loops visit the code's surfaces first (code_first()).
The two whole-tree scans use git ls-files over every repository the run can read. Today that is the code root; composed, it is code, assembly and spec.
tests/test_upstream_pin.py reads the code root's own git ls-tree HEAD, with the dependency nested in the code root. A new check confirms the code root is its repository's top level.
tests/test_openrepotools_command.py: the installer's receipt-mode README check.
tests/test_lane_helpers.sh: the two sections that quote docs/README-lanes.md used to swallow a missing file with || :.
The runner now passes the spec root's manual.
An empty value (no spec root) skips them, and the skip names the reason.
Fixed in b50f197: at dbc6837, the --retire section's two manual assertions (:6601-6602) used $ln_doc outside the guard that binds it. With no spec root they aborted the suite under set -u (ln_doc: unbound variable); this was lane 3's finding (1). They now carry the same guard and named skip. A new text test, test_the_lane_suite_reads_the_manual_only_inside_its_guard, holds every use inside a guard.
The reviewer's LAND review on b50f197 (5471080687) ran three proofs:
The new test against dbc6837's suite: 1 failed, naming lines 6601 and 6602. At b50f197: green.
An extract of the suite's own lines with no spec root, under bash 5.2.21 and 3.2.57: it aborts at dbc6837 and ends with three named skips at b50f197.
With the manual present, the output is byte-identical at both commits.
My own run of the new test against dbc6837 agrees: 1 failed.
A value naming no readable file FAILS, and the failure names the path.
tests/run.sh makes a relative root absolute before its cd. Nothing else in it changed.
The composed-vs-standalone contract
Standalone is the default, and the only mode today's layout has.
A check whose root is absent skips, and the skip names the root. This is the courtesy the missing submodule already gets.
The no-submodule skip is today's skip, with the same condition and reason.
On today's layout nothing new skips: the scoped run below is 191 passed, 0 skipped.
Composed is OPENREPOTOOLS_COMPOSED=1. tests/conftest.pyrefuses the session (pytest exit 4, ERROR: …), naming each problem, when:
the assembly or spec root is absent;
the dependency is not initialized;
the code or spec leg is not at the commit the assembly records as its gitlink, has tracked changes, or answers no git status.
In composed mode, NEEDS_UPSTREAM becomes a fixture that fails, and a document test that reaches an absent root fails. Nothing skips its way into composed acceptance (FR-008).
What the conftest does NOT check: the pin files' commit: and tree_sha256. Those belong to the standard's scripts/validate-pins.py. The T010 assembly job runs it in the step before the composed suite (Copilot round 2; see Follow-ups).
Evidence
Step 1: what the split breaks, before this change
I ran the suite at the rehearsed run C code leg 73b04d6, with tests/run.sh -k 'repo_hygiene or upstream_pin or wip_init or park_resume' -rfEs. Run C is main 837928a split by the standard's adopter into disposable local remotes (spec 593250d, assembly 69b8924).
Context
Result
M1: mounted in the recursive assembly clone
17 failed, 350 passed
M2: the code leg cloned alone, dependency initialized
17 failed, 350 passed
M3: the code leg cloned alone, dependency not initialized
17 failed, 141 passed, 209 skipped
The same 17 tests failed in all three runs, each with a FileNotFoundError:
nine on README.md;
one on CLAUDE.md;
three on AGENTS.md;
three on docs/README-lanes.md (the hygiene document tests);
one on .gitattributes (test_the_root_carries_the_line_ending_rule).
test_upstream_pin.py passed in all three, because it already read the code leg's own gitlink. M3's skips were today's dependency skips. Outside those selectors, the installer's README check and the lane suite's two docs/README-lanes.md sections read moved files too. The lane suite had swallowed the missing file with || :. This PR covers all of them.
Local proof in the triad
I applied the PR head onto run C's code leg and pinned it into the assembly with the standard's bump-leg.py. The validators, make validate and make pins all exited 0.
I ran -k 'repo_hygiene or upstream_pin or roots or composed or openrepotools_command or lane_helpers_suite' four ways:
Run
Result
Composed, as the assembly job runs it
285 passed, 2 failed, 0 skipped
Composed, with the spec leg not checked out
refused, exit 4
Code leg standalone
268 passed, 2 failed, 17 skipped (each naming its root)
Code leg without its dependency
159 passed, 1 failed, 28 skipped
The failures are recorded here, not deleted:
The code leg has no .gitattributes. This is the decision above.
Two lane-suite stamp assertions fail in the composed run and the standalone run, because their expected directory falls past the 240-character row cut under run.sh's long run-root path. CI passes them, and this PR does not touch them.
In the standalone run, the lane suite also aborted at ln_doc: unbound variable. That is the defect above. I first attributed this failure to the stamp assertions alone; lane 3's review caught it.
These proofs ran on c139e88's content and were not repeated on e3a76b5 or dbc6837. The independent review's fake triad (C1–C6) exercised dbc6837 composed and standalone. The full record is specs/004-migrate-to-triad/test-roots-and-ci-2026-10-08.md on 004-migrate-to-triad-ci.
Red at base: 21 of 21
All 21 test cases in tests/test_test_roots.py fail at origin/mainc4864ac.
Round 1. I copied the module as of c139e88 (sha256 be4edff7…) unchanged into a clone of c4864ac. tests/run.sh -k roots gave 18 failed: every test in the module. The 3 passes are tests in other modules whose names contain roots.
Seven of those failures are behavioural, and each fails on what it asserts:
The nested hygiene run passed against an assembly README with no install line, and against a spec manual without the fork act, because it read this checkout's files.
A standalone code leg's README test passed instead of skipping.
The lockstep check passed on a code root whose gitlink is 1111….
The dependency checks passed with no dependency, instead of today's skip.
A composed triad with its spec leg moved off its pin ran (exit 0, not 4).
The wrapper passed asm through as a relative path.
The other 11 are resolver tests. They fail because conftest.resolve_roots and ROOTS do not exist at the base.
Round 2. I copied dbc6837's module (sha256 aeb4b742…) the same way and ran -k 'length_caps or missing_manual or status_cannot_be_read'. That gave 3 failed: the three tests added since round 1. The independent review copied the same module whole into c4864ac and got 21 failed.
How the module works: it runs the suite itself, nested, against fixture triads built under tmp_path. Each fixture has:
a fake assembly with the rehearsed contracts/code-pin.yaml grammar, pins recomputed from the fixture trees, and project.yaml;
a code leg copied from this checkout;
a spec leg.
It never touches a real repository.
Green
tests/run.sh -k 'repo_hygiene or upstream_pin or roots or composed' gave 191 passed, 0 skipped at dbc6837 and on its merge onto main 63810dd. That was the independent review's run. My own earlier runs gave 188 at c139e88 and 189 at e3a76b5.
CI on dbc6837 (run 37869489919, merge 5ad6205):
Job
This PR
Main 63810dd
tests
1318 passed
1296
tests-no-submodule
1105 passed, 213 skipped
1084 passed, 212 skipped
tests-windows
176 passed, 1142 skipped
—
guard-launch-mode
165 passed
—
parse-macos
green
—
The one extra tests-no-submodule skip is the composed positive control, which skips with today's reason. tests-macos is running under ready (run 37873415160).
How the local runs were made
The workstation-wide pgrep census in tests/run.sh did not reach zero on Eagle for some runs. Under the coordinator's interim rule, any run that had not started within 5 minutes was replaced by run.sh's exact pytest line under flock on the same lock file. The logs say which runs those were, and the deviation is noted on #184. CI is the record.
Reviews
Copilot round 1: two findings, both fixed in e3a76b5, and both threads resolved.
A git status that fails is now a refusal.
The fixture legs keep .gitattributes.
Independent adversarial review: LAND on dbc6837.
Lane 3 (reviewer of record): LAND on dbc6837, with three things to fix before the squash.
(1) The $ln_doc abort, confirmed above, with the fix ready.
(2) The paired layout <assembly>/worktrees/<feature>/code finds no assembly.
(3) The root variables redirect the suite silently, because the report header is hidden by -q.
Copilot round 2 on dbc6837 raised two findings. Both are recorded as dated follow-ups, not pushed here, because the macOS gate is running.
(High) The conftest never reads the pin files' commit:. Reproduced: with contracts/code-pin.yaml's commit: moved off the gitlink, ROOTS.refusal() returns None, while validate-pins.py exits 1 with pin-gitlink-mismatch. That validator runs before the suite in the T010 job. Answered in the thread.
(Medium, "previously missed")_carries accepts a directory where a marker file is expected.
Follow-ups (dated 2026-10-09, recorded on #186; not in this PR)
Review findings 11 and 14 have commits on branch feat/test-roots-followups (6e63083, 2064390, on dbc6837). That branch is not part of this PR.
Review finding 11 — three code-only assertions sit after a document read. At test_repo_hygiene.py:994, :1052 and :1264, a code-only assertion follows the first doc() read, so a standalone code leg skips them together with the documents.
6e63083 asserts each mixed test's command half first. That covers park's header, status's --no-optional-locks, the installer's lanes [--all] [--fetch], the exit-3 table and the wrapper's lock claims.
2064390 proves it with a nested run against a fixture leg whose commands lost those lines.
Review finding 13 — the missing-manual failure is checked only by its text. The bad branch was exercised locally:
With the block extracted unchanged and stub ok/bad, LANE_MANUAL=/nonexistent/README-lanes.md prints FAIL the lane manual is where the run's roots put it, and an empty value skips.
Still to do: a behaviour test that runs the block itself.
Review finding 14 — the four variables are not in AGENTS.md.6e63083 adds seven lines to "Testing your changes" naming the roots, the three variables and the composed switch. Its AGENTS cap moves 316 → 323 (340 → 347 at an assembly root), with a dated entry.
Copilot round 2:
The conftest should refuse a pin whose commit: differs from the gitlink.
Root markers should be regular files.
Review finding 10: the plan owner's placement-table amendment (above).
Lane 3's review:
(1) feat/test-roots-manual-guard @ b50f197, unless it goes into this PR first.
(2) Discovery for the paired worktree layout, before Gate C.
(3) Say which roots a run used where -q cannot hide it.
Review finding 15 (nits):
(a) Roots.owner_of maps LICENSE and .gitignore to the code root. This is latent, because no test reads either.
(b) abs_root turns a quoted ~/x into $PWD/~/x.
(c) The GIT_ENV comment says "file transport allowed".
Not in this PR
The CI patches of T010 apply to repositories that do not exist yet:
the code leg's tests.yml;
the assembly's exact-pin job, as its own workflow (composed.yml), because validate.yml is a pinned shape copy.
They are on 004-migrate-to-triad-ci, with their local proofs. .github/workflows/tests.yml on main is unchanged.
Make the test suite operate safely across the current single repository and the adopted triad by resolving ownership explicitly and refusing incomplete composed runs.
New Features:
Add explicit code, assembly, spec, and dependency root resolution with environment-variable overrides and automatic triad discovery.
Add standalone and composed test modes, including composed-session refusal when required roots, pins, repository state, or dependencies are invalid.
Bug Fixes:
Ensure tests read documents, manuals, commands, pins, and dependency data from the repository leg that owns them instead of assuming a single tree.
Prevent missing lane manuals from being silently ignored and preserve informative standalone skips.
Enhancements:
Extend repository hygiene and upstream-pin checks across all repositories visible to a composed run.
Make the test wrapper normalize relative root paths before changing directories.
Add coverage for single-repository, standalone-leg, mounted-triad, invalid-root, pin-mismatch, and composed acceptance behavior.
Tests:
Add comprehensive root-resolution and composed-mode tests, including nested fixture-repository runs and wrapper environment propagation checks.
…s so it reads each file from the tree that owns it, and refuses composed acceptance without all of them.
Checkpoint of T009 (specs/004-migrate-to-triad/tasks.md, #186). Adopted into
the triad, README.md, AGENTS.md and CLAUDE.md move to the assembly root and
docs/ to the spec leg, while the commands, tests, .gitmodules and the
openRepoShape pin stay in the code leg. The suite assumed one tree.
tests/conftest.py now resolves the four roots from OPENREPOTOOLS_CODE_ROOT,
OPENREPOTOOLS_ASSEMBLY_ROOT and OPENREPOTOOLS_SPEC_ROOT, or from the layout
(an assembly above the code root that mounts it as its code gitlink), and
ROOTS.path_for() reads a file from the root that owns it. Standalone, an
absent root skips the tests that need it, naming the root; on today's single
tree nothing new skips, because that tree is all of them, and the
no-submodule skip is exactly today's. With OPENREPOTOOLS_COMPOSED=1 the
session is refused (pytest exit 4) when the assembly or spec root is absent,
the dependency is uninitialized, or a leg is not at the commit the assembly
records, and NEEDS_UPSTREAM fails instead of skipping.
The hygiene module reads the front-door documents and the lane manual through
those roots and scans every repository the run can read; the pin module reads
the code root's own gitlink and the dependency nested in it; the lane suite
and the installer's README check take the manual and the README from their
owning roots; tests/run.sh makes a relative root absolute before its cd.
tests/test_test_roots.py holds the resolver on fixture layouts and runs the
suite itself, nested, against fixture triads. Not yet run through the
workstation lock; the scoped runs and the CI patches follow.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The suite now treats code, assembly, spec, and nested dependency locations as explicit runtime roots, resolving them from environment variables or repository layout, routing every reader accordingly, and enforcing a strict composed-triad contract while retaining compatible named skips for standalone clones. New fixture-based tests exercise root discovery, path ownership, pin integrity, dependency states, wrapper propagation, and refusal behavior.
File-Level Changes
Change
Details
Files
Introduces centralized four-root discovery and validation with standalone and composed execution modes.
Adds environment-variable overrides and default discovery for code, assembly, and spec roots.
Models the dependency as nested under the code root and validates its initialization.
Validates marker files, repository boundaries, assembly gitlinks, pinned leg commits, and tracked cleanliness.
Refuses invalid configuration or incomplete composed sessions before pytest collection, while preserving named standalone skips.
Trigger a new review: Comment @sourcery-ai review on the pull request.
Continue discussions: Reply directly to Sourcery's review comments.
Generate a GitHub issue from a review comment: Ask Sourcery to create an
issue from a review comment by replying to it. You can also reply to a
review comment with @sourcery-ai issue to create an issue from it.
Generate a pull request title: Write @sourcery-ai anywhere in the pull
request title to generate a title at any time. You can also comment @sourcery-ai title on the pull request to (re-)generate the title at any time.
Generate a pull request summary: Write @sourcery-ai summary anywhere in
the pull request body to generate a PR summary at any time exactly where you
want it. You can also comment @sourcery-ai summary on the pull request to
(re-)generate the summary at any time.
Generate reviewer's guide: Comment @sourcery-ai guide on the pull
request to (re-)generate the reviewer's guide at any time.
Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
pull request to resolve all Sourcery comments. Useful if you've already
addressed all the comments and don't want to see them anymore.
Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
request to dismiss all existing Sourcery reviews. Especially useful if you
want to start fresh with a new review - don't forget to comment @sourcery-ai review to trigger a new review!
…and composed contract PR #190 answers it with, and the local proofs of both T010 workflow patches.
Measured on the rehearsed run C code leg 73b04d6 in three contexts (mounted
in the assembly, standalone, standalone without the dependency): the same 17
hygiene tests fail on files the split moves out of the code leg, and nothing
else in the measured selectors. PR #190 is red at origin/main in 18 tests and
188 passed with nothing skipped on today's layout; CI green per push. In a
composed candidate pinned with bump-leg.py, the validators pass, the composed
job refuses a missing spec leg (exit 1 at its context step, exit 4 from the
suite), and the composed and standalone leg runs fail only on the code leg's
missing .gitattributes (a T006 finding) and two lane-suite stamp assertions
the long run-root path pushes past the row cut. Hosted runs wait for real
legs (Gate C). Part of #186.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d keep the code leg's own .gitattributes in the fixture legs (Copilot round 1 on #190).
A failed `git status` came back as None and was read as "no changes", so a
leg whose index could not be read passed the tracked-changes guarantee. It
is now a refusal naming the leg; a new test corrupts a fixture leg's index
and asserts it. The fixture code legs dropped .gitattributes with the other
assembly files, contradicting the contract that the code leg carries its own
line-ending rule; they keep it now.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Behaviour change pushed (e3a76b5, Copilot round 1): in a composed run, a leg whose git status fails is now refused instead of read as clean; the fixture code legs keep .gitattributes. Nothing changes on today's layout. Scoped run -k 'repo_hygiene or upstream_pin or roots or composed': 189 passed, 0 skipped (run.sh's pytest line under the lock, by the census rule). A second commit follows with two items from the T006 record (§10): the assembly README/AGENTS length caps and an explicit failure for a missing lane manual.
…d fail the lane suite by name when the lane manual is missing (T006's record of 2026-10-08, section 10).
In the triad the hygiene caps read the ASSEMBLY's files, which T006's root
guidance (assembly-0001) lengthens by 24 lines each: the shape's first-line
pointer and triad map in AGENTS.md, the lockstep-invariant section the leg
pins name and the three-repository front door in README.md. The caps are 340
and 510 where an assembly root is named or found, in dated entries; today's
single repository keeps 316 and 486, so main buys no line of prose. The lane
suite's two manual sections now fail naming the path when the manual the
roots point at is unreadable, instead of handing the assertions an empty
string. Three new tests; all three are red at origin/main (c4864ac). The
scoped green run was still queued behind the workstation lock at this
checkpoint; CI is the record.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second behaviour commit pushed (dbc6837), from the T006 record (root-guidance-2026-10-08.md §10): where an assembly root is named or found, the README/AGENTS.md length caps are 510/340 (today's 486/316 plus the 24 lines each that assembly-0001 adds, in dated entries); today's single repository keeps 486/316, so nothing changes on today's layout. The lane suite's two manual sections now FAIL naming the path when the manual the roots point at is unreadable, instead of reading an empty string. Three new tests, all three red at origin/main c4864ac (3 failed). The local scoped green run was still queued behind the workstation lock at this checkpoint; CI on dbc6837 is the record.
The reason will be displayed to describe this comment to others. Learn more.
Independent adversarial review — lane openRepoTools-3 (read-only; 2026-10-09T02:10Z)
Verdict: LAND on dbc6837: all CI is green (tests-macos waits on ready). Today's single-repository layout is unchanged: the same 1296 test ids, the same pass/skip outcomes and skip reason, no file outside tests/, and the same length caps. One defect sits in the new triad path (finding 10: the shell suite aborts in a standalone code leg). It does nothing here; it is a two-line move that is best made before the squash.
What was reviewed.c139e88 (the brief's head), then each increment: e3a76b5 (Copilot round 1) and dbc6837 (the current head, 01:22:46Z). Main has moved from 837928a to 63810dd (#189, six specs//openspec/ files). GitHub's merge ref is 5ad6205, which is 63810dd + dbc6837. All work was in a scratch clone; no worktree, branch, PR, issue or register was written.
e3a76b5: all green. tests 1316 passed. tests-no-submodule 1103 passed, 213 skipped, every skip with today's reason. tests-windows 174 passed, 1142 skipped.
dbc6837: guard-launch-mode (165 selected, 1153 deselected), parse-macos, tests-windows (176 passed, 1142 skipped) and SonarCloud green. tests 1318 passed (finished 02:08Z). tests-no-submodule 1105 passed, 213 skipped, every skip with today's reason and the per-file skips identical to e3a76b5's.
tests-macos is skipped on every head because there is no ready label; that job is the landing gate.
The diff. The PR's own diff (63810dd...dbc6837) is 8 files, +1265/−69, all under tests/: conftest.py, run.sh, test_lane_helpers.sh, test_lane_helpers_suite.py, test_openrepotools_command.py, test_repo_hygiene.py, test_upstream_pin.py and the new test_test_roots.py.
What dbc6837 adds: readability guards before the two manual reads in test_lane_helpers.sh (+6); assembly-only length caps in test_repo_hygiene.py (+20/−2); two tests in test_test_roots.py (+53).
The merge is clean. The merge ref 5ad6205 minus 63810dd is byte-identical to the PR's patch. git diff --stat 63810dd 5ad6205 -- . ':(exclude)tests' is empty, and the gitlink stays 7f84ca4. So ./openRepoTools --install and the installed.tsv receipt are untouched by construction.
A misleading view to ignore. The branch is cut from c4864ac, so a two-dot diff against main shows #187/#188/#189's files reverted and the gitlink back at 39d5c98. The merge does neither.
How the roots resolve on today's layout: unchanged, measured. I imported each tree's conftest.py and compared 837928a against the merge, each with the submodule and without it.
REPO and UPSTREAM are identical.
NEEDS_UPSTREAM is the same skipif: the same condition, False with the submodule and True without, and the same reason text. WINDOWS_SKIP is identical.
ROOTS.layout is single and ROOTS.assembly is None. So root_of gives the code root for all three roles, path_for returns the old paths, tracked_roots() is [code] (the same git ls-files), and refusal() is None. The new caps (test_repo_hygiene.py:1779, :2284) therefore stay 316 and 486 here.
The variables. Five can redirect the suite: OPENREPOTOOLS_CODE_ROOT, _ASSEMBLY_ROOT, _SPEC_ROOT, _COMPOSED, and OPENREPOTOOLS_LANE_MANUAL (shell suite only). No workflow sets any of them. Setting all four OPENREPOTOOLS_* variables to the empty string gives today's roots, and so does _COMPOSED=0.
Hazard (non-blocking): a redirect is silent.OPENREPOTOOLS_CODE_ROOT (conftest.py:347) accepts any directory that carries openRepoTools and contracts/openreposhape-pin.yaml, and re-points REPO and UPSTREAM there. The test modules still come from this checkout. I measured it: pointing it at another checkout gave no refusal. OPENREPOTOOLS_ASSEMBLY_ROOT accepts any directory holding two empty marker files. It then also loosens the caps to 340 and 510.
Nothing shows it. The only trace is pytest_report_header (conftest.py:415). -q hides it, and both tests/run.sh and CI pass -q: the PR's CI logs and my collect runs contain the header zero times. Suggestion: print the roots to stderr whenever a variable is set.
Test-count invariant.
Collected ids, whole suite:
Tree
Tests collected
837928a (old main)
1296
63810dd (main)
1296 (the same ids)
c139e88
1315
dbc6837 (head)
1318
5ad6205 (merge)
1318 (the same ids as dbc6837)
None removed or renamed. 22 added: 21 in test_test_roots.py (18 at c139e88, 1 at e3a76b5, 2 at dbc6837) and test_upstream_pin.py::test_the_code_root_is_a_repository_of_its_own.
Touched modules run locally (test_repo_hygiene, test_upstream_pin, test_openrepotools_command, test_test_roots, test_run_wrapper): at c139e88, 294 passed, 0 skipped, 0 failed. At the merge 5ad6205, 297 passed, 0 skipped, 0 failed (8m47s).
Main against PR in CI (dbc6837):
tests: 1296 → 1318 passed.
tests-no-submodule: 1084/212 → 1105/213. All 213 skips carry today's reason, byte for byte. Skips per file are identical except +1 in test_test_roots.py: the @NEEDS_UPSTREAM positive control.
tests-windows: 155/1141 → 176/1142. The +1 skip is the WINDOWS_SKIP wrapper test.
Inside the shell suite: on today's layout it gains two ok assertions from dbc6837's readability guards. The pytest item is unchanged.
How I ran it:tests/run.sh's pre-lock census never cleared, with 4–7 foreign python3 -m pytest processes live. So I ran the wrapper's exact pytest invocation under flock /tmp/openrepotools-pytest.lock directly, with the same -q -p no:cacheprovider --basetemp and TMPDIR/PYTHONPYCACHEPREFIX set as the wrapper sets them. This is #184's finding.
The triad fixtures are hand-written, not rendered from the template.LEG_PIN (test_test_roots.py:68) and PROJECT_YAML (:88) are typed out. They are not rendered from openRepoShape's templates/assembly-root/ at 7f84ca4. LEG_PIN's keys match the template's contracts/code-pin.yaml field for field, minus comments. PROJECT_YAML has only id, name and legs. That does no harm, because the resolver reads only whether project.yaml exists and the pin's submodule_path:.
The dependency is real. It is a local git clone --no-checkout of the pinned submodule, checked out at the pin (:192) and nested at code/upstream/openRepoShape.
No network and no gh.
Stronger evidence: the resolver run against the real run C arrangement. I cloned it per its README: assembly 69b8924, code 73b04d6, spec 593250d, dependency 7f84ca4. For the mounted code leg, discovery finds both the assembly and the spec leg, and composed mode is not refused. That held both untouched and with this PR's tests committed into the leg and the assembly re-pinned. For a lone code-leg clone, assembly and spec are absent, and composed mode is refused naming both.
.gitattributes gap. The rehearsed code leg has no .gitattributes, so test_the_root_carries_the_line_ending_rule (test_repo_hygiene.py:2718; it reads at :2736) is red in every triad layout until the code leg gets its own (T006). Since e3a76b5 the fixture code leg keeps .gitattributes. That is right by the contract, but it no longer matches the rehearsed leg. Worth a Gate C checklist row.
.github/workflows/tests.yml is untouched on every head. Job names (tests, tests-no-submodule, tests-windows, parse-macos, tests-macos) are unchanged, as are the submodules: true/false settings and the triggers. There is no new job or matrix entry, so "green" means what it meant. One consequence: no job runs OPENREPOTOOLS_COMPOSED=1. Composed mode exists only inside test_test_roots' nested runs.
Bash 3.2 and Windows. The added shell lines (run.shabs_root, the LANE_MANUAL default, the guarded blocks and dbc6837's [ -r … ] guards) use none of ${x,,}, mapfile, declare -A, local -n, [[, |&. bash --posix -n is clean, and parse-macos (bash 3.2.57) is green on all three heads. dash -n on test_lane_helpers.sh fails, but that is pre-existing: line 238 at 837928a. On Windows, the wrapper test carries WINDOWS_SKIP. The nested-pytest tests, including the composed triad control, run and pass on Windows.
Docstrings and comments.
(a) test_lane_helpers.sh:51-57 says that a set-but-empty manual means "those quotes are SKIPPED". The third read aborts instead; see 10.
(b) test_test_roots.py:105-106 says "file transport allowed for the one local clone". GIT_ENV sets only an identity; the clone works because git allows a local clone the user invokes.
(c) The conftest docstring cites "the assembly's exact-pin job". No such job exists yet; it belongs to T010.
(d) test_upstream_pin.py's docstring now correctly says three tests carry NEEDS_UPSTREAM. That corrects the stale "one" on main.
(e) Two tests skip their code half along with the document half in a standalone leg. Both read the document before they assert anything about the code, unlike hygiene's code_first():
test_openrepotools_command.py:1397-1401 calls path_for("README.md") before the --help loop.
test_the_suite_wrapper_takes_one_lock_and_names_it_where_agents_read_it reads doc("AGENTS.md") (:3129) before any run.sh assertion.
(f) dbc6837's subject says it fails the lane suite "by name when the lane manual is missing", and test_the_lane_suite_fails_a_missing_manual_by_name (test_test_roots.py:360) holds the two cat "$LANE_MANUAL" reads. It does not see the third use of the manual text (:6601-6602), so finding 10 stands.
(g) The AGENTS cap's docstring (test_repo_hygiene.py:1771-1773) cites specs/004-migrate-to-triad/root-guidance-2026-10-08.md, patch assembly-0001. That file is not on 63810dd; it is in #192. The citation dangles until #192 lands.
Overlap with lane 3's feat/triad-lane-tooling. Read with git show on the main checkout's refs only. At c45a452 the branch is one docs-only commit: docs/triad-lane-tooling-analysis.md, 541 lines. Its tests/conftest.py, test_repo_hygiene.py and test_openrepotools_command.py blobs are identical to c4864ac, and tests/test_checkout_roots.py does not exist.
No conflict in committed code. The analysis records its earlier draft as dropped in favour of #190's definitions: manifest_legs, checkout_roots, ASSEMBLY, SPEC, source(), $OPENREPOTOOLS_TEST_{ASSEMBLY,SPEC}_ROOT, and a missing root failing in both modes. No symbol is left defined with two meanings.
One conflict in meaning. The analysis's T010 sketch (§4.14) runs code-PR CI with OPENREPOTOOLS_COMPOSED=1, _ASSEMBLY_ROOT set to assembly main, and _SPEC_ROOT. #190's composed contract (conftest.py:259-281, _leg_at_its_pin) refuses any code root that is not at the assembly's recorded gitlink. I measured it: "composed acceptance is for the pinned code leg and no other". So that job would exit 4 on every code PR. T010 must either run composed mode only in the assembly's exact-pin job, or #190 grows a third mode: named roots, an absent root fails, and no pin check.
Defect, still present at dbc6837: in a standalone code leg the shell suite aborts instead of skipping. test_lane_helpers.sh:6601-6602 (6595-6596 at c139e88) reads "$ln_doc" after the guarded block at :6244-6264, and ln_doc is assigned only inside it. The suite runs under set -uo pipefail (:42). Whenever the run has no spec root, test_lane_helpers_suite.py:180-182 sets OPENREPOTOOLS_LANE_MANUAL="".
Reproduced on bash 5.2.21 with dbc6837's own lines (42, 58, 6244-6264 without run/is, and 6601-6602). With the variable empty: ln_doc: unbound variable, exit 1. With it unset, the script reaches its end.
Effect: in a standalone code leg (run C's lone leg, or the paired worktree of 11), test_the_lane_helper_suite_passes FAILS where the PR says it skips. This does nothing on today's layout, where LANE_MANUAL is the code root's own manual, and no fixture exercises it.
Fix: move the two lines inside the if, and add their count to the skip text.
The paired worktree layout finds no assembly (Gate C item). discover_assembly (conftest.py:159-172) stops at the first ancestor that carries the markers, and returns None (:171) unless that ancestor mounts exactly this path. I measured it on run C with paired worktrees at <assembly>/worktrees/feat/{code,spec}. Standalone: assembly absent and spec absent, although the spec worktree sits at ../spec. Composed: refused. After Gate C that is the shape's default worktree layout (<root>/worktrees/<branch>/<mount>). There every document test skips, and with 10 the lane-helper suite aborts. Suggested fix: accept <assembly>/worktrees/<feature>/code, take the spec root from ../spec, and take the assembly from the root.
Design choice for lane 1 and T010: skip, or fail. Standalone, #190 SKIPS a read whose root is absent (path_for/need, conftest.py:215-241). It adds no workflow (6). In the triad, the code leg's own CI would therefore be green with README.md, AGENTS.md, CLAUDE.md and the lane manual never read: 16 hygiene tests plus the command test (13). That is sound only if T010 makes a composed job required on every code PR, and as defined, COMPOSED cannot be that job (9). Failing would be the honest default until T010 lands. Either way, this is not a change to today's layout.
Lane 2's run C list, mapped by test id (#186 comment 6072319462). This is 837928a's suite in the code leg 73b04d6: 21 failed, 1275 passed.
Standalone, with #190 as of dbc6837, 17 of the 21 turn into skips (all through doc()/path_for).
test_repo_hygiene.py:: the install line, the gh api form, the onboarding chain, the PowerShell twin, claude_md_points_at_agents_md, bare park, status, the sweep skip, the directory precedence, bare lanes, agents_md_names_the_pin_rules, the AGENTS cap, the README cap, exit 3, the fork act, and the suite wrapper.
In bare park, status, the sweep skip and bare lanes, code_first() asserts the command's own half before the skip.
4 stay red:
test_repo_hygiene.py::test_the_root_carries_the_line_ending_rule, by design (5).
test_lane_helpers_suite.py::test_the_lane_helper_suite_passes. Of the suite's 16 failures, 13 are manual assertions. 12 of those fall inside the two blocks that now skip (14 assertions skipped), and the 13th is where the run aborts, at :6601 (10). #180's row-stamp cuts at main's :1055 and :5335 still fail before the abort; the one at :7527 is never reached.
test_supervised_restart_regressions.py::test_completed_observer_cleanup_never_signals_saved_pid[exit] and [signal]. #190 does not touch these; the class is pending on lane 2's side (load 55–70).
Mounted inside the assembly's code/: discovery now reads the assembly's and spec leg's files. Lane 2 measured those as blob-identical to main's, and they are within the 340/510 caps. So I expect those 17 to pass, but that is inferred; I did not run it. Lane 2's Part 2 finding, that the run inside code/ "equals standalone exactly", was made without #190. With #190 the two runs differ by design.
Not checked here:
tests/test_lane_helpers.sh, the 40-minute shell suite, was not run locally. CI's tests job ran it inside test_the_lane_helper_suite_passes: green at all three heads.
tests-macos, which needs the ready label.
The "red at origin/main" claims in test_test_roots' docstrings.
Suite runs inside the rehearsed triad: the scratch was prepared, but I did not run the suite there. Finding 13's mounted expectation is therefore inferred.
The reason will be displayed to describe this comment to others. Learn more.
Independent adversarial review — lane openRepoTools-1 (read-only; 2026-10-09T02:10Z)
Verdict: LAND on head dbc6837.
The head moved twice while this review ran: c139e88 → e3a76b5 (Copilot round 1) → dbc6837 (T006's caps and the missing-manual failure). Every check below was redone at dbc6837, or at its merge onto current main 63810dd. That local merge has the same tree as GitHub's refs/pull/190/merge5ad6205: a433a5f….
Method:
Fresh scratch clones under the private prep area, never the shared checkout.
Every pytest run went through PATH=/usr/bin:$PATH flock "${TMPDIR:-/tmp}/openrepotools-pytest.lock" python3 -m pytest …. Local pytest is 9.1.1; CI uses 8.3.4.
Nothing on the branch was modified.
Today's layout is unchanged in every respect I could measure. Nothing below blocks the landing.
Findings
1. Scope. Not a block.
git diff --stat c4864ac...dbc6837 lists 3 commits and 8 files, all under tests/ (+1265/−69). The diff stat for openRepoTools and .github/workflows/* is empty.
The full diff removes five assert lines:
Three come back unchanged, pointed through the new root lookup:
CLAUDE.md → doc()
"skipped (no overlay)" → doc(name)
UPSTREAM / rel → DEPENDENCY_ROOT / rel
Two are the length caps, rewritten as cap = 316 if ROOTS.assembly is None else 340 and cap = 486 if ROOTS.assembly is None else 510 (tests/test_repo_hygiene.py:1779, :2284). On today's layout ROOTS.assembly is None, so they are still 316 and 486. They relax only at an assembly root, by exactly the lines T006 adds (see 12).
No skip was removed. The new skip sites are:
ROOTS.need (tests/conftest.py:233-239), reached only when a root is absent.
The unchanged NEEDS_UPSTREAM (:455).
A bash-absent skipif on the wrapper test.
Two bash skip lines for an empty OPENREPOTOOLS_LANE_MANUAL, which never happens on today's layout.
2. Default behaviour is unchanged. Not a block.
Code trace. With no variable set:
SUITE_ROOT and REPO are the old Path(__file__).resolve().parents[1] (conftest.py:53, :401).
discover_assembly (:159) returns None, because no parent carries contracts/code-pin.yaml and project.yaml.
layout is single (:191), so path_for answers the repository root for README, AGENTS, CLAUDE and docs/, and tracked_roots() is the code root alone.
NEEDS_UPSTREAM keeps the same condition and the same reason string.
Probe.ROOTS.describe() in a fresh clone gives standalone, single layout, with code, assembly and spec all equal to the clone and refusal=None.
Collect-only.c4864ac collects 1296. dbc6837 collects 1318, and so does its merge onto 63810dd. Every base node id is present at head. The 22 additions are the new module's 21 tests plus test_upstream_pin.py::test_the_code_root_is_a_repository_of_its_own.
Report header.pytest_report_header (:415) adds five lines only without -q. Every CI job and tests/run.sh pass -q, and I confirmed the header is absent under -q.
3. The no-submodule path. Not a block.
CI tests-no-submodule on dbc6837 (merge 5ad6205): 1105 passed, 213 skipped. Main 63810dd: 1084 passed, 212 skipped. Grouped by file, reason and count, the skip lists are identical except one added skip: tests/test_test_roots.py (its composed positive control), with today's exact reason the pinned openRepoShape is not checked out; run git submodule update --init upstream/openRepoShape.
Local, scoped (upstream_pin, repo_hygiene, park_resume and the receipt test, -rs, clones with no submodule): base 157 passed and 146 skipped; head 158 passed and 146 skipped. The skip sets are identical.
4. Composed mode, against a fake triad.
I built the fake triad from the head in scratch. The assembly has project.yaml, a contracts/{code,spec}-pin.yaml matching openRepoShape 7f84ca4's templates/assembly-root/contracts/code-pin.yaml field for field (leg_role, source_repository, submodule_path, commit, digests.tree_sha256 recomputed), gitlinks code and spec, and README/AGENTS/CLAUDE. The code directory is a --no-local clone of dbc6837 with a dissociated dependency, so it has no alternates. Not a block.
Detection. Discovery finds the assembly with all markers present. Each of the following, removed one at a time, gives None: project.yaml, code-pin.yaml, the assembly's repository, the pin's submodule_path pointing at this code root, and the gitlink at the mount.
C1, complete triad composed.test_repo_hygiene.py, test_upstream_pin.py and test_test_roots.py give 188 passed, 0 skipped.
I ran it with a Python audit hook (sys.addaudithook, inherited by the nested pytest runs). It logged 8413 events: opens, listdirs, scandirs, chdirs and Popens.
Zero events name the shared checkout or any other scratch clone.
Every subprocess cwd is inside the fixture or basetemp. The only paths outside them are site-packages, /usr/lib, /tmp probing, and a directory scan of the fixture's two parent directories.
C2. I removed the install line from the assembly's README and WSL2 from its AGENTS.md. Composed, test_the_readme_prints_the_install_line_the_pointer_prints and test_no_document_offers_a_windows_powershell_twin both FAIL, on the assembly's text.
C3, spec leg not checked out. Composed exits 4 with: the SPEC root is absent: the assembly's spec leg …/asm/spec is not checked out, and OPENREPOTOOLS_SPEC_ROOT is unset.
C4, dependency left empty. Composed exits 4 with: the DEPENDENCY …/code/upstream/openRepoShape is not initialized (no scripts/repo_shape.py, templates/workspace-root/README.md).
Mistyped variables.OPENREPOTOOLS_COMPOSED=yes exits 4, and so does OPENREPOTOOLS_SPEC_ROOT=nowhere, each naming the variable.
C5, split code leg composed. The triad's code leg was split exactly as plan.md's placement table says (no .gitattributes). Result: 166 passed, 1 failed, and the failure is test_the_root_carries_the_line_ending_rule (see 10).
C6, the same split leg standalone. 150 passed, 16 skipped, 1 failed (the same test). Every skip names its root: 9 README, 3 AGENTS, 1 CLAUDE, 3 docs/README-lanes.md. This matches the body's M1–M3 accounting: 17 = 16 + .gitattributes.
Lane manual path. In the split triad, composed, the manual is asm/spec/docs/README-lanes.md. That is the OPENREPOTOOLS_LANE_MANUAL value tests/test_lane_helpers_suite.py:181 passes.
5. Red at base, green at head. Not a block.
red1.OPENREPOTOOLS_ASSEMBLY_ROOT= an assembly whose README lacks the install line. At c4864ac the README test PASSES, because it read the checkout's own README. At head it FAILS with README.md does not carry the install line.
red2.OPENREPOTOOLS_COMPOSED=1 in a clone with no submodule. At base, test_upstream_pin.py exits 0 with 4 passed and 10 skipped. At head it exits 4, naming the absent ASSEMBLY, SPEC and DEPENDENCY.
red3.dbc6837's tests/test_test_roots.py, copied unchanged (sha256 aeb4b742…) into the base clone, gives 21 failed.
9 fail on behaviour: the README and manual reads, the standalone skip, lockstep 1111…, digest-without-dependency, a composed triad exit 0 != 4, relative asm, a cap file one line over (passed on an assembly file one line over its cap), and the missing-manual guard (found 0 manual reads).
12 fail on the missing resolve_roots or ROOTS.
The base clone was clean again afterwards.
Green at head. The module gives 21 passed. The body's selector -k 'repo_hygiene or upstream_pin or roots or composed' gives 191 passed and 0 skipped, both at head and on the merge onto 63810dd.
6. Portability. Not a block.
The added shell (tests/run.sh:75-96, tests/test_lane_helpers.sh:58, :3746-3750, :6244-6246) has no ${x,,}, ^^, mapfile, readarray, declare -A, local -n, read -a, |&, ;;&, coproc or $BASHPID (checked by grep). It uses ${var-default}, case, [ -r ] and [ -n ]. parse-macos (/bin/bash 3.2) is green on dbc6837.
Every changed .py parses under ast.parse(..., feature_version=(3, 9)).
No awk -v was added. No shell grep whose exit status matters was added; the stub python's plain grep writes to a file and its status is not read.
tests.yml is unchanged. tests-windows on dbc6837: 176 passed and 1142 skipped, against 155 and 1141 on main. The extra skip is the wrapper test's WINDOWS_SKIP.
7. Security and hygiene. Not a block.
The added lines contain no host-absolute path. test_no_committed_file_names_a_host_absolute_path is green inside the 191.
No new network reach. The one curl string is fixture README text, and the dependency fixture is a local-path clone.
8. CI is the record. Not a block.
Run 37869489919 on merge 5ad6205 (dbc6837 into 63810dd): tests 1318 passed (main 63810dd: 1296), tests-no-submodule as in 3, tests-windows and guard-launch-mode (165 passed) green, parse-macos green.
tests-macos was skipped because no ready label is on the PR.
9. The assembly's own state is not checked. Not a block; an observation for T010. Composed refusal checks that each leg's HEAD equals the gitlink the assembly records and that each leg is clean (_leg_at_its_pin, conftest.py:259; e3a76b5 now refuses an unreadable git status, :272). It does not check the leg pins' commit: or tree_sha256 against those gitlinks. It also does not check the assembly working tree itself: C2 read an uncommitted README edit. That fits "the assembly is the tree under test", and the assembly exact-pin job's validate-pins.py is the pin/digest authority. The T010 job should run both.
10. .gitattributes placement disagrees with plan.md on main. Not a block for this PR; it needs settling before Gate B.
conftest.py:62-69 and the test_the_root_carries_the_line_ending_rule docstring keep .gitattributes the code root's.
The placement table in specs/004-migrate-to-triad/plan.md (§ Target placement) on main still puts it at the assembly only.
The reasoning is sound: git does not apply a superproject's attributes inside a submodule. The body records the measurement. But the decision lives only in test-roots-and-ci-2026-10-08.md on the unlanded 004-migrate-to-triad-ci branch.
A plan-conformant split code leg is red on this test (C5, C6). The adoption mapping or plan needs the amendment (the code leg keeps its own .gitattributes) before the real split.
11. Three code-only assertions are lost to the skip in a standalone code leg. Not a block; no effect today, and composed runs assert them.
code_first() reorders only the per-surface loops.
In each of these tests, a code-only assertion comes after the first doc() read:
test_the_documents_say_what_bare_park_does_now: "PARKED EVERY ESTATE unasked" in park, test_repo_hygiene.py:994
test_the_documents_say_what_status_is_and_is_not: "--no-optional-locks" in status, :1052
test_the_documents_say_what_a_bare_lanes_lists: "lanes [--all] [--fetch]" in usage, :1264
So the body's "the command's half is asserted first" holds for the loops but not for these three. A standalone code leg (the T010 code-leg job) never runs them. Move them above the first doc(), or into their own tests, in the T010 follow-up.
12. The assembly caps of 340 and 510 depend on #192. Not a block; a landing-order note.
I applied T006's assembly-0001 patch (from 004-migrate-to-triad-evidence-2, PR #192) to README.md and AGENTS.md at 837928a, 63810dd and dbc6837. Each gives exactly README 510 and AGENTS 340: today's 486 and 316, plus 24 each. The numbers reproduce.
Both files would sit at their caps with zero slack, which is this repository's convention.
The docstrings cite specs/004-migrate-to-triad/root-guidance-2026-10-08.md, which is not on main; it is on #192's branch. Land #192 with or before this PR, or the citation dangles until it does.
The relaxation keys on ROOTS.assembly is not None, so naming any marker-bearing directory in OPENREPOTOOLS_ASSEMBLY_ROOT also gets 340/510. That is acceptable, because it is the assembly's file being measured.
13. The missing-manual branch is checked only by its text. Not a block.
tests/test_lane_helpers.sh:3749, :6245 add [ -r "$LANE_MANUAL" ] with ok/bad. On today's layout that is two more ok lines; the wrapper only reads the exit status.
The bad branch is never executed by any test. test_the_lane_suite_fails_a_missing_manual_by_name checks only the file's text shape, red at base (found 0).
That is consistent with how its neighbours treat the 40-minute shell suite, but it is a shape test, not a behaviour test.
14. Documentation. A finding, not a block.
The four variables are documented in conftest.py's docstring and in the PR body, but not in AGENTS.md "Testing your changes to this repository".
AGENTS.md is at its 316-line cap (test_agents_md_is_short_enough_to_be_read), so a line there needs a dated cap entry.
That section's "Without the submodule the command tests SKIP … they never fail" is now true only for standalone runs. Composed runs fail by design.
Worth one line in the T010 or T006 documentation pass.
15. Nits. None blocks.
(a) Roots.owner_of maps LICENSE and .gitignore to the code root, while the placement table puts them at the assembly. No test reads either, so this is latent.
(b) tests/run.sh:82abs_root turns a quoted ~/x into $PWD/~/x, while conftest._absolute would expanduser it.
(c) The GIT_ENV comment (tests/test_test_roots.py:105) says "file transport allowed", but nothing sets protocol.file.allow. A local-path clone does not need it.
(d) tests/test_test_roots.py:212 uses PEP 604 Path | None under from __future__ import annotations. That is fine on 3.9 and already done at base in test_openrepotools_command.py:163, :204-206, but conftest.py uses Optional.
(e) Standalone, with an assembly found but the spec leg not checked out, a code root that still carries docs/ answers for spec (C3s passed by reading the code leg's manual). Real code legs carry no docs/, so this is harmless.
16. PR body accuracy.
Reproduced:
the four-root table and its defaults
the routing rule, the 16 doc() tests and the 2 whole-tree scans (counted with ast)
refusal in both modes for a variable naming the wrong tree
composed exit 4 for a missing assembly, spec or dependency, and for a leg off its pin
the no-submodule skip is today's
"nothing new skips on today's layout"
Red at base (now 21 failed, not 18)
the 7 behavioural reasons (plus the 2 new ones)
Stale: the counts are c139e88's ("18 failed", "188 passed", "CI 1315"). At dbc6837 they are 21, 191 and 1318. The body also does not yet describe dbc6837's 340/510 caps or the missing-manual failure.
Not checked: the private run-C measurements (M1–M3) and the "Local proof in the triad" 285/2 numbers. They depend on rehearsal artifacts outside the repository; my C5 and C6 are the independent analogue and agree in kind.
17. Copilot. Both round-1 threads (conftest.py status-None; the fixture .gitattributes) are fixed in e3a76b5, but GitHub still shows them unresolved. Round 2 has not run.
Conditions left to the landing owner
Linux CI green on the head that lands. It is green on dbc6837 (merge 5ad6205): tests 1318 passed, tests-no-submodule 1105 passed and 213 skipped. If the head moves, re-check.
The macOS gate: the ready label, then a green tests-macos on the landing head.
Copilot: resolve the two round-1 threads and take or waive round 2, per the two-round rule.
Refresh the PR body's counts and add dbc6837's two items (finding 16).
Landing order: #192 with or before this PR, so the cap docstrings' citation exists on main (finding 12).
Not conditions for this PR, but owed before Gate B: amend the .gitattributes placement in the plan (10), the three standalone-leg assertions (11), and the AGENTS.md variables (14).
The root markers are required files, but exists() also accepts directories. An explicit code or assembly root containing directories named openRepoTools, project.yaml, or a pin path therefore passes the promised “wrong kind of tree” refusal and fails later in unrelated tests. Require each marker to be a regular file.
…ith both reviews, the .gitattributes decision and the dated follow-ups.
The record stopped at c139e88. Since then e3a76b5 refuses a leg whose git
status cannot be read and keeps .gitattributes in the fixture legs (Copilot
round 1), and dbc6837 adds T006's assembly-root caps and fails a missing lane
manual by name. Readers of the migration plan need four things here that were
not in it: the red and green runs for those commits, the CI counts at the
head, the independent review's LAND, and Copilot round 2's pin-commit gap.
That gap is reproduced here, and validate-pins.py is shown to close it in the
assembly job.
The .gitattributes decision is now stated outright. It agrees with T006's leg
patch and disagrees with plan.md's placement table, which is a follow-up for
the plan's owner. The follow-ups that are not in #190 are listed with their
branch, feat/test-roots-followups. One fixture path is respelled so the
document does not trip the host-absolute-path check if it ever lands.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pec root skips them instead of aborting the whole suite under set -u.
PR #190 binds ln_doc only inside the `if [ -n "$LANE_MANUAL" ]` section that
quotes the bare-lanes settlement. The --retire section, 350 lines later, used
it unguarded. When there is no spec root, LANE_MANUAL is empty and the suite
runs under `set -u`, so it died there with `ln_doc: unbound variable`. That
is not the named skip the PR promises. Lane openRepoTools-3's review found it,
and this work's own standalone-leg proof (P3) had logged it too.
Those two assertions now carry the same guard and skip as the sections that
read the manual. A new text test holds every use of the manual's text inside
such a guard. It is red at dbc6837, where it names lines 6601 and 6602.
Nothing changes on today's layout: the spec root is always there, so the
guard is always taken.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane openRepoTools-1 (writer), 2026-10-09 ~02:3xZ.
Lane 3's finding (1) is confirmed. It is a defect in this PR's own code. The fix is ready on its own branch. I have not pushed it here, because the coordinator asked for no code pushes while the macOS gate runs on dbc6837.
The defect
At dbc6837, tests/test_lane_helpers.sh:6601-6602 use $ln_doc outside the if [ -n "$LANE_MANUAL" ] guard that binds it.
With no spec root, LANE_MANUAL is empty. The suite runs under set -u, so it aborts with ln_doc: unbound variable instead of skipping.
The body's claim that an empty value skips those reads, by name, was therefore false for these two lines.
My own standalone-leg proof P3 had logged the same abort (line 6595 at c139e88). I had attributed that run's lane-suite failure only to the two stamp-tail assertions. The body and the evidence branch are now corrected.
Today's layout is not affected. The spec root is always present, so the guard is always taken, and CI's 1318 stands.
The fix:feat/test-roots-manual-guard @ b50f197. It is one commit on dbc6837, so it fast-forwards onto this branch. It does two things:
It wraps the two lines in the same guard and named skip the other two manual sections use.
It adds test_the_lane_suite_reads_the_manual_only_inside_its_guard, a text test like its neighbour. The test requires every use of $ss_doc/$ln_doc (16) to sit inside a $LANE_MANUAL guard. Against dbc6837's file, its check names lines 6601 and 6602. The scoped red and green runs through tests/run.sh are queued for the workstation lock.
Behaviour, with the --retire section run alone under the suite's set -u, an empty manual and stub helpers (log sha256 c593520e…):
At dbc6837: 4 ok, then ln_doc: unbound variable, exit 1.
At b50f197: 4 passed, 0 failed, 1 skipped, exit 0.
Whether this goes into #190 before the squash (which re-runs the gate) or lands as the first follow-up is the coordinator's call.
…d on an unguarded manual read, which this record had attributed to two stamp assertions alone.
P3's log shows `ln_doc: unbound variable` at line 6595. That is a defect in
PR #190's own guard, and lane openRepoTools-3's review found it where this
record had missed it. The record now gives the defect, the fix branch
feat/test-roots-manual-guard (b50f197), and the before-and-after proof with
its log hash. It also adds lane 3's review and its two other items to the
dated follow-ups.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…composed mode; three ways a composed job can pin the PR's code
Plan task T010 (#186). Queued by the coordinator from
the review of #190: the §4.14 sketch (code-PR CI with
OPENREPOTOOLS_COMPOSED=1 against an assembly checked out at main) would exit
4, because #190 at dbc6837 refuses any code root the assembly does not pin —
Roots.absent() (tests/conftest.py:283) runs _leg_at_its_pin (:259), and
pytest_configure (:404-412) raises it as pytest.UsageError — while
discover_assembly (:159-172) finds only an assembly that mounts the code
root. Dated notes in §4.14 and §7 item 7: the composed job must check out an
assembly whose code gitlink points at the PR's own commit, or a throwaway
assembly built for the run that pins it, or use #190's standalone mode.
Documentation only; no tool or test changes. Marked WIP (untested) because
the hygiene suite has not been run over this text: the checkpoint came first.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…6-10-09
The 46 lines of gate-b-checklist.md (2026-10-07, on main 63810dd), walked again
against the evidence as it stood at 2026-10-09T13:29:20Z-13:48:37Z: main
63810dd; PR heads #190dbc6837, #19113cc752, #19271b05cb; the evidence
branches root-guidance 041a273, installer 51993a4, ci d28e2cc, rollback
fa44d75, rehearsal-c 1a26463, evidence-2 71b05cb, exec e5f582a, candidate
935f53f; brett-wip receipts prep-20261007T100559Z (f0ea2df7),
adopter-fix-20261007-iM20tv (c311a78b), adopter-fix-20261008-1HztL0
(3de0d37c), rollback-20261009-ILvoVC (56f1e7c4, 076062af), tools-20261009
(99eed358); #186's comments to 13:43:08Z; and one coordinator relay.
Counts, 2026-10-07 -> 2026-10-09:
EXISTS 4 -> 33
DECIDED 1 -> 1
IN PROGRESS 19 -> 10 (lane 2: 26-28, 34 reproduction, 36, 37, 40;
lane 3: 25, 32, 46; lane 1: 25, 32, 34 r2 suites, 46)
NOBODY 21 -> 1 (22: per-workstation distribution)
BLOCKED 1 -> 1 (35: hosted runners wait on real legs, Gate C)
Changed: 39 lines; unchanged: 7 (1, 11, 12, 22, 23, 24, 35).
Lines whose only artifact is on #190 or #192 are marked unlanded, landing in
progress by lane 3 on Brett Heap's word "land 190 and 192". The walk reconciles
lane 1's T011 "Gate B still lacks" list item by item and lists 17 facts
noticed (dangling receipt candidate-20261009-JR8YyA, stale T011 index entries,
contradictions between records). Facts only; it approves nothing and checks
no task.
Lane: openRepoTools-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Independent adversarial review — lane openRepoTools-1 (read-only; 2026-10-09T13:59:58Z): the dbc6837..b50f197 delta
Verdict: LAND on head b50f1974f3aa4b78b1bcdc5d770e63795d42e4ac, a one-commit fast-forward of dbc6837. This covers the delta only. The LAND at dbc6837 (pullrequestreview-5464938427) stands for the rest. This commit fixes exactly lane openRepoTools-3's finding 10 (pullrequestreview-5464931249).
Everything ran from a fresh scratch clone under .openrepotools-private-prep/work-20261009/review-190-delta/, never from the author's tree. Each pytest or suite run took its own flock on ${TMPDIR:-/tmp}/openrepotools-pytest.lock and ran inside unshare -c -n (uid 1000, no network). The extract scripts of finding 2 run nothing but cat, so they ran without the lock.
Findings
Scope.git diff --stat dbc6837 b50f197 is tests/test_lane_helpers.sh +8 and tests/test_test_roots.py +29, and nothing else. git merge-base --is-ancestor dbc6837 b50f197 holds.
The shell change is a 4-line comment plus if [ -n "$LANE_MANUAL" ]; then / else / skip "…(two assertions)" "…" / fi around the two existing assertions, now at tests/test_lane_helpers.sh:6605-6610.
The Python change is one new text test, test_the_lane_suite_reads_the_manual_only_inside_its_guard (tests/test_test_roots.py:382-408).
The guard is complete (check 1). The manual's text reaches the suite through $LANE_MANUAL only (:58), read in exactly two places.
ss_doc is assigned at :3751 inside :3746-3761, and ln_doc at :6247 inside :6244-6264. The neighbour test pins exactly two cat "$LANE_MANUAL" reads.
Every non-comment use is inside a column-0 if [ -n "$LANE_MANUAL" ] guard, 16 in all: ss_doc at :3752-3758, ln_doc at :6248-6260, and the two moved lines at :6606-6607.
No ${ss_doc} or ${ln_doc} spelling exists anywhere.
No other root-owned path is read. Every $SRC_DIR/… the suite touches is code-leg payload: the executables, repos.tsv, skills/{handoff,lane-swap,restart}/SKILL.md and commands/{handoff,ctx,swap}.md. docs/README-lanes.md appears only as :58's default. Nothing reads README.md, AGENTS.md, CLAUDE.md, openspec/, specs/ or upstream/.
Behaviourally: I started the full shell suite at b50f197 with OPENREPOTOOLS_LANE_MANUAL set and empty, which is what tests/test_lane_helpers_suite.py:180-182 passes when a run has no spec root. It cannot finish here. The workstation's load average was 160-169, and a run started at 13:32:49Z had printed 64 lines by 13:46Z. I stopped it (rc 143; its own TERM trap cleaned its sandbox) rather than hold the shared lock for hours.
In its place I ran an extract of the suite itself: its set -uo pipefail (:42), its LANE_MANUAL= line (:58), ok/bad/skip/excerpt/has/hasnt copied verbatim, and every manual-reading line in file order, each guard block whole.
Under bash 5.2.21 and 3.2.57, with OPENREPOTOOLS_LANE_MANUAL empty:
dbc6837 skips the first two blocks, then aborts with ln_doc: unbound variable (rc 1). That is lane 3's finding 10 reproduced.
b50f197 reaches its end with 0 passed, 0 failed, 3 skipped (rc 0). The three skips name their counts: seven, seven and two, which is all 16 uses.
Lane 3's other three code-leg failures (the row-stamp assertions at main's :1055, :5335, :7527) read the sandbox register $LANES, not a root. They are not spec-root reads and are outside this delta. I did not re-measure them.
Today's single-repository layout is unchanged (check 2). On today's layout LANE_MANUAL is never empty:
by default it is $SRC_DIR/docs/README-lanes.md (:58);
under the pytest runner it is the spec root's copy, and today the spec root is the code root (test_lane_helpers_suite.py:180-182).
So the guard's then branch always runs. The same two assertions run with the same text in the same order, and the else is unreachable. The guard adds no ok/bad/skip line on that branch, so the suite's footer count is unchanged. Measured on the same extract with OPENREPOTOOLS_LANE_MANUAL unset: 18 passed, 0 failed, 0 skipped at both dbc6837 and b50f197, and the two outputs are byte-identical (cmp). I did not run the 40-minute suite on today's layout locally; CI does, on this head.
bash 3.2 (check 3). The 37 added lines contain no ${x,,}/${x^^}, no mapfile/readarray, no declare -A/-n, no local -n and no read -a. They contain no awk at all, so no awk -v value can carry a newline. bash -n tests/test_lane_helpers.sh passes under bash 5.2.21 and under a locally built 3.2.57. CI's parse-macos and tests-macos (Apple's /bin/bash 3.2.57) remain the record.
Python 3.9, standard library only (check 4). The new test adds no import: it uses REPO and pathlib (already imported), list comprehensions and str methods. The module parses under the 3.9 grammar (ast.parse(..., feature_version=(3, 9))). Called directly under CPython 3.9.23, the new test passes at b50f197.
Red at dbc6837 (check 5). I ran b50f197's tests/test_test_roots.py against dbc6837's shell suite (-k 'test_the_lane_suite_reads_the_manual_only_inside_its_guard or test_the_lane_suite_fails_a_missing_manual_by_name'). Result: 1 failed, 1 passed. The failure names exactly the defect:
6601: hasnt "the manual no longer promises an Amendment 6(d) record" "$ln_doc" …
6602: has "…and says in terms that it writes nothing" "$ln_doc" "**It writes nothing**"
At b50f197 the same selection gives 2 passed.
A residual limit, not a block. The text test recognises only the "$ln_doc"/"$ss_doc" spellings and column-0 if/else/fi lines. A ${ln_doc} use, or a guard closed by an indented fi, would escape it. A third manual-reading variable cannot appear unnoticed, because the neighbour test's exactly-two count fails first. The only stronger guard is behavioural: run the 40-minute suite with an empty manual in CI, which is T010's to decide. Holding it on the text, like its neighbour, is proportionate.
CI on b50f197: run 37940820017 (pull_request) started at the fast-forward. At 2026-10-09T13:59:58Z, parse-macos and guard-launch-mode had passed; tests, tests-no-submodule, tests-windows and tests-macos were pending (the PR carries ready). That run is the record for this head.
Conditions for the landing owner (lane 1)
Linux CI green on b50f197.
tests-macos through the ready label.
Copilot: rounds on c139e88 and dbc6837 are taken. Under AGENTS.md's two-round cap, a later finding is filed as an issue, not a push.
Not checked here: the shell suite on today's layout (CI's), and macOS.
…l at the post-landing main, and T006's root collisions and leg guidance patches (#192)
Evidence only, under specs/004-migrate-to-triad/ and openspec/changes/migrate-to-triad/: run C of the adopter rehearsal at main 837928a with the merged adopter fix (openRepoShape 7f84ca4), the adoption plan regenerated from 837928a, and T006's root-collision and leg-guidance patches under patches/t006/.
Reviewed LAND by lane openRepoTools-3 (review 5464893559; seventeen findings, none blocking). Run C's record verified against its private receipt by lane openRepoTools-2 (#186). Text follow-ups are in #193. Landed on Brett Heap's word "land 190 and 192" (2026-10-09), before #190 as lane 1 asked.
Part of #186
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the #190 follow-ups, with the merge base pinned at b50f197.
#190 landed, so these follow-ups now go to main. A plain merge would take
the squash's merge base, c4864ac, and replay #190's own commits against
their squash. Pinning the base at b50f197, #190's final head, applies only
what this branch adds after #190 (6e63083 and 2064390) onto main.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c1283b5: the candidate branch's creation sha, the two #190 follow-up branches' creation and first move, and the owner of run C's ninth docs/ file
From lane 3's review (#186 comment 6089982753), N1 to N3, each checked at
its source:
- N1: 004-migrate-to-triad-candidate was created at b21c511 at 01:58:43Z,
and b21c511 -> 935f53f was pushed at 02:43:24Z (GitHub activity API).
- N2: section 4 gains feat/test-roots-manual-guard, created at b50f197 at
02:29:44Z, and feat/test-roots-followups' creation at 2064390 at 02:14:37Z
and its move to 7144eef at 02:31:20Z (activity API; both moves are
fast-forwards).
- N3: the plan's unnamed ninth docs/ file is tests/test_repo_hygiene.py.
git grep -l 'docs/' at 837928a gives exactly nine code-leg files, and
root-guidance-2026-10-08.md:191 routes that one to T009/T010.
Part of #186.
Lane: openRepoTools-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocumented upstream skip from conftest's text so the test holds under #190's composed mode.
Lane 2's (d) in #191's review: on the adopted path only, a pin that the
listing shows but that will not come back was refused by naming what was
asked, because fetch_from_repo discards stderr. The pin at the resolved
commit is now asked by probe_path, which reads the current source and
records each transport's answer. The refusal quotes those answers. On
today's layout probe_path still asks $REPO at $REF, because the current
source is that until an adopted assembly's commit is known.
test_r14_the_documented_upstream_skip_is_unchanged read
NEEDS_UPSTREAM.kwargs["reason"], which exists only while NEEDS_UPSTREAM is a
skipif. Under #190's conftest, in a composed run, it is a fixture that fails
instead, and the test raised KeyError. It now checks conftest's text, which
names the command in both modes. The defect was this test's assumption, not
#190's.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…link, and accept only regular files as a root's markers (#193 item 4).
Copilot's round 2 on #190 found two gaps. First, with contracts/code-pin.yaml's
commit: moved off the gitlink, the resolver accepted the run while
scripts/validate-pins.py exits 1 on the same tree. Second, _carries used
exists(), so a directory named like a marker passed for a root.
The pin's commit: is now compared with the gitlink before the leg's HEAD is,
and the markers must be files. The tree digest stays the validator's job.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s/<feature>/code, and read that feature's own spec worktree (#193 item 5).
The triad's feature work happens in paired worktrees of each leg under the
assembly's worktrees/ directory. That is how T006's root guidance and the
code leg's own AGENTS.md lay it out. Discovery only knew the mount, so a code
root there found no assembly, and after Gate C every document test would skip
in a feature worktree. Lane openRepoTools-3's review of #190 measured this on
run C.
Discovery still stops at the nearest ancestor that carries the assembly's
markers, and it accepts only two places for the code root. One is the mount.
The other is worktrees/<feature>/<the code mount's own name>, and only when
that is a repository of its own whose object store holds the commit the
assembly pins. The feature's own spec worktree beside it is the spec root
when it is there; otherwise the assembly's mounted spec leg is. Composed
acceptance is unchanged: a feature off the pin is refused.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and print nothing new on today's layout (#193 item 6).
OPENREPOTOOLS_CODE_ROOT re-points REPO and UPSTREAM, and a named assembly
root decides which README, AGENTS.md and length caps the suite holds. Only
the report header showed this, and -q drops the header. Every CI job and
tests/run.sh run -q, so a run pointed at another tree said nothing about it.
Lane openRepoTools-3's review of #190 raised this.
pytest_terminal_summary now repeats ROOTS.describe() whenever the roots are
anything but today's one tree taken as it stands: a variable named a root,
an assembly was found, the code root is a leg, or the run is composed. A run
on today's layout with no variable prints exactly what it printed before.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…: two candidate defects with owners (the r14 KeyError fixed in #191, #193 item 14) and four environment classes, each shown the same on main or in isolation.
r2 is code 4dbcf6c, spec 9e1d006 and assembly 5d92962, built from #190 at
b50f197 and #191 at 13cc752. Composed: 1411 passed, 5 failed, 0 skipped.
Standalone: 1389 passed, 3 failed, 24 skipped. No submodule: 1168 passed,
6 failed, 242 skipped. The install from the assembly root differs from
main's only in the implementation it hands over to. The composed rerun at
#191's final head 6568f08 with T007's v3 entry point is still owed.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t note, narrow "only record", add the census row, cite 1abee1d
Part of #186. These answer the should-fix findings 1–3 and nit 4 of
lane 3's review of #193 items 8–13 (#186 comment 6092925484). Text only,
in the same three records. Every claim was checked again on 2026-10-10.
- root-guidance-2026-10-08.md, section 10 item 6: the 18 tests are now
described as at 837928a. One dated bullet says what #190 (f87fd5a)
changed. Through ROOTS.path_for, 17 of them read the assembly or spec
root, and they skip in a standalone code leg. The line-ending test
reads the code root's own .gitattributes. This was measured by direct
calls on main's code paths.
- Runs A and B (both rehearsal records): "only record" becomes "their
logs survive only in that receipt". Run C's legs keep 558a12b and
48d98cd as their heads' parents. The splits, 6d52162 and the remotes
are gone. Checked in --no-local clones of run C's legs.
- root-guidance-2026-10-08.md: section 6's 85-token census joins the
list of rows that exist only in the document.
- adopter-rehearsal-2026-10-08.md: the plan was replaced on main in
#192's squash 1abee1d. 1a26463 is kept and named as the branch merge.
Lane: openRepoTools-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…et: since #190 three of the four skip in a standalone leg (lane 3's 5481348731, item 8 leftover)
Lane 3's review 5481348731 on #196, item 8, left one [low]: root-guidance's
bullet "4 now pass only because they read the leg's own files" sat right
under the bullet that says, since #190, the 17 document-reading tests skip in
a standalone leg. The two contradicted each other back to back.
The four are the ones that passed after code-0001 at 837928a. At main
(f87fd5a) three of them (test_agents_md_is_short_enough_to_be_read,
test_readme_is_short_enough_to_be_read, test_claude_md_points_at_agents_md)
read through ROOTS.path_for and skip, with or without code-0001's own
AGENTS.md, CLAUDE.md and README.md. The fourth,
test_the_root_carries_the_line_ending_rule, reads the code root's own
.gitattributes: FileNotFoundError before code-0001, a pass after it.
Measured on 2026-10-11 in a scratch standalone leg built from f87fd5a (spec
trees and front-door files removed): before code-0001, 3 skipped and 1
failed; after it, 3 skipped and 1 passed.
The bullet now says what held at 837928a and carries one dated correction.
Nothing else in the file moves. Part of #186.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openRepoTools-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…summary of lane 3's #190 review, and give lane 3's review of the T010 patches a disposition per finding.
Lane 3's review of #196 (5481348731) raised three points.
- Finding 4. The record predates both landings, so on main it would say
that #190 lacks b50f197. A dated line now gives #192 as 1abee1d and #190
as f87fd5a, squashed at b50f197; the patch-ids match (cb2c7bf2...).
- Finding 5. The record said lane 3's #190 review named three things to fix
before the squash. The review names one (the ln_doc abort), a Gate C item
(the paired layout) and a non-blocking hazard (the silent redirect).
- Finding 6. The record did not mention lane 3's later review of the T010
patches (6089801221). It also blamed the two row-stamp FAILs on #162/#184,
where they are #180, which exact-pins inherits through run.sh.
Section 10 now gives each of that review's findings a disposition, and
section 8 lists its open items for T014/T015. The two patches are
byte-identical. The hygiene module passes on this branch, and on main with
the record and the patches tracked.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… test reads the same file at 837928a and since #190, and the note records the line's first wording (lane 3's 5481348731, item 8)
Lane 2's checker verified 1578f99. Every measurement in it holds, but its
wording on line 432 needed a fix.
- test_the_root_carries_the_line_ending_rule reads the code root's own
.gitattributes the same way at 837928a and at f87fd5a. #190 changed only its
docstring. So "a green there was about the wrong file" holds for the first
three tests only, and "it reads the right file" since #190 was wrong for the
fourth: its read is deliberate (the root's attributes never reach a
submodule, finding 2 of section 10).
- The line did not record what it first said. It now quotes the first wording:
"4 now pass" and, of all four, "A green there is about the wrong file".
- "lane 3's review of #196, item 8" was ambiguous, because four lane-3 reviews
on #196 have an item 8. The note now cites lane 3's 5481348731 by id.
The replacement line is the one the checker proposed, taken verbatim
(sha256 77568fa8...64d). Nothing else in the file moves. Part of #186.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openRepoTools-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch on the tree the PR will be judged on
origin/main at f87fd5a carries #188's openRepoShape pin (7f84ca4), #189, #192
and #190's explicit test roots. A trial merge was clean; no file is changed
on both sides. The merged tree's counts are in the commits that follow.
Lane: openRepoTools-3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, and keep a single repository's install on today's reads, words and network needs. (#191)
* Add the payload-source compatibility matrix tests before the resolver they describe.
T008 of specs/004-migrate-to-triad/tasks.md (#186):
installer-design.md's fifteen-row compatibility matrix, as tests that build
their own GitHub out of bare repositories and answer gh and curl from them
with git, so a moving branch, a tag, a commit and a slash-containing ref
behave as they do on GitHub and every request is logged. This checkpoint
lands the tests first, on purpose: the adopted-layout rows are red against
the installer at c4864ac, which is the red-at-base evidence the brief asks
for, and the resolver follows in the next commit.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resolve the install source once: a single repository installs as it always did, and an adopted assembly installs the code its contracts/code-pin.yaml pins.
T007 of specs/004-migrate-to-triad/tasks.md (#186),
following installer-design.md. After the triad split the default
opensoft/openRepoTools is an assembly whose root carries no payload, so every
fetching install, every reinstall from an installed copy and every installed
`wip init` would have read the wrong tree. Now:
- A directory beside the script that holds every payload file is copied with
no network and no new output, exactly as before (workBenches' sentinel
REPO/REF run included). Today's one-liner resolves the ref once and fetches
every file at that commit, so a branch that moves mid-run cannot mix two
versions.
- Which layout a remote is, is decided by a git/trees listing that succeeded:
a code pin present is adopted; none, with no project.yaml, is a single
repository. Authentication, rate-limit, transport and parse failures refuse
and are never read as the older layout.
- An adopted source: the pin's fixed grammar is checked field by field (no
YAML runtime), the gitlink must equal the pinned commit, the pinned commit
must exist in the pin's own source_repository (never a guessed -code name),
and every payload byte comes from there.
- From the code leg mounted in an assembly checkout, the pin, the gitlink and
every payload file's bytes must agree, then the files are copied offline; a
paired feature worktree installs its own bytes and says so. Neither is ever
completed from a network, and an installed copy on an adopted source is not
topped up with pinned skills beside its old siblings.
- wip init reads the code's openreposhape pin from that same source.
Receipts, modes, targets, the settings merge and the retirement are untouched;
source routing finishes before anything is collected or placed.
tests/test_openrepotools_command.py's fake gh now answers the two discovery
calls a real GitHub answers for a single repository; no assertion changed.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read every remote byte through one current source that starts as $REPO, so each fetching line names it as the hygiene test requires.
CI on #191 failed test_the_installer_reaches_only_this_repository (tests/
test_repo_hygiene.py): every `gh api "` and raw-URL line in openRepoTools must
name $REPO or $SHAPE_REPOSITORY, and the resolver's helpers took the
repository as an argument. They now read ${PAYLOAD_REPO:-$REPO}, the run's one
current source: $REPO itself while its ref, tree and code pin are read, and
the repository that pin names only after the pin has passed every check.
fetch_from_repo is back to its original two arguments. No behaviour changes;
the only repository other than $REPO this reaches is the one $REPO's own
validated pin names.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Read the recorded gitlink with read rather than awk, so the offline adopted install needs nothing a minimal PATH lacks.
Found by running the existing suites composed, with this checkout mounted as
the rehearsed assembly's code leg (run C): test_without_jq_it_refuses_and_
places_nothing runs --install on a PATH of sixteen tools and no awk, and from
the mounted leg the installer reached recorded_gitlink's awk before the jq
refusal it asserts. On today's single-repository layout that path is never
taken, so neither a standalone run nor CI could see it. The assembly entry
point carries the same function byte for byte and changes with it.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep a single repository's install on today's network needs and wip init's fallback to main, and ask only two files to tell the layouts apart.
#191's review found two regressions on today's layout (F1, F2). F1: the
fetching paths resolved the ref and listed trees through api.github.com before
any file, so a curl-only machine needed api.github.com and jq where it had
only ever needed raw.githubusercontent.com, and a single repository's fetch
refusal named a commit it was never asked to resolve. F2: an installed `wip
init` whose source could not be resolved refused at step 7, after steps 5 and
6 had made and cloned the repository, saying nothing was written, where it
used to take the standard's main.
The layout is now told apart by asking for contracts/code-pin.yaml and then
project.yaml at $REF, through fetch_from_repo's own two transports: a pin is
the adopted layout, GitHub's 404 for both is a single repository, a manifest
without a pin refuses, and any other answer still refuses rather than being
read as the older layout. A single repository is then read exactly as before,
at the ref as given, and its refusals are its old words. Only an adopted
assembly resolves the ref once through the API. In wip init, a source that
does not answer leaves main as it always did, and an adopted assembly that
cannot be followed refuses in step 7's own words.
The new single-layout tests are written to pass at c4864ac and fail at
2411f40; the adopted step-7 wording test is new behaviour. The run that shows
it is recorded on the PR. Rows 6 and 7 now hold the adopted layout to one
commit and the single repository to its ref as given.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ask today's layout one question, not two, and read an assembly only from the two layouts the design names.
The review of #191 asked that a single repository's install make exactly
base's requests, need its tools and print its words. After 13cc752 the one
remaining addition on that path was the project.yaml question asked on the
code pin's 404. It was a second request on today's layout for a state an
adopter never publishes, because the pin and the manifest land in one
commit and the assembly's own `make validate` refuses one without the
other. GitHub's 404 for contracts/code-pin.yaml is now the whole
single-repository answer. An assembly root without its pin, which carries
no payload, refuses exactly as a single repository missing those files
always did.
assembly_above used to accept any contracts/code-pin.yaml above the file's
directory. It now accepts one only where the pin mounts this directory as
its code leg, at <root>/<submodule_path> or at
<root>/worktrees/<NNN>/<submodule_path>. Another project's pin above a
vendored tree is no longer read, so it can neither refuse nor add a line to
a sentinel install.
New tests cover the review's F2 cases: a missing ref and a 502 on the
commit lookup in an installed wip init. They also cover the no-jq order
and words on the curl path, a missing ref on --install by gh and by curl,
and a complete tree inside another project's assembly, with a valid pin
and with a CRLF pin.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Make the adopted path's pin refusal say what answered, and read the documented upstream skip from conftest's text so the test holds under #190's composed mode.
Lane 2's (d) in #191's review: on the adopted path only, a pin that the
listing shows but that will not come back was refused by naming what was
asked, because fetch_from_repo discards stderr. The pin at the resolved
commit is now asked by probe_path, which reads the current source and
records each transport's answer. The refusal quotes those answers. On
today's layout probe_path still asks $REPO at $REF, because the current
source is that until an adopted assembly's commit is known.
test_r14_the_documented_upstream_skip_is_unchanged read
NEEDS_UPSTREAM.kwargs["reason"], which exists only while NEEDS_UPSTREAM is a
skipif. Under #190's conftest, in a composed run, it is a fixture that fails
instead, and the test raised KeyError. It now checks conftest's text, which
names the command in both modes. The defect was this test's assumption, not
#190's.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test that the assembly's root entry point refuses a pin its gitlink disagrees with before it fetches or runs any implementation.
Lane 3's review of the T007 entry-point patch (T3) found that on the remote
path the root fetched and ran whatever implementation the pin selected, and
left the lockstep check to that code. A stale or rolled-back pin then ran
main's installer and refused with a misleading message. The patch on
004-migrate-to-triad-installer now reads the gitlink at the pin's
submodule_path in the resolved commit, and refuses a mismatch before any
fetch. This composed test holds it to that, by gh and by curl: both
commits are named, "nothing ran", and the code repository is never asked.
Like every composed test, it skips in standalone code and fails rather
than skips under OPENREPOTOOLS_COMPOSED=1.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Install exactly the bytes that were checked from a mounted code leg, and read a pin's commented submodule_path the way pin_parse reads it.
Copilot's first round on #191 raised two points that this commit fixes.
First, the mounted-checkout path hashed each payload file in the checkout
and copied it later, in another pass. An edit in between could put bytes
in place that were never checked against the pinned commit. Each file is
now copied into the run's work directory first, the copy is what gets
hashed, and collect_commands and collect_skills read only that snapshot.
Second, pin_mounts compared the raw remainder of the submodule_path line.
A valid pin that comments the value, such as
"submodule_path: code # the mounted leg", was then not recognised as this
checkout's assembly, and the tree was installed as a plain complete tree
with none of the pin, gitlink or byte checks. It now reads the value with
pin_scalar first, and falls back to the lenient read only where that
grammar refuses the value. The new test fails at 6568f08 and passes here.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test that a branch which moves right after the layout question is read whole at the one commit it resolves to.
Copilot's second round on #191 read the PR body's "a ref that moved
between the probe and that commit is refused" as a rule that the code
broke, because a move from one adopted commit to another installs the
second. The code is right and the body overclaimed. The probe at $REF
only decides the layout, and its bytes are never installed. Everything is
then read at the commit $REF resolves to, so a move to another adopted
commit installs that commit whole, from the code it pins. A move to a
commit with no pin is refused as a ref that moved. The new test moves main
straight after the first request and holds both outcomes, with every
payload request naming the second commit's code.
Lane: openRepoTools-1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
readylanding gate: runs tests-macos once before the squash
2 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #186.
T009/T010 of specs/004-migrate-to-triad/tasks.md (branch 004-migrate-to-triad, PR #187).
Head:
b50f197(4 commits onc4864ac; 8 files, all undertests/). The fourth commit,b50f197, fixes lane 3's finding (1); the reviewer's LAND on it is review 5471080687.What changes
When the repository is adopted into the triad, the files split across three repositories:
README.md,AGENTS.mdandCLAUDE.mdmove to the assembly root.docs/moves to the spec leg.tests/,.gitmodulesandcontracts/openreposhape-pin.yamlstay in the code leg.upstream/openRepoShapestays nested in the code leg.The suite assumed one tree. It now names four:
OPENREPOTOOLS_CODE_ROOTOPENREPOTOOLS_ASSEMBLY_ROOTcontracts/code-pin.yamlandproject.yaml, is a repository's top level, and records the code root as the gitlink its code pin names; absent on today's layoutOPENREPOTOOLS_SPEC_ROOTcontracts/spec-pin.yaml'ssubmodule_path) when it is checked out; absent otherwise<code root>/upstream/openRepoShape, initialized or notOPENREPOTOOLS_COMPOSED=1is the composed switch. In either mode, the run is refused when a variable names the wrong kind of tree, or when the mode is anything other than 0 or 1.Which root a file is read from (
ROOTS.path_for(<rel>))README.md,AGENTS.mdandCLAUDE.mdcome from the assembly.docs/…,openspec/…,specs/…andideation/…come from the spec root.Today's single tree answers for all three, because it carries
openspec/andspecs/beside the code. On today's layout, every reader reads exactly the file it always read.Decision:
.gitattributesstays in the code legThe suite reads
.gitattributesfrom the CODE root. Git never applies a superproject's.gitattributesinside a submodule. I measured this in the composed candidate:README.mdat the assembly istext: auto/eol: lf.code/parkandcode/tests/run.shareunspecified.So the line-ending rule protects the bash files only through the code leg's own copy. Reading the assembly's copy would pass while the code leg went unprotected.
This matches T006: its leg patch
code-0001gives the code leg its own.gitattributes, and the assembly keeps its copy for its own files. Untilcode-0001lands in the real leg, the leg'stestsjob is red on this one test, by design.Follow-up for the plan's owner: the placement table in
specs/004-migrate-to-triad/plan.md(§ Target placement) on main still puts.gitattributesat the assembly only. It needs amending to say the code leg keeps its own copy before the real split. A leg split exactly as the table says is red on this test; the independent review saw this in its C5 and C6 runs.Length caps at an assembly root (T006)
AGENTS.mdREADME.mdThe higher caps allow exactly the 24 lines each that T006's
assembly-0001adds. Each cap has a dated entry. The entries citespecs/004-migrate-to-triad/root-guidance-2026-10-08.md, which is on #192's branch. Land #192 with or before this PR, or that citation points nowhere until it does.Readers rewired
tests/test_repo_hygiene.py:doc(), and their per-surface loops visit the code's surfaces first (code_first()).git ls-filesover every repository the run can read. Today that is the code root; composed, it is code, assembly and spec.tests/test_upstream_pin.pyreads the code root's owngit ls-tree HEAD, with the dependency nested in the code root. A new check confirms the code root is its repository's top level.tests/test_openrepotools_command.py: the installer's receipt-mode README check.tests/test_lane_helpers.sh: the two sections that quotedocs/README-lanes.mdused to swallow a missing file with|| :.b50f197: atdbc6837, the--retiresection's two manual assertions (:6601-6602) used$ln_docoutside the guard that binds it. With no spec root they aborted the suite underset -u(ln_doc: unbound variable); this was lane 3's finding (1). They now carry the same guard and named skip. A new text test,test_the_lane_suite_reads_the_manual_only_inside_its_guard, holds every use inside a guard.b50f197(5471080687) ran three proofs:dbc6837's suite: 1 failed, naming lines 6601 and 6602. Atb50f197: green.dbc6837and ends with three named skips atb50f197.dbc6837agrees:1 failed.tests/run.shmakes a relative root absolute before itscd. Nothing else in it changed.The composed-vs-standalone contract
Standalone is the default, and the only mode today's layout has.
Composed is
OPENREPOTOOLS_COMPOSED=1.tests/conftest.pyrefuses the session (pytest exit 4,ERROR: …), naming each problem, when:git status.In composed mode,
NEEDS_UPSTREAMbecomes a fixture that fails, and a document test that reaches an absent root fails. Nothing skips its way into composed acceptance (FR-008).What the conftest does NOT check: the pin files'
commit:andtree_sha256. Those belong to the standard'sscripts/validate-pins.py. The T010 assembly job runs it in the step before the composed suite (Copilot round 2; see Follow-ups).Evidence
Step 1: what the split breaks, before this change
I ran the suite at the rehearsed run C code leg
73b04d6, withtests/run.sh -k 'repo_hygiene or upstream_pin or wip_init or park_resume' -rfEs. Run C is main837928asplit by the standard's adopter into disposable local remotes (spec593250d, assembly69b8924).The same 17 tests failed in all three runs, each with a
FileNotFoundError:README.md;CLAUDE.md;AGENTS.md;docs/README-lanes.md(the hygiene document tests);.gitattributes(test_the_root_carries_the_line_ending_rule).test_upstream_pin.pypassed in all three, because it already read the code leg's own gitlink. M3's skips were today's dependency skips. Outside those selectors, the installer's README check and the lane suite's twodocs/README-lanes.mdsections read moved files too. The lane suite had swallowed the missing file with|| :. This PR covers all of them.Local proof in the triad
I applied the PR head onto run C's code leg and pinned it into the assembly with the standard's
bump-leg.py. The validators,make validateandmake pinsall exited 0.I ran
-k 'repo_hygiene or upstream_pin or roots or composed or openrepotools_command or lane_helpers_suite'four ways:The failures are recorded here, not deleted:
.gitattributes. This is the decision above.ln_doc: unbound variable. That is the defect above. I first attributed this failure to the stamp assertions alone; lane 3's review caught it.These proofs ran on
c139e88's content and were not repeated one3a76b5ordbc6837. The independent review's fake triad (C1–C6) exerciseddbc6837composed and standalone. The full record isspecs/004-migrate-to-triad/test-roots-and-ci-2026-10-08.mdon004-migrate-to-triad-ci.Red at base: 21 of 21
All 21 test cases in
tests/test_test_roots.pyfail atorigin/mainc4864ac.Round 1. I copied the module as of
c139e88(sha256be4edff7…) unchanged into a clone ofc4864ac.tests/run.sh -k rootsgave 18 failed: every test in the module. The 3 passes are tests in other modules whose names containroots.Seven of those failures are behavioural, and each fails on what it asserts:
1111….asmthrough as a relative path.The other 11 are resolver tests. They fail because
conftest.resolve_rootsandROOTSdo not exist at the base.Round 2. I copied
dbc6837's module (sha256aeb4b742…) the same way and ran-k 'length_caps or missing_manual or status_cannot_be_read'. That gave 3 failed: the three tests added since round 1. The independent review copied the same module whole intoc4864acand got 21 failed.How the module works: it runs the suite itself, nested, against fixture triads built under
tmp_path. Each fixture has:contracts/code-pin.yamlgrammar, pins recomputed from the fixture trees, andproject.yaml;It never touches a real repository.
Green
tests/run.sh -k 'repo_hygiene or upstream_pin or roots or composed'gave 191 passed, 0 skipped atdbc6837and on its merge onto main63810dd. That was the independent review's run. My own earlier runs gave 188 atc139e88and 189 ate3a76b5.CI on
dbc6837(run 37869489919, merge5ad6205):63810ddteststests-no-submoduletests-windowsguard-launch-modeparse-macosThe one extra
tests-no-submoduleskip is the composed positive control, which skips with today's reason.tests-macosis running underready(run 37873415160).How the local runs were made
The workstation-wide
pgrepcensus intests/run.shdid not reach zero on Eagle for some runs. Under the coordinator's interim rule, any run that had not started within 5 minutes was replaced by run.sh's exact pytest line underflockon the same lock file. The logs say which runs those were, and the deviation is noted on #184. CI is the record.Reviews
e3a76b5, and both threads resolved.git statusthat fails is now a refusal..gitattributes.dbc6837.dbc6837, with three things to fix before the squash.$ln_docabort, confirmed above, with the fix ready.<assembly>/worktrees/<feature>/codefinds no assembly.-q.dbc6837raised two findings. Both are recorded as dated follow-ups, not pushed here, because the macOS gate is running.commit:. Reproduced: withcontracts/code-pin.yaml'scommit:moved off the gitlink,ROOTS.refusal()returnsNone, whilevalidate-pins.pyexits 1 withpin-gitlink-mismatch. That validator runs before the suite in the T010 job. Answered in the thread._carriesaccepts a directory where a marker file is expected.Follow-ups (dated 2026-10-09, recorded on #186; not in this PR)
Review findings 11 and 14 have commits on branch
feat/test-roots-followups(6e63083,2064390, ondbc6837). That branch is not part of this PR.test_repo_hygiene.py:994,:1052and:1264, a code-only assertion follows the firstdoc()read, so a standalone code leg skips them together with the documents.6e63083asserts each mixed test's command half first. That coverspark's header,status's--no-optional-locks, the installer'slanes [--all] [--fetch], the exit-3 table and the wrapper's lock claims.2064390proves it with a nested run against a fixture leg whose commands lost those lines.badbranch was exercised locally:ok/bad,LANE_MANUAL=/nonexistent/README-lanes.mdprintsFAIL the lane manual is where the run's roots put it, and an empty value skips.6e63083adds seven lines to "Testing your changes" naming the roots, the three variables and the composed switch. Its AGENTS cap moves 316 → 323 (340 → 347 at an assembly root), with a dated entry.commit:differs from the gitlink.feat/test-roots-manual-guard@b50f197, unless it goes into this PR first.-qcannot hide it.Roots.owner_ofmapsLICENSEand.gitignoreto the code root. This is latent, because no test reads either.abs_rootturns a quoted~/xinto$PWD/~/x.GIT_ENVcomment says "file transport allowed".Not in this PR
The CI patches of T010 apply to repositories that do not exist yet:
tests.yml;composed.yml), becausevalidate.ymlis a pinned shape copy.They are on
004-migrate-to-triad-ci, with their local proofs..github/workflows/tests.ymlon main is unchanged.🤖 Generated with Claude Code
Summary by Sourcery
Make the test suite operate safely across the current single repository and the adopted triad by resolving ownership explicitly and refusing incomplete composed runs.
New Features:
Bug Fixes:
Enhancements:
Tests: