Skip to content

Add project clean --all-safe, a safe batch worktree cleanup (feature 004-project-clean-all-safe) - #33

Draft
brettheap wants to merge 537 commits into
mainfrom
004-project-clean-all-safe
Draft

brettheap wants to merge 537 commits into
mainfrom
004-project-clean-all-safe

Conversation

@brettheap

@brettheap brettheap commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Lane: openRepoProject-2

Closes #9

Realizes the ratified OpenSpec change add-project-clean-all-safe (PR #11, landed as 0050f3e on Brett Heap's word "ratify, merge #11 and run the runbook", 2026-10-09) as Speckit feature 004-project-clean-all-safe.

What changes

  • Foundation (Phases 1 and 2, T001 to T035): the git-floor job in .github/workflows/tests.yml (T003) builds Git 2.36.6 from kernel.org's tarball, checks its sha256 and runs -k test_clean_ -k test_status_ -k test_doctor_; the test job is unchanged. project gains named regions (T004) for the timing table, the bounded runner (child handle, one-wide driver, scoped signal handlers), the five incremental parsers, the 31-name environment scrub (17 names and the 14 trace names of Q9 B, R-99 (b)) with the protocol and status pins, path escaping, the evidence model, and the internal-error guard.
  • US1, retire every safe worktree in one run: project clean ROOT --all-safe previews one plan, the selected worktrees first, then the excluded rows grouped by reason with each group's next step; with --apply it builds, prints and confirms its own fresh plan with one default-No question (Remove N worktrees, keeping N branches? [yes/N]), removes the selected worktrees in canonical order and keeps every local branch at its planned SHA. --yes skips the question; --expect-plan DIGEST refuses with plan-digest-mismatch when the fresh plan's digest differs; no terminal and no --yes refuses confirmation-required; bad flag combinations refuse invalid-arguments. Code: clean() as a fixed pipeline (T047) and its stages (T048 to T057).
  • US2, work not proven disposable is never removed: a worktree is removable only when local ancestry proves its branch merged, its reflog shows that work began on it here, and nothing in it is hidden from Git's own check; every other row is kept with its first failing gate and next step. This closes three holes in today's single-target removal: an edit hidden by assume-unchanged, a worktree holding a submodule, and a freshly created published worktree with no commit of its own (hidden-local-state, contains-submodule, unstarted-branch, reflog-unavailable). Code: T066 and T067.
  • US3, a named action gains the batch's checks: --apply --action remove --worktree P becomes a one-item batch with the same plan, gates, stages, refusal codes and exit statuses; it removes P beside a row it cannot read, among seventeen eligible rows and past the row cap, and removes a merged worktree whose upstream was deleted (Brett Heap's ruling of 2026-10-09). Push and delete-branch keep their flags; push refuses a branch whose upstream was deleted (remote-gone) and an incomplete inspection; both print their paths escaped and neither child is signalled (the push child's pins are LQ-3 below). Code: single mode in build_plan() (T075), push_or_delete() (T076), execute() and run_bounded() (T077).
  • US4, a change after the plan stops the removal: before each removal a targeted check of at most six Git children, seven where row 4c reads extensions.worktreeConfig true, and nine or ten where the path of the root or of the common directory holds a line feed (RI-3, below), compares the identities and evidence recorded at planning and refuses at the first difference; the rescan after a removal re-establishes the repository first and reports a branch race without losing a commit. No lock is claimed: the confirmation and the result each state the residual window in one line. Code: T084 and T085.
  • US5, every removal ends in a reconcilable record: each selected target ends in one stage with one reason and, unless it was not attempted, a reconciliation record, and is removed only on rescan evidence. A started removal is never interrupted (5 s floor, 300 s ceiling, a wait line); the result names what was removed, what was not attempted, every branch kept and the next command (the stop block). Code: the removal stages (T091 to T094), with a tick hook in run_bounded() (R-83 P-1).
  • US6, every run is bounded and says what it omits: one invocation deadline (60 s with a 10 s reserve; a run with no removal in flight ends within 64 s), per-child budgets of min(5 s, work remaining), row caps (128, and 256 for the plain report) and a limit of 16 targets per run; a plan that meets the deadline, a budget or a row cap is incomplete, says so and blocks apply, and plain project clean now exits 1 when its report is incomplete. Rows past the sixteenth are deferred to the next run (deferred-target-cap) with the plan complete. Code: T099 to T102.
  • US7, acting on exactly the repository given: every clean mode resolves its argument Git-first to one repository and its main worktree, and refuses what is not a repository root with target-not-repository-root (today an absolute path can be redirected to a family holder or a manifest ancestor, and a bare repository's worktrees can be removed). The merge target is found in a fixed, recorded order, with the manifest read bounded at 1 MiB. Code: T107 and T031.
  • US8, exact paths and one JSON document per run: a path holding a line feed, a control or bidirectional character, or bytes that are not UTF-8 is reported exactly (the $'...' form in text, path_valid_utf8 in JSON) or excluded as unsupported-path-bytes, never misread and never passed to a removal. Every clean --json run but a usage error or an interruption before the apply phase prints one versioned document, and with --apply the human plan and question go to standard error. Code: json_path() and human_path() (T017).
  • US9, status, doctor and update share the evidence: they inspect repositories through the one bounded evidence model on Git 2.36 or later (version checked once, the caller's Git variables scrubbed, the status configuration pinned, lazy fetches refused locally, every child stopped and reaped on every exit path). clean refuses an old or missing Git; status, doctor and the update report show an inspection-error row instead of refusing; a shape or bench update refuses a dirty that is not exactly false. Code: T117 to T120.
  • README.md and AGENTS.md (T122, T123): the "Clean up Git worktrees" section opens with the five behaviour changes users will notice (plain project clean exits 1 on an incomplete report; clean refuses below Git 2.36; --worktree P refuses a freshly created merged worktree as unstarted-branch and one whose reflog keeps no decisive entry as reflog-unavailable; --worktree P removes a merged worktree whose upstream was deleted, keeping its branch), then the preview and apply, the gates, the 16-per-run deferral, the result record and recovery; Install states Git 2.36; the exit-code line gains clean's 1, 143 and 129. AGENTS.md gains Git 2.36, the clean tests and a section, "Cleaning up worktrees for a person".
  • Retired helpers (T125, 78cbde8): default_branch(), ancestor(), cleanup_report(), print_clean_report() and matching_worktree() are removed as baseline helpers that no caller reaches, the list lead ruling R-34 names. cleanup_signature() keeps its caller and stays.
  • Change 1's tasks.md (L1, R-377; doc round 25, ecd0a7e): 004 no longer edits openspec/changes/add-project-clean-all-safe/tasks.md; its copy equals main's (15504f1). Three of 004's four hunks there lay on lines OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 rewrote, so the PRs conflicted at landing (R-366, R-368); OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's round (p) carries the four, and 004's tasks.md records it in its Path Conventions (:109-121) and T133's rewritten verify. The landing, the archive and PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's ruling belong to the change's own governance record.
  • Next steps and refusals that Git accepts (lane 3's W52, F-2 to F-10, and W59's G-4 and G-5; R-259; merged as d764c26). Each told the user to do what Git refuses (reproduced on Git 2.43.0 and 2.36.6), and each string equals OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's at 6bf9f77 where OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 states it (W65, R-269). F-3: registration-mismatch names the repository's config and the worktree's config.worktree. F-4, F-5, F-6 and F-10 have their texts under Departures. F-7, F-8 and G-4 add --force for a worktree that holds a submodule (Q4 (c) adds what to keep first, below); F-9 with G-5 adds --quit where --abort fails for a marker with nothing in progress. F-2, in its own commit 64ad453: a locked row's partial-removal note says to unlock it first.
  • Revalidation's deadline arms (writer B, R-241, merged as 8f6fa31; W49 LOW-3, R-266, merged as b60792c): nine cut-at-the-deadline tests, four more cases and one mutant-killing test, in tests/test_clean_revalidation.py.
  • Brett Heap's F-1 and O-1, and OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 round (h)'s strings (writer G, merged as 7164d7f and 3bc1d68; R-274, R-292, R-295). F-1: after a ceiling kill the rescan reads the target's .git by one lstat, and the note says inspect, then git worktree prune and delete PATH by hand, the --force form where the registry entry and .git are both present, or, where .git cannot be read, a third note that says not to prune ("R-284: yes", R-290; "R-324: yes", R-343). O-1: a gate-7 reflog read that the work deadline cuts ends the plan deadline-exceeded, in single mode too (R-283, R-295). operation-in-progress's and inspection-incomplete's steps gain what to delete or unset by hand (R-287 (a)). R-365's build and the Q-build change these notes' texts, below.
  • The redirected main worktree's message (R-286; 2707ff2, merged as 60631eb): a repository whose main worktree is redirected by core.worktree or core.bare is refused at resolution with a message that says to unset both in the repository's config, or in the main worktree's config.worktree, then run again (the old message told the person to pass the repository root, R-271); five tests, and W81's three (1061dd7, merged as 769215d, R-302).
  • LQ-11's work-tree probe reads one path (W70 M-2, R-287 (b); 4dfd6f5, merged as fc0019e), so a linked worktree whose path holds a line feed no longer reads registration-mismatch under extensions.worktreeConfig true.
  • Unread rows, T1 to T8 (ruled by Brett Heap, below). git worktree prune, git worktree remove of that row and, after three months, git gc delete the administrative directory of a worktree whose path Git cannot read, so while any plan row's path could not be read every next step that named prune drops it or warns against it, ending do not run git worktree prune, git worktree remove or git gc until it can (nine TEXT entries). Built in e569564, dfc2baa and e8dda2a (R-312, R-327, R-343), with real-Git subtests for rows 4c and 4b (2057c31, R-344). Q3 (b) adds the dry-run suffix to the plain forms.
  • Closed standard output (Codex's one review of Add project clean --all-safe, a safe batch worktree cleanup (feature 004-project-clean-all-safe) #33, P2 on project:6600, R-322; 42454cb, R-326): an --apply --json run with standard output closed at startup removed worktrees and exited 0 with no JSON; it now refuses internal-error, exit 2, before any removal. Six tests (R-331).
  • W90's M-1 and rows 4b and 4c (R-329 to R-338; 488d6f6, c8e23cd, b3f7e0b): where a directory above the target denies search the .git counts as unread, so R-284's third note prints; rows 4b and 4c read an exit 1 as unset only when Git printed nothing, as OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 D7 says; 7 tests.
  • Doc rounds 17 to 24, docs only, each with its verifies passing: merged as 0570efd, 41dcdd2, 4916642, 4b4c36b, 0a82a62, 211e43d, d6bac50 and f3724fc (R-256, R-272, R-308, R-317, R-331, R-340, R-355, R-361); the last fixed T030's spy clause and T128's bound and noted that the hand runs are owed again at the landing head.
  • R-365's build, F-1's (b) and T9 (Sonnet; 7a20789, merged as d8724e2, R-372, R-376), on Brett Heap's answers, "Yes, warn (Recommended)" and "Yes, add T9 (Recommended)" (R-365). F-1's (b): where any row's path could not be read, the ceiling's note reads Probable partial removal: inspect, then delete PATH by hand; a row's path could not be read, so do not run git worktree prune, git worktree remove or git gc until it can. (one new TEXT key, none changed). T9: status's stale recommendation (repo_state(), shared by doctor and update) ends with the same warning beside an unread row. Three new tests fail at f3724fc and pass here; CI green (run 39953390038); the suite at d8724e2 gives 494 OK. Q3 (a) later replaced (b)'s text; T9 stands.
  • The A-items, the lane-fixable findings of the wave 22 triage and lane 3's reads (Sonnet; e702a65, R-373, R-374, R-385; CI green, run 39962712250). T7: a NUL byte in a linked worktree's .git file or admin commondir, or a lone surrogate as a manifest's tracking_branch, ended status in a traceback and refused every clean internal-error; now the row is inspection-error and the name no local branch. T10: a branch named detached is classified by its upstream. T19: a failed row 7 makes the row inspection-error, as pcl:178-179 says, not review-required. T14: unsupported-path-bytes' step is move it to a valid UTF-8 path with git worktree move, or mv it and then git worktree repair NEWPATH. T15, T16 and the LOW-5 tests are tests and one comment; T17 was already pinned. project and six test files, TEXT unchanged; every behaviour test fails at d8724e2 and passes at e702a65.
  • The Q-build, Brett Heap's Q1 to Q5, Q8 and Q9 (b), on the A-items (Opus; 703b58c, merged as 1579c22; R-380, R-395, R-398). Each is OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's text from round (q), 818311a (R-387, R-391).
    • Q1, 9d5c2fb: -c core.ignoreCase=false joins the status pins and the removal, so that under core.ignoreCase=true a file that differs only in case from a tracked one is no longer hidden, and git worktree remove no longer resolves a path to another worktree that differs only in case.
    • Q2 and Q9 (b), 5eeaedd, 338edb0, 703b58c: a linked row whose path is, or encloses, the common directory, the main worktree or another row, or whose registered path is a symbolic link, is registration-mismatch; contains-submodule takes an entry named .git that one lstat finds in any directory holding an index entry, counted in the filesystem budget and run again at revalidation (step 7, nested_git_check()).
    • Q3, da5502d: (a) the ceiling's prune note names the target's own administrative directory, Probable partial removal: inspect, then delete PATH and its administrative directory ADMIN by hand., in every mode, superseding R-365's text (T9 stands); (b) every plain text that names git worktree prune ends ; git worktree prune also deletes the entry, and so the HEAD reflog, of every other worktree whose path is missing, one moved or on an unmounted drive included, so run git worktree prune --dry-run first and remount or repair any it lists that still exists.
    • Q4, c6daebc: the --force notes name their check first (check that git -C PATH status --short --ignored lists only deleted files, then git worktree remove --force PATH by hand; the ceiling's own note keeps inspect), and contains-submodule's step says to keep each submodule's commits first.
    • Q5, c28c9ea: gate 7's two steps name the checks their skipped gates would have made (git log -g HEAD, git ls-files -v and git status), and the non-drain line defers to each group's next step.
    • Q8, 70345ee: a status probe that exits 0 with a standard-error line other than Git's hint: lines, or with more than the 4 KiB kept, is probe-failed, so a repository holding refs/heads/HEAD has every row inspection-error.
    • Q6, Q7 and Q9 (a) are Risks entries and a residual in OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19, and Q9 (c) scopes OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's text; 004 builds nothing for them (R-380).
  • Doc rounds 25 and 26, docs only (Sonnet; ecd0a7e, R-379; then 8e90096, 8abd901 and 497f5a3, R-393, R-394, R-397; merged as 41759a4): round 25 documents R-365's two texts and L1's 004 half; round 26, the A-items, Q1 to Q5, Q8 and Q9 (b) (Parts A to C), then revalidation's nested .git checks and round (r)'s two scenarios, rows 123 and 124 (Part D). project and tests/ at 41759a4 equal 1579c22's.
  • Speckit files under specs/004-project-clean-all-safe/: spec.md, plan.md, research.md, data-model.md, contracts/project-clean.md, quickstart.md, tasks.md, checklists/requirements.md, traceability.md (124 rows, one per scenario of the three deltas: 100 on main and 24 of OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's), and hand-runs.md (T132's record, 0570efd).
  • Change 1's amendment, draft PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 (branch change/amend-clean-all-safe-push-pins), and what it brings into this build. OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 is at 4512441, its round (r) (R-395, R-396), and is not merged. Until OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 lands, 004 departs from main's copy of the change wherever OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 amends it; 004's strings equal OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's where OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 states them (R-391, R-396). OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 holds 73, 25 and 26 scenarios in its three deltas, and openspec validate --all --strict passes 9 of 9 at 4512441 (R-396). OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 brings in:
    • Gate 11, operation-in-progress (LQ-14), and gate 12, worktree-only-commits (LQ-13). Gate 11 follows hidden-local-state, and gate 12 is the last gate. Both run again at revalidation (R-121, R-124, R-127).
    • Q9 B, his earlier ruling, "yes, recommended for Q1-Q17" (R-99 (b)): GIT_TRACE2, GIT_TRACE2_EVENT and GIT_TRACE2_PERF are set to 0 on every Git child, and the scrub grows from 17 to 31 names with 14 GIT_TRACE names that Git 2.36.6 to 2.43.7 read alike (R-99 (d), R-103). Merged into 004 as ab10102 (R-123).
    • The revalidation rules (R-171, R-195, R-198, R-208). A read that decides nothing, or a Git child or filesystem read that fails or runs past its own budget while the deadline is not the limit, gives state-changed whatever its step; a decided value keeps its step's reason (a work-tree probe that names another toplevel is identity-changed; ENOENT or ENOTDIR decides that a path is absent); a read the deadline cuts leaves the target not-attempted with deadline-exceeded. contains-submodule or operation-in-progress outranks identity-changed (R-198). A target's check is at most six Git children, seven where row 4c reads extensions.worktreeConfig true (R-168, R-201), and nine or ten where the path of the root or of the common directory holds a line feed (RI-3, above).
    • The drain line. When all 16 rows taken past deferred-target-cap are excluded by a gate after it, the preview says these deferred rows will not drain while the 16 rows ahead of them are REASONS; follow each group's next step for those worktrees, REASONS naming those gates' reasons, each once, in gate order. It named only contains-submodule before (R-146 L-f; 310f97d, R-201), and then said review those worktrees, then remove them yourself (--force for any that holds a submodule), until Q5 (c28c9ea).
    • The relative or ~ HOME refusal (LQ-15, R-205). Before any Git child, git --version included, clean, status, doctor and update test HOME and XDG_CONFIG_HOME for a non-empty value whose first byte is not /. clean refuses with relative-config-path, exit 2, and the others report it as they report a Git below the floor. A HOME that begins with ~ fails the test, and nothing is read or expanded against it. R-205's two accepted departures are under "Departures for Brett Heap's word" below. GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM are the lead's extension, which he kept (O10 (A), R-370; see the residuals below).
    • Rounds (p) to (r) since 88d0d36 (R-378, R-387, R-396). (p), e0b1671: R-365's rule, T9 in D18, and L1's four tasks.md hunks. (q), 818311a: Q1 to Q9 and a project-command clause for T9, so that the parity rule (pc:289-290: baseline values stay, except where a requirement states a change) holds. (r), 4512441: revalidation re-runs Q2 (2)'s <dir>/.git lstats, a stating requirement for clean's stale recommendations, and M1 and M3 in Open Questions until Brett Heap answers. Round (s) owes the D7 phrase an entry found being a new .git entry, the Open Questions copy of M1 that says "parity rule FR-064" (to become pc:289-290's), and his M1 and M3 (R-396).
    • README.md and specs/004-project-clean-all-safe/ follow all of this (doc rounds 13 to 26, the last 497f5a3, above).

Departures for Brett Heap's word

R-numbers are the lane's lead rulings, cited as in specs/004-project-clean-all-safe/; rulings:N is line N of the lead's rulings file, lead-rulings-alignment.md, which is the lead's working file and is not in this repository; the R-numbers and the quoted texts are enough to follow each question, and the rulings:N pointers are for the lead. Where the change's text and the build differ, the change's text is given as path:line.

  • R-32, the test layout.
    • Ruling: R-32 (rulings:201); R-32a to R-32c (rulings:202-204).
    • Departs from: openspec/changes/add-project-clean-all-safe/design.md:971 (Migration Plan step 6, "Tests in tests/test_project.py: one per delta scenario, named after it") and openspec/changes/add-project-clean-all-safe/proposal.md:625 (Impact, "tests/test_project.py: each ADDED and MODIFIED requirement has a test named after its scenario").
    • The build: the new tests live in 20 modules tests/test_clean_<lane>.py and in tests/test_status_doctor_update.py, with helpers in tests/clean_support.py and the launcher in tests/launch_project.py; tests/test_project.py changes in one method (T046, test_clean_revalidates_a_worktree_after_confirmation) and one loader line (T009, the sys.modules registration). Stated at specs/004-project-clean-all-safe/tasks.md:48-53, plan.md:348-356 and research.md:664-700.
    • If he declines: the new modules are folded into tests/test_project.py before the merge.
  • AGENTS.md (T123), outside the proposal's Impact.
    • Ruling: R-32 (rulings:201: plan.md, research R15 and quickstart follow tasks.md on AGENTS.md (T123)).
    • Departs from: openspec/changes/add-project-clean-all-safe/proposal.md:598, :612 and :625, the Impact bullets for project, README.md and tests/test_project.py; nothing in the change names AGENTS.md. Stated at specs/004-project-clean-all-safe/tasks.md:54-56 and plan.md:352-354.
    • The build: AGENTS.md gains Git 2.36 and the clean tests with their commands under "Runtime and verification", and the section "Cleaning up worktrees for a person"; the OPENSPEC-SPECKIT-GLOBAL block and CLAUDE.md are untouched.
    • If he declines: the AGENTS.md hunk (T123) is dropped before the merge (the lead's proposed fallback).
  • R-35, CI on Linux only, by Brett Heap's decision of 2026-10-15 (ci: run the test suite on Linux only #32, e9d6711).
    • Ruling: R-35 (rulings:208) and R-46 (rulings:239).
    • Differs from: openspec/changes/add-project-clean-all-safe/proposal.md:651-652 ("on Linux and macOS with Python 3.10 and 3.12, as CI runs"; :648 and design.md:55 also name macOS). The change text stays as written, since ci: run the test suite on Linux only #32 leaves historical records under openspec/ and specs/ alone.
    • The build: every job of the test matrix runs on ubuntu-latest only, and the feature's text counts no jobs (R-46); the git-floor job runs on ubuntu-latest with Python 3.10 and Git 2.36.6.
    • If he declines: not applicable, since the difference is his own decision; this PR only notes it (specs/004-project-clean-all-safe/tasks.md:58-64, plan.md:357-362).
  • R-27, what probes and operations count.
    • Ruling: R-27 (rulings:185), read by R-57 L4 (rulings:325) and R-59 D10 (rulings:347); specs/004-project-clean-all-safe/research.md:368-406 and spec.md:1805-1815.
    • Departs from: openspec/changes/add-project-clean-all-safe/specs/project-clean/spec.md:868 (scenario "Bounded revalidation work": "the record's probes.performed equals the wrapper's count"), which read literally includes the three removals.
    • The build: probes.performed counts every Git child other than a removal child and operations.performed counts the removal children (3 in that scenario), each child once, as the packet counts them (ideation/brainstorm/project-maintenance-batch-cleanup.md:816-830 at da33d92). It awaits his word through draft PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's commit cecb4fc, "State which count holds the removals".
    • If he declines: the literal text holds, probes.performed counts the removals too, and the counters and the assertions of T096 to T098 and T102 follow.
  • Two more departures are open as questions below: the push child carries none of the protocol pins (LQ-3), and a bare repository's registry entry is left out of status's worktrees (LQ-9, widened below, which T117 now calls two departures).
  • R-21, the Git floor kept at 2.36 with GIT_INTERNAL_SUPER_PREFIX added to the scrub (openspec/changes/add-project-clean-all-safe/proposal.md:766-776, a departure from task 2.2's rule that the floor is raised to the lowest version verified), and the bounded runner (proposal.md:795, a departure from BA:1212-1213): both stand in the proposal's departures list, which Brett Heap ratified with OpenSpec: add-project-clean-all-safe (batch worktree cleanup from the maintenance design packet) #11's merge. The build keeps both as the change states them.
  • R-205 (a), a ~ target refuses under a HOME that fails the relative-path test (lane 3's W30 M-1 found the crash, R-202).
    • Ruling: R-205 (a) (rulings:1576), on R-202 M-1 (rulings:1563); lane H's addendum 1 at 22f95d1, merged as 0b7dd94 (R-206, R-209; rulings:1587-1588).
    • Departs from: OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's rule that status, doctor and update report a failed configuration-path test as they report a Git below the floor (design.md:845-848, and proposal.md:317-322, at f0bf538), which names no target argument that needs ~ expanded; and the baseline, where HOME=~ or HOME=~/x crashed status, doctor and update with RuntimeError: Could not determine home directory. (rulings:1563).
    • The build: a target argument that needs ~ expanded, ~ alone or ~/..., refuses with relative-config-path and the message about HOME, exit 2, with no Git child; ~user is read from the user database and expands as before. A HOME of ~, ~/x or ~user fails the test like any relative value, and nothing is read or expanded against it (home_directory(), expand_user(); contracts/project-clean.md:728-729 at 4f921f8).
    • If he declines: R-205 records no fallback. The crash stays fixed either way, since it was a defect (R-202 M-1). Only the exit-2 refusal of this target is in question.
  • R-205 (b), new refuses under a HOME that fails the test.
    • Ruling: R-205 (b) (rulings:1577): "new is outside LQ-15; a defensive refusal is safer. Record it in Add project clean --all-safe, a safe batch worktree cleanup (feature 004-project-clean-all-safe) #33's body for Brett."
    • Departs from: OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's LQ-15 scope at f0bf538, which names only clean, status, doctor and update (proposal.md:957-983); and the baseline, where new raised a traceback or created the project under a relative h/projects.
    • The build: new with no parent argument, no --into and no PROJECTS_DIR, under a HOME that fails the test, has no default parent and refuses with the message about HOME, exit 2 (contracts/project-clean.md:747-751 at 4f921f8).
    • If he declines: the refusal is dropped and new is as at the baseline, a traceback or a project under a relative h/projects.
    • Open LOW, no fix: new --into ~/x under a ~ HOME still raises a traceback. It is outside LQ-15 (R-205's LOW follow-up, rulings:1578; contracts/project-clean.md:752-753 records it).
  • F-1, F-2 and O-1, amendments of ratified text, ruled by Brett Heap on 2026-10-22 (R-263, rulings:1719-1722), verbatim "F-2: yes; F-1: prune-then-delete; O-1: deadline-exceeded"; he added "R-284: yes" for F-1's note on an unreadable .git (R-290, rulings:1869).
  • F-4, the printed -- in delete-branch, declared in the contract (W65's LOW-2, R-269, rulings:1742-1746; R-259, rulings:1701).
  • F-5, the refusal for a branch the main worktree holds, declared in OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 (R-248, rulings:1669; W59's G-2, R-253, rulings:1681-1683; R-258, rulings:1694-1696).
  • F-6 and F-10, 004's own report advice (R-259, rulings:1701; "not stated in OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 and are 004-only", R-251, rulings:1676).
    • Departs from: the baseline's report texts, which OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 neither states nor quotes (proposal.md:1858-1860): 15504f1:project:607 and :610 for the main worktree's merged rows ("may be removed explicitly", "switch away before removal") and :568 for a locked stale-worktree row ("Review and prune stale worktree metadata manually.").
    • The build: row_evidence() replaces the advice after the ladder, the class unchanged: The main worktree is never removed by project clean., and Review, unlock it, then prune stale worktree metadata manually., since git worktree prune keeps a locked entry (contracts/project-clean.md:402-409); Q3 (b) later adds the dry-run suffix to both stale recommendations. If he declines: the baseline texts return.
  • RI-3, F-7 and F-8, amendments on lane rulings that wait on his ratification of OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 (lane 3's W70, M-1 and M-3, R-285; OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's round (j) groups them, with the cap's re-run, R-187 and R-190, and the count split above, at proposal.md:1243 at 173d83c, and quotes RI-3's departure at :1167, R-293).
    • Departs from: RI-3 (R-259, rulings:1701), where the path of the root or of the common directory holds a line feed (git rev-parse has no NUL form): a revalidation spawns up to nine children (ten with extensions.worktreeConfig true) and the final rescan up to six, against BA:679 (at most five children), BA:726-727 and BA:743 (three for the final rescan); it is W70's one new DEPARTS. F-7 and F-8 (R-248, R-252) change ratified next-step texts to add --force.
    • The build: as under "What changes". If he declines: no fallback is recorded in the rulings.
  • T9, a departure from FR-064, and F-1's (b), RULED by Brett Heap on 2026-10-23 (R-365, rulings:2125): through the question tool, answered 2026-10-23T11:49:16Z, "Yes, warn (Recommended)" for F-1's (b) and "Yes, add T9 (Recommended)" for T9.
    • Departs from: for T9, FR-064 (specs/004-project-clean-all-safe/spec.md:1863-1886 at 497f5a3, its dated notes; R-399), which keeps baseline functions unchanged, and OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's parity rule (pc:289-290: baseline values stay, except where a requirement states a change); F-1's (b) amends the ratified F-1 text (pcl:253-256, pcl:922-926), under the F-1 entry above.
    • The build: status, doctor and update end the stale-entry recommendation, "Review the stale worktree metadata before pruning it manually.", with ; a row's path could not be read, so do not run git worktree prune, git worktree remove or git gc until it can. beside an unread row; with every path readable it is the baseline's text byte for byte (7a20789; declared in contracts/project-clean.md; stated in OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's round (q), R-380). Q3 (a) later replaced F-1's (b) text; T9 stands. If he declines: not applicable, he ruled.
  • Q1 to Q9, RULED by Brett Heap: "yes, recommended for Q1-Q9" (2026-10-23T13:30:27Z; R-380, rulings:2161). Each of the nine stated a lean (wave22/outputs/triage.md, section 3), so each is taken as recommended. They amend ratified text of the project-clean, project-command and project-clean-review-safety deltas, as OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's round (q) restates it; none is a departure from OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19.
    • The build: Q1 to Q5, Q8 and Q9 (b), above. specs/004-project-clean-all-safe/spec.md records each changed behaviour in a dated note under FR-054 and FR-011 (Q1), FR-055 (Q2 (1), Q9 (b)), FR-023, FR-046 and FR-047 (Q2 (2)), FR-028 (Q3, and Q4 (c)'s and Q5's texts), FR-009 (Q4 (a), (b)), FR-020 (Q5, Q4 (c)), FR-032 (Q5) and FR-057 (Q8); FR-064's note records that Q3 (b)'s warning is not given to status (M1, below). If he declines: not applicable, he ruled.
  • Checked for further accepted departures that stand: every ruling from R-98 to R-400. LQ-10 to LQ-12 are OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's text now, none a departure (R-102 (c) 5). The lead's file-list and region exceptions for lane writers concern who edited what, not the change's text. R-205's two and the entries above stand, with two that OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's round (f) declares against the packet (R-245, R-247): 19-10 (BA:271-272; a revalidation read or child that the work deadline cuts counts as no difference, proposal.md:1113-1126) and 19-11 (BA:424-425; push refuses a wider set of upstream mappings with the baseline's message, proposal.md:1127-1135). 004 builds both as OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 states them, and with F-5 they take effect only when he ratifies OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 (R-258).

Ratified with #11 and not reopened here: V1, V4, V5, V10, OQ-23, OQ-24, R-4, R-8, R-9, R-21 and the runner (the departures record, rulings:183).

Open questions for Brett Heap

  • LQ-2, squash-merged branches. Question: may a local patch-equivalence proof (git cherry or patch IDs against the merge target) count a squash-merged branch as merged? The build now: ancestry alone proves a branch merged, so a squash-merged branch is never selected and a squash-merging repository's batch selects little (specs/004-project-clean-all-safe/spec.md:780-786, EC-001; :1780-1788). Recommendation: a local patch-equivalence proof as a follow-on change, not in this feature (openspec/changes/add-project-clean-all-safe/design.md:999-1001; proposal.md:1001-1005). Rulings: V1 to V3 (rulings:104), the naming note (rulings:181), the departures record (rulings:183).
  • LQ-3, the push-child exception. Question: the ratified text says every Git child carries the seven protocol pins (openspec/changes/add-project-clean-all-safe/specs/project-command/spec.md:113-117; design D11), but the retained push needs its child to reach a remote, and a pinned push ends fatal: transport 'file' not allowed, exit 128; so (a) amend the unarchived change by draft PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 (branch change/amend-clean-all-safe-push-pins), or (b) a follow-up change after the archive? The build now: the push child runs through execute() with none of the seven pins, in project's own process group, unbounded, never signalled and waited for, and keeps the caller's GIT_ALLOW_PROTOCOL; the other 30 of the 31 scrubbed names stay removed from it, and it carries the three trace2 pins at 0; every other Git child, the delete-branch child included, keeps the 31-name scrub and the pins (specs/004-project-clean-all-safe/research.md:32-101; contracts/project-clean.md:601-603). Recommendation: (a). If he rules (b), only the push child's treatment moves: T074, T077 and the push halves of T076 and T073 (specs/004-project-clean-all-safe/tasks.md:4216-4219). Rulings: R-28, R-31, R-80 (P-9 to P-11 and P-14; no ruling), R-81 (b). PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 is an open draft at 4512441.
  • LQ-4, the preflight's scope. Question: does "the first target's repository-wide checks SHALL serve as the preflight of every selected target" (openspec/changes/add-project-clean-all-safe/specs/project-clean-review-safety/spec.md:94-96; scenario "New worktree and pre-removal changes", :134-141) also cover a later target's per-worktree ignored-file state and classification before the first removal, which the child budgets of D7 (at most five per target in main's copy, six, or seven where row 4c reads true, in OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's) do not provide? The build now: the first target's revalidation compares every selected target's registration, lock, filesystem identity, branch tip and upstream object, and the merge-target SHA; a difference on any target refuses the FIRST target with the matching reason, removes nothing and leaves later targets not-attempted with batch-stopped, and the refusal does not name the differing path; ignored-file state and classification are checked in each target's own revalidation before its own removal (specs/004-project-clean-all-safe/research.md:702-755). Recommendation: none given; the build follows R-41's reading. Rulings: R-41 (rulings:214, :227), R-77, R-80 (P-9 to P-11 and P-14; no ruling), R-82.
  • LQ-5, the remedy reading. Question: does the delta's rule that every plan-blocking refusal names git worktree prune, git worktree repair or removing the offending row by hand "as the cause fits" (openspec/changes/add-project-clean-all-safe/specs/project-clean/spec.md:649-651) let a cause that no repair fixes name its own remedy? The build now: D18's deadline-exceeded remedy, "the run ran out of time; remove worktrees explicitly or re-run", names none of the three and stands (specs/004-project-clean-all-safe/contracts/project-clean.md:340-345). Recommendation: none given; R-45 (7) keeps the reading. Rulings: R-45 (7) and R-80 (P-9 to P-11 and P-14; no ruling).
  • LQ-6, PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 and lane 5's extension clauses. Question: should PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's evidence-model rewrite absorb lane 5's three extension clauses (for OpenSpec: add-project-clean-branch-retirement (opt-in paired retirement of a removed worktree's local branch) #26's branch child and OpenSpec: add-project-overview-remote-merge-status (opt-in exact-head GitHub pull-request evidence in project overview --remote) #29's remote reads and gh child), or do OpenSpec: add-project-clean-branch-retirement (opt-in paired retirement of a removed worktree's local branch) #26 and OpenSpec: add-project-overview-remote-merge-status (opt-in exact-head GitHub pull-request evidence in project overview --remote) #29 carry their own MODIFIED blocks? The build now: this feature builds nothing for it and PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's text is not widened; OpenSpec: add-project-clean-branch-retirement (opt-in paired retirement of a removed worktree's local branch) #26's block is 16051d1 and OpenSpec: add-project-overview-remote-merge-status (opt-in exact-head GitHub pull-request evidence in project overview --remote) #29's is f66ed58, both written against PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's text at c462d87 and both still in their PRs (open drafts), and whichever archives second re-bases. Recommendation: none beyond the ruling, which keeps the two blocks separate unless he prefers that PR OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 absorb them. Rulings: LQ-6 (rulings:238) and the LQ-6 record (rulings:279).
  • LQ-7, the delta's two exit rules and the closed-output cases. Question: a write that fails in a preview or report after its plan is built meets two sentences of the project-clean delta (openspec/changes/add-project-clean-all-safe/specs/project-clean/spec.md) that disagree, :940-941 (an unexpected exception before the apply phase refuses internal-error, exit 2) and :958-960 (a preview or report exits 2 only for a refusal raised before a plan is built); which governs, and should an internal error after the apply phase begins print its exception's type on standard error? The build now (R-55): before the apply phase the write failure is internal-error, exit 2, nothing mutated; the first failed write points that stream at /dev/null; a recorded signal outranks it (130, 143 or 129 with Cancelled.); after the apply phase begins writes are best-effort and D19 decides the exit; with standard output closed, --action push exits 2 with REFUSED: BrokenPipeError: [Errno 32] Broken pipe and starts no child, and an --apply --json run with it closed at startup refuses internal-error, exit 2, before any removal (R-322, R-326); an unwritable question is internal-error, not cancelled; after the apply phase begins no exception type or text is printed (specs/004-project-clean-all-safe/contracts/project-clean.md:326-338). Recommendation: R-55's reading stands until he rules; none given on printing the type. Rulings: R-55, R-59 (D1 to D3), R-74 (c) (3), R-76 (g), R-80 (P-9 to P-11 and P-14; no ruling), R-81 (b), R-87 (c), R-91.
  • LQ-8, the command form's escapes. Question: should change 1's path rule write non-ASCII code points in a printed command as their UTF-8 bytes, \xHH, which every shell with $'...' reads alike in any locale, while the display form keeps \u and \U? The build now: it implements the delta as written (openspec/changes/add-project-clean-all-safe/specs/project-clean/spec.md:1061-1066), \xHH, \uXXXX and \UXXXXXXXX in text and commands alike, and documents the limit: the command needs bash, zsh, ksh93 or a POSIX.1-2024 shell, not dash, and a \u or \U escape needs a UTF-8 locale, since in a C locale it can name a different path or branch (specs/004-project-clean-all-safe/contracts/project-clean.md:378-395; quickstart.md:228-231). Recommendation: the \xHH amendment, not applied (R-61 (a)). Rulings: R-60, R-61 (a), R-80 (P-9 to P-11 and P-14; no ruling), R-83 (P-2), R-92 (a).
  • LQ-9, a bare repository's entry, and a gitfile layout's, in status's worktrees. Question: does the project-clean delta's "the common directory SHALL never be probed as a worktree row" (openspec/changes/add-project-clean-all-safe/specs/project-clean/spec.md:478) cover status's worktrees, so that leaving a bare repository's registry entry out is a fill, or is it a departure from the project-command delta's "Values SHALL equal those of the inspection this model replaces, except where a requirement states a change" (openspec/changes/add-project-clean-all-safe/specs/project-command/spec.md:196-197) that needs his word? Lane 3's F5 M-2 widened it (R-101 (e), rulings:960, :1012): for a gitfile layout, a separate git directory or a submodule's checkout, the git directory's record becomes the checkout's row, with its dirty and ignored_files read there, false and 0 where they were null; the lead accepted that as a fill too. The build now: T117 leaves a bare repository's registry entry out of worktrees (specs/004-project-clean-all-safe/tasks.md:3804-3820; plan.md:419-433). Recommendation: a fill, which the lead and lane 3 recommend, since :478 and data-model.md:18 both say the common directory is never a worktree row. Rulings: R-75 (a), R-81 (d), R-101 (e).
  • LQ-10 to LQ-15, RULED on 2026-10-21 (R-102, rulings:1042-1064): "LQ-15: yes, refuse relative HOME like OpenSpec: add-project-overview-remote-merge-status (opt-in exact-head GitHub pull-request evidence in project overview --remote) #29" in this lane's session (rulings:1043); "yes to all six recommendations, LQ-10 to LQ-15" in lane 3's (rulings:1044). Each is OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's text now, so none is a departure (R-102 (c) 5): the stat pins (LQ-10, ab10102), a redirected work tree read as registration-mismatch (LQ-11, 91552e3), update's owner tools run with the push child's scrub (LQ-12, 2439838), worktree-only-commits (LQ-13, 93a3ae9), operation-in-progress (LQ-14, e0c589a) and relative-config-path (LQ-15, 5794f91, with addendum 1 for a ~ HOME, 0b7dd94).
  • LQ-16, OPEN. Question: a clean or process filter that a repository's configuration defines (in its own file, an includeIf or a submodule) runs inside every read-only status probe: report, preview, status, doctor, revalidation, and a fresh clone alike. That is user code in a "read-only" probe. Should those probes neutralize every configured clean and process filter? Lane 3's reader RA checked the fix, an empty -c filter.<x>.clean= for each configured filter. It fails safe: a filtered file whose stat changed then shows as modified and the worktree is kept (R-105 H2, rulings:1092-1093). The build now: nothing; the probes run configured filters as Git does. The exposure stays under an absolute HOME (R-107, rulings:1120; R-125 L2, rulings:1238). Recommendation: yes, in the read-only probes only. The removal child keeps the user's own filters, since it is a confirmed mutation once LQ-15 holds (R-105 (b), rulings:1098).
  • LQ-17, OPEN. Question: the removal's own internal git status writes the index and fires post-index-change, and branch -d fires reference-transaction. Should the removal and branch children run with -c core.hooksPath=/dev/null, as RA's checked fix has it (R-105 H3, rulings:1094-1095)? The build now: nothing; both children run the repository's own hooks, as Git does and as the baseline did (e45155a:project:719 and :734, R-105 (b)), with no core.hooksPath pin. Recommendation: AGAINST suppressing hooks. A reference-transaction hook can veto a ref deletion and may be the user's guard on a branch, so suppressing it would bypass a protection, and post-index-change only informs.
  • LQ-18, OPEN. Question: should the push child pass --no-follow-tags and --recurse-submodules=no, so that the push does only what the plan shows? A configured push.followTags or push.recurseSubmodules would make it do more (R-108 (b), rulings:1127; R-130, rulings:1275). The build now: neither flag is passed, and the push child follows the repository's configuration as git push would; they are not built until he rules. Recommendation: yes, pass both.
  • LQ-19, OPEN. Question: a relative GIT_EXEC_PATH made worktree remove run a planted git, exit 0, which the amendment writer reported at its checkpoint (R-114, rulings:1174). Should LQ-15's relative-path test cover it? A PATH with an empty or relative entry is the same class, not recommended. The build now: nothing; the test covers HOME, XDG_CONFIG_HOME, GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM only, and OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 says other relative paths in the environment are outside it (design.md:850-852 at f0bf538). Recommendation: add GIT_EXEC_PATH to the test. It goes to him with R-132's question on update, in the residuals below.
  • LQ-20, RULED on 2026-10-22 (R-236): "LQ-20: go with (i), measure first". Measured (R-243): LQ-11's probe 8.7 ms and LQ-13's rev-list over 9,940 commits 160.3 ms, with the margin; the cap test passes, the 16th removal spawning at 30.0 s with 20.0 s left. OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's round (f) records it (662be89, R-247).
  • F-1, F-2, O-1, R-284 and T1 to T8, RULED on 2026-10-22 (amendments of ratified text, as under Departures; OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 records them from round (h)). "F-2: yes; F-1: prune-then-delete; O-1: deadline-exceeded" (R-263, rulings:1719-1722); "R-284: yes" (R-290, rulings:1869), for the third note where Git cannot stat .git, which his F-1 question had not covered (W73 M-1); "R-307: yes; R-310: yes" (R-319, rulings:1976) for T1 to T5; "R-320: yes" (R-321, rulings:1987) for T6 to T8; and "R-324: yes" (R-343, rulings:2084), which extends them: every no-prune warning ends "do not run git worktree prune, git worktree remove or git gc until it can", since git worktree remove and git gc also delete an unread row's administrative directory. OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 carries the texts in round (n) (R-345), 004 in e8dda2a.
  • F-1's (b) and T9, RULED on 2026-10-23 (R-365, rulings:2125): "Yes, warn (Recommended)" for F-1's (b), where a ceiling-cut P sits beside an unreadable sibling, and "Yes, add T9 (Recommended)" for status's stale-entry advice beside an unread row (the questions of R-310, R-324 and R-327). Built in 7a20789; Q3 (a) later replaced (b)'s text, and T9 stands (R-380).
  • O-2, OPEN, put to Brett Heap on 2026-10-22 as a reading to confirm (R-216, R-221; design.md:493-496 and proposal.md:1657-1661 at f0bf538). Question: R-208 reads BA:271-276, "a value that cannot be re-read, including a filesystem call that times out, counts as a mismatch", this way: a read that fails decides no identity difference, so its mismatch is the packet's "any other" difference, state-changed, for a Git child and for a filesystem read alike. Does he confirm it? The build now: it builds that reading (73d8888, merged as 9ab996e, R-218). The rules are under What changes (R-208, R-212).
  • M1, OPEN, put to Brett Heap on 2026-10-23T15:11:48Z (lane 3's W133 MEDIUM-1; wave23/brett-questions-w133.md). Question: do status, doctor and update, with no row's path unread, take Q3 (b)'s dry-run warning on their plain stale advice, "Review the stale worktree metadata before pruning it manually."? It names no command, so Q3 (b) did not reach it; lane 3 calls it a gap that needs his word, not a departure. The build now: the baseline text stays (FR-064's note, R-380). Recommended (a): no. It names no command and already says to review first; the warning matters most in clean, where the prune advice sits beside one row and reads as about that row alone; and keeping it adds no second departure, across three commands, from the parity rule (pc:289-290) and no change to the status rules lane 4's 005 reads. (b) Yes: all three end it with Q3 (b)'s dry-run warning (Q3 above). That is a second departure from the parity rule, and changes 004's code and tests, OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's pc:308-309, :394 and D18, lane 4's pin and lane 5's OpenSpec: add-project-overview-remote-merge-status (opt-in exact-head GitHub pull-request evidence in project overview --remote) #29 block.
  • M3, OPEN, put to Brett Heap on 2026-10-23T15:11:48Z (W133 MEDIUM-3; the same file). Question: under his Q2 (1), a linked worktree whose registered path is, or holds, the main worktree, the common directory or another worktree is registration-mismatch, and prints that class's next step, which ends "... or remove the row by hand". Followed literally for such a row, that step deletes the main worktree and the repository, or the other worktree (confirmed on plain Git, triage T2). Should such a row get its own step? The build now: the class's step, (c). Recommended (a): yes, "its path is or holds the main worktree, the common directory or another worktree, so do not delete it by hand or with git worktree remove; review its registration with git worktree list --porcelain" (197 characters, inside lane 4's 200-character rule; a row that shares its path with another row takes it too, R-67 (c)); a new next-step text in 004 (code and tests) and OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's D18. (b) No next step for such a row, and D18 says why: simpler, but no guidance. (c) Keep the class's step: not recommended, unsafe as worded.
  • "Yes, recommended for M1 and M3" takes both leans. 004's code changes only if he rules M1 (b), M3 (a) or M3 (b): M3 (a) would add a next step and its tests, and M3 (b) would print no step for such a row, either in a later code round.

Also open from #11, and known residuals

  • Carried from OpenSpec: add-project-clean-all-safe (batch worktree cleanup from the maintenance design packet) #11, not an LQ of this lane: the CPU-saturation risk is Brett Heap's call (openspec/changes/add-project-clean-all-safe/design.md:906-912; R-24). Under heavy load a plan reaches the deadline and goes incomplete with deadline-exceeded; it is never unsafe.
  • Residuals of the final fixes (R-95 (d)), no fix: an entry whose registration alone cannot be read is compared by its realpath only (R-57 L2); in single mode a twin of P that existed at planning, spelled through an unsearchable directory, is not caught, and both spellings name one clean merged worktree, so no work is lost (lane 3's LOW-3).
  • LQ-15's GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM (R-102 (c) 3, rulings:1054-1055). The two are the lead's extension of his ruling, Git's own config-path variables, and he ruled to keep them: O10 (A), "yes, recommended for O3, O5, O8-O11 and D5 clause 2" in lane 4's session (2026-10-23T12:19:11Z; R-370). OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 states them in one separate sentence (design.md:803-828 at f0bf538; R-115 L2, rulings:1179).
  • R-132, update's scope under LQ-15, to be put with LQ-19 (rulings:1284-1292). The build follows OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's D16 (R-131 (a), rulings:1279): under a bad HOME, update --apply refuses shape and bench only, and workflow and tools still run. Lane 3, as packet owner, recommends that update refuse every component with relative-config-path, exit 2, before any child, owner tools included, with no substitute HOME: an owner tool's git -C <repo> reads the planted configuration, OpenSpec: add-project-overview-remote-merge-status (opt-in exact-head GitHub pull-request evidence in project overview --remote) #29's C-44 refuses up front, a substitute HOME would change what the tools read, and the cost is one re-run with an absolute HOME. Until he rules, 004 builds OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's text as written.
  • Lane 3's env L-1 (PROTOCOL_PINS), from its reader RA (RA-L1): a remote-helper protocol of another name with its own allow=always in the repository's configuration still runs its helper from PATH, which the protocol pins cannot stop. It is R-25's accepted residual (OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19, Decisions), rated LOW. The rulings hold it "to be read and put to Brett Heap with the other LQs" (R-116, rulings:1188); no LQ number records it yet.
  • Task 1.7 of OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 (tasks.md:123-127 at 173d83c) is unticked: the push-child exception ratified by his word on OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19, with his words and the link still to fill. It waits for LQ-3. His words on Q9 B, on LQ-10 to LQ-15, on LQ-20 and on F-2, F-1, O-1 and R-284 are recorded in task 1.2, which is ticked. 004 plans under the exception until he rules (specs/004-project-clean-all-safe/spec.md:1801 and :1879).
  • update's tools component reaches the host's workBenches from any root (R-202, rulings:1568). An ad hoc update <fixture> --apply --component tools --yes ran the host's real setup.sh once, writing two gitignored files. This is existing behaviour, outside 004's scope; tests must put fake owner tools first on PATH.
  • A repository that holds refs/heads/HEAD makes every row inspection-error, not only that branch's (R-116, rulings:1184-1186). Git warns refname 'HEAD' is ambiguous. on standard error in every worktree's status probe, and a status probe whose standard error holds anything but Git's hint: lines is probe-failed with unparseable output. It fails closed (R-61 (b), rulings:358-361), and Brett Heap's Q8 puts the rule into OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19 with this consequence (R-380).
  • A branch.B.remote that is a path or a URL reads as unpublished, and a push then re-points the upstream to origin, as the baseline does (R-108 (d), rulings:1129). Recorded for a later evidence ruling; no change now.
  • Observations, no change: SonarCloud's gate failed at 2cf34d3 on a new security rating C from the git-floor job and passes since c9103ba, with four S8714 issues left open on purpose (R-99 (a), R-100 (f), R-106).
  • W65 LOW-3 (R-269, rulings:1747), a baseline function, left as a follow-on: repo_state()'s advice for a stale entry, "Review the stale worktree metadata before pruning it manually." (project:6806 at 7164d7f; baseline 15504f1:project:501), omits unlocking for a locked entry, which git worktree prune keeps; FR-064 keeps baseline functions unchanged. The no-prune warning beside an unread row is T9, ruled (R-365).
  • Q3 (b)'s warning is not given to status's text (R-380; M1 above).
  • The nested .git lstats of Q2 (2) are not measured on a worktree with very many tracked directories (R-398). They fail closed: a call over budget makes the row inspection-error at planning or state-changed at revalidation.
  • No test drives a .git made after the plan through --apply (R-397); the exit 2 and the later target's batch-stopped are pinned for contains-submodule only with a modules directory (test_clean_submodules_after_confirmation). It is for the next code round, with M3's step.
  • T127's tuple X holds two old gate-7 spans that main's D18 still quotes, until OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's D18 carries round (q)'s; revisit at landing (R-394).
  • The redirect message's limits, declared in OpenSpec: amend add-project-clean-all-safe so the push child reaches its remote #19's D18 (W81 M-1 and L-1, W80's L-2 and L-5, R-297; round (k), R-301), with no change to 004's code: a relative core.worktree whose target is missing gets the plain message, a submodule's redirect takes the variant, and a main checkout whose .git is a symlink gets the plain refusal, as before (W81 O-3).

Gate

R-76 (b) says this PR opens only when every task of feature 004 has landed, T084, T085, T092, T093 and T099 among them, and that its body names this gate. Tasks ticked: 133 of 133 at 41759a4, the head. The final gates ran at 4f921f8; the rows below are restated at later heads where a ruling gives them. The code merge the suite runs at is 1579c22 (703b58c, the Q-build, on the A-items e702a65, on d8724e2, R-365's build on f3724fc); 41759a4 adds doc rounds 25 and 26 (497f5a3), docs only. merge-tree of 41759a4 with #19's 4512441 is clean, and change 1's tasks.md equals main's copy (L1, R-377). Lane 3's sweep at d6bac50 is done (R-358, R-360); its batched read of 41759a4 and 4512441 is pending. Merges to main wait on Brett Heap's word, and so do the OPEN questions above, M1 and M3. Landing steps still owed: main (15504f1) merged into 004, with no rebase; the hand runs (T132) walked again at the landing head, by the lead through tmux or by Brett Heap (R-50, R-360); and his word. #19 lands first. Line cites to specs/004 are at 4f921f8 and to #19 at b3228bb, unless a cite names another sha. origin/main (e45155a) was merged into the branch as 05e6218, never rebased; it now reads 15504f1, with #21 (b233333) and #28 (15504f1) after e45155a, both OpenSpec changes only, and is not yet merged here.

Verification

Check Result
Suite on Python 3.12 and 3.10 (T128), with the module count equal to the tests/test_*.py count and tests/test_project.py unchanged but the one method and the loader line At 1579c22, the code merge (R-398): the full suite gives 516 OK (skipped=1) in 760.5 s. At 41759a4, docs only after it: 516 OK (skipped=1) in 742.3 s (R-401). The 14 modules the Q-build touched pass on Python 3.10 (and on 3.12), 299 tests with 1 skipped (R-395). Earlier: 494 OK (skipped=1) at d8724e2 (R-376); 491 OK on 3.12 and 3.10 at d6bac50 (R-360). 22 of 22 test modules; tests/test_project.py differs from main in the loader line and T046's method only
Git 2.36 floor (T129) git version 2.36.6; the 14 touched modules, 299 tests OK (R-398); test_status_the_sixteenth_scrubbed_name_on_git_2_36 ran and passed, not skipped (4f921f8); every earlier round's modules passed on 2.36.6 too (R-274 to R-352), and T129 passes at d6bac50 (R-360)
/usr/bin/openspec validate --all --strict (T130) 7 of 7 at 41759a4, and at every head since d764c26 where a ruling gives it (R-259 to R-361, R-372); the 10 kept scenarios byte-identical (4f921f8). #19 at 4512441: 9 of 9, with 73, 25 and 26 scenarios; its one INFO is main's own #28 delta against #21's requirement, not #19's (R-396)
Realization checks (T131) No non-ASCII added line; no file outside the path conventions and none changed but unlisted; parser() changed under clean only; requirements.txt unchanged. Exit 0 at every head a ruling gives, e69f6ef to d6bac50 (R-202 to R-360), and at 7a20789 (R-372), e702a65 (R-385), 8e90096 and 8abd901 (R-393, R-394, R-398; non-ASCII 0, unlisted [])
Hand runs, User Stories 1 to 9 (T132, hand-runs.md) Complete: walked by the lead through tmux on 2026-10-22 on project at e69f6ef, 41 of 41 MATCH (R-250, R-255; 0570efd); three runs walked again at d764c26, all MATCH (R-259); T132's verify exit 0 at f6e3d4b (R-272). Not re-walked since, and owed again at the landing head (R-50, R-360); T132 stays ticked
Traceability, one row per delta scenario (T126) 124 rows (100 on main, 24 of #19's), with six named gaps, each covered by tests whose names do not begin with the scenario's title (R-397; the gap notes in traceability.md); T002 and T126 pass
README and AGENTS.md review re-run on the final code (R-84 (b)) ACCEPT WITH FIXES (R-97) at 71e58fe; not re-run since. README changed after it only in doc rounds 13 to 15 and 18 to 26
Every task's verify Lane 3's sweep of all 133 at d6bac50 (W101, W102; R-358, R-360): 133 of 133 exit 0. Since then each writer ran the verifies of the tasks it touched (R-372, R-385, R-393, R-394, R-398); no full re-sweep at 41759a4 is recorded
CI (test jobs and git-floor) 41759a4: green (runs 40006309838 and 40006299501; R-403). Earlier heads green (R-261 to R-363): f3724fc (runs 39897478275 and 39897473466), 7a20789 (run 39953390038, R-376) and e702a65 (run 39962712250, R-385)

Lane 3's packet-fidelity and delta reads of this feature and of #19, with counts as the rulings record them (H, M, L are HIGH, MEDIUM, LOW):

Landing

Draft until the gate is met and the independent re-verification is posted here. Lands by squash only on Brett Heap's word for this PR, naming lane, PR and head, after #19 lands; the hand runs (T132) are walked again at that head first (R-360). If main moves (15504f1 now), main is merged in, with no rebase and no force push. After landing comes the archive PR for add-project-clean-all-safe (runbook step 3), on Brett Heap's word.

🤖 Generated with Claude Code

Brett Heap and others added 30 commits October 21, 2026 07:37
Lane: openRepoProject-2

Drop the `from tests import clean_support` fallback and keep a plain
`import clean_support`. A dotted run's fallback loaded a second
clean_support module with its own load_project() cache; the module runs
alone through discovery, which puts tests/ on sys.path (lead ruling
R-38; rule 3 as R-50 (g) words it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

Remove the sys.path insert, its comment and the `import sys` it alone
used; the module keeps its bare-name `from clean_support import
load_project`. It runs alone through discovery, which puts tests/ on
sys.path, so the insert was a second idiom (lead ruling R-38; rule 3 as
R-50 (g) words it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

Fill the Tests column of all 101 rows from the tests merged at 941443f,
each name as `def NAME` finds it in a tests/test_*.py module and built by
rule 1 from the scenario's title. Two tests that only share a title prefix
or a topic with a clean scenario are left out: the status test of a
submodule checkout and the status and doctor test of Git older than 2.36.
No other column, row order or text changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

Remove the sys.path insert, its comment and the `sys` and `Path`
imports it alone used; the module keeps its bare-name `from
clean_support import load_project`. It runs alone through discovery,
which puts tests/ on sys.path, so the insert was a second idiom (lead
ruling R-38; rule 3 as R-50 (g) words it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t handoff

Lane: openRepoProject-2

The Tasks line under "Speckit Handoff" read "(reserved; created by
/speckit.tasks)"; it now reads "(created by /speckit.tasks)", as lane
openRepoProject-1's archived changes record theirs. No other line of the
file changes, and no box is ticked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

timing_repo() goes; its two callers use CleanCase.repo(), which makes
the same repository: one empty commit on main at self.base/repo.
FAKE_STATUS_GIT goes; interrupted_status() puts the counting wrapper
first on PATH over the status-hangs-in fake (lifetime 10 s, as the old
fake's sleep). The status child's pid and parent's pid come from the
wrapper's first `status` record, and project's process group is read
with os.getpgid() while project runs. Method names, assertions and
expected values are unchanged (lead ruling R-38; rule 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

R-34 names the list: default_branch(), ancestor(), cleanup_report(),
print_clean_report() and matching_worktree() have no caller in project or
tests/ since T076 rewrote push_or_delete() and
require_unchanged_cleanup_state(). cleanup_signature() still has its caller
and stays. inspect_repository()'s docstring no longer says
cleanup_report() is live code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s 1 to 3

Lane: openRepoProject-2

Item 1 (R-91 M-1): T093 and research.md R6 say a rendering fault is
dropped by best_effort() with no change of stage and no exit, a fill of
the delta's pcl:938-941 and :1020-1024; the list of exceptions that make a
target `unknown` with `internal-error` loses "the record's rendering". T093
also gains "after exit 0" for the rescan-read rule (item 5, its T093 part).
Item 2 (R-91 M-2, R-89 (a)): data-model.md says revalidation now sees a
twin made after planning (registry_twin(), R-81 (a)) and its step table
gains the twin check and the deadline cut; T098 names tests/test_clean_bounds
tests for the cut rescan and the twin, and T099 states the floor before
each revalidation, the cut revalidation and registry_twin(). The verifies of
ticked T098 and T099 gain those names; both pass.
Item 3 (R-91 LOW A): interim rows 1, 3, 4, 6 and 7 are marked CLOSED with
their tasks, and the PR gate's tasks (T084, T085, T092, T093, T099) landed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

Item 4 (R-91 LOW C): T091, T094 and the contract name the TEXT key
`branches_not_confirmed` beside the words `Branches not confirmed: B2.`.
Item 5 (R-91 LOW D): FR-036 and data-model.md's signal table add that a
child killed at the ceiling keeps `removal-ceiling` after an abandoned
rescan, and T087 says its fake's child exits 0 (T093's part is in the
previous commit).
Item 6 (R-91 LOW E): T085 cites BA:521-523 for a gone-but-unreconciled
child and BA:520-521 for a failed registry listing, beside BA:518-526, all
at da33d92; data-model.md's first overlap is the reading.
Item 7 (R-91 LOW F): T049, T075, the contract and data-model.md name "an
undecided P exempts no entry from the cap" a fill of FR-013 and of the
delta's spec.md:262-263.
Item 8 (R-91 LOW H, R-85 (d)): T128 says an empty discover exits 0 on
Python 3.10, so its module count is checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 11 items 9, 10 and 12

Lane: openRepoProject-2

Item 9 (R-92 (d), R-80 P-13): the contract says steps 3 and 4 order the
refusals, not the reads; the delta asks only "before any plan or digest"
(pcl:1067-1069), and the code reads the manifest and rows 4, 6 and 7 in its
one inspect_repository() call before it checks root and common_dir.
Item 10 (R-92 (d), P-8): T113's verify chain adds
test_status_doctor_update.py, and its text says so; the new verify of
ticked T113 passes (22 modules OK).
Item 12 (R-92 (c)): the contract's refusal text records discover()'s two
raw-path messages as PENDING, to be escaped by resolve_clean_target() in
a final fixes lane.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
Retires the five helpers that lost their callers when the push lane
landed: cleanup_report(), matching_worktree(), ancestor(), default_branch()
and print_clean_report() (R-80 P-15). cleanup_signature() stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
traceability.md names each of the 101 scenarios' tests (T126); the
change's tasks.md points at feature 004's tasks (T133). Nothing archived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
R-91's items from lane 3's delta read of doc rounds 9 and 10, and R-92(d):
T093 and research R6 follow the contract on best_effort(); US6 as built
in data-model.md; interim rows 1, 3, 4, 6 and 7 closed with their tasks;
T098 and T099 name their tests; T113's chain gains
test_status_doctor_update.py; the contract's step-order note. Item 11
waits for the test-hygiene lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
test_clean_scrub, test_clean_escaping and test_clean_parsers import
clean_support by its bare name with no sys.path insert;
test_clean_timing.py builds on CleanCase.repo(), install_fake_git() and
install_counting_git(). No assertion changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
T125 (retire, merged f9f63ad), T126 and T133 (records, 9a89357) and
T127 (tests, 51d44a9) pass their verifies at 51d44a9. T128-T131 run
again after the final fixes merge (lead ruling R-93 (h)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-92 (c), feature 004)

Lane: openRepoProject-2

discover() names the user's own argument raw in `Directory does not exist:
PATH` and `No project found at PATH; supply a path or name.`, which clean
reports as repository-not-found. resolve_clean_target() now prints the PATH
of each through human_path(), so a bare name holding U+202E or the byte 0xFF
reaches the terminal and the JSON document escaped. discover() is unchanged,
since status, doctor and update share it. The JSON refusal keeps its two
fields, code and message; it never carried a path.

Tests in tests/test_clean_paths.py: one per message for U+202E, the same for
0xFF on Linux, and the fallback where discover() codes neither message. They
fail on 41971d9 (5 subtests across 2 tests) and pass here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

build_plan() now gates P's rows (gates 1 to 7, gate 8 with P alone,
gates 9 and 10) right after match_target() builds them, before
worktree_rows() inspects the other rows, the order of design D13 and
pcl:262 (lead ruling R-93 (d)). No gate reads another row to decide P.
With no deadline the plan is unchanged: only P's gate children move
ahead of the other rows' status probes. A work deadline reached among
the other rows now leaves P's row decided. The docstring states the new
order and drops the retired cleanup_report().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

- tests/test_clean_paths.py: delete the unused local root_form.
- project, print_plan()'s docstring (region preview): it said print_plan()
  replaces print_clean_report(), which T125 retired; it now says the function
  replaced it and that T125 then retired it. No code changed.
- tests/test_clean_timing.py: the comment on os.killpg() said a status child
  is left in project's group. Every Git child of `project status` leads its
  own group, so the call reaps project's own group alone; the comment says so.
  No code changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

Lead ruling R-93 (e), LOW-1: during the apply phase the first signal
stops scheduling, so that no further probe starts (pcl:931-933). In
region apply, revalidation_difference() now reads the signal slot
(slot_set()) before each read it starts, its Git children and its
filesystem steps alike. A set slot ends the revalidation as the seam
before the spawn does: the target is not-attempted with interrupted and
no reconciliation, later targets follow, and the exit stays 130, 143 or
129. The rescan of a removal already spawned is unchanged.

Test: test_clean_a_signal_during_a_removal_sigint_during_a_revalidation
(lane 3's sig-reval-INT), with a new fake kind, status-slow-again-in,
in tests/clean_support.py. At 41971d9 it fails: the hidden-state probe
started after SIGINT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

test_clean_single_target_a_deadline_among_other_rows_leaves_p_decided
follows lane 3's single-deadline-note run: P and a merged worktree whose
status probe hangs, through the launcher with deadline 3 s and reserve
1 s. P's status probe and ls-files run before the other rows' status
probes; the plan is complete with the note deadline-exceeded naming P;
P is not-attempted with deadline-exceeded, exit 1, in the human result,
the JSON plan and the JSON result record; nothing is removed. At 41971d9
it fails: exit 2, inspection-incomplete, P excluded reflog-unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

Lead ruling R-93 (e), LOW-2: nothing pinned R-80 P-2's revalidation
deadline cut. Two tests beside T098's, both through the launcher, with a
new fake kind, ls-files-stuck-again-in, whose second ls-files in a path
ignores SIGTERM and sleeps:

- a revalidation read cut at the deadline (deadline 8 s, reserve 2 s,
  probe budget 30 s, grace 1 s): both targets not-attempted with
  deadline-exceeded, records null, nothing removed, exit 1;
- the same read cut at a 1 s probe budget: the first target refused
  with hidden-local-state, exit 2, a timeout staying a difference.

Both pass at 41971d9. With read_cut() and work_spent() returning False
(lane 3's mutant), the first fails with exit 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
resolve_clean_target() prints the PATH of discover()'s two bare-name
repository-not-found messages through human_path(), with U+202E and
0xFF tests in tests/test_clean_paths.py; discover() is unchanged. The
three nits: an unused local, print_plan()'s docstring and a comment in
tests/test_clean_timing.py (lead rulings R-92 (c) and (e), R-93 (c)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

When match_target() leaves P's match undecided (lead rulings R-73 (d)
and R-78 (b)), build_plan() records none of single mode's non-blocking
notes inspection-incomplete, inspect-cap and deadline-exceeded: each
says the rows do not block P beside the plan's refusal of P, and the
inspect-cap note counted P among the rows past the cap. The refusal,
inspection-incomplete, speaks alone (lead ruling R-93 (e)).

New test_clean_single_target_an_undecided_match_has_no_non_blocking_note,
lane 3's r78b.py shape in process (the single cap patched to 4; P's own
realpath, then another entry's, unread): inspection-incomplete, exit 2,
no note; at 41971d9 it fails with the inspect-cap note. The one existing
assertion that pinned that note in
test_clean_single_target_p_s_own_realpath_unread_keeps_the_cap now
expects no note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

Lead ruling R-93 (e), LOW-3: registry_twin() skipped a listed entry
whose lstat or realpath could not be read, so a twin spelled through an
unsearchable directory went uncaught (lane 3's twin-unreadable). Now
such an entry, failing other than with ENOENT or ENOTDIR (absence,
R-36), gives identity-changed (rs:92-93), as planning fails closed
(R-78 (b)). One entry is exempt: a row the plan read with present null
whose lstat fails again, null in the plan and null again (rs:89-92), so
that single mode's non-blocking inspection-error row does not refuse P
(pcl:258-262, pinned in test_clean_single.py). The docstring's "as in
planning" is corrected.

Test:
test_clean_admin_registration_changed_a_twin_spelled_through_an_unsearchable_directory.
At 41971d9 five of its seven subtests fail: the target is removed, exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2

test_clean_single_target_an_undecided_match_has_no_non_blocking_note
patched the single-mode row cap down to 4, which tasks.md's rule 6 for
new tests forbids (caps are never scaled, R11). LOW-5 stays pinned at
the real cap of 128 by the assertion in
test_clean_single_target_p_s_own_realpath_unread_keeps_the_cap, which
expects no note. No helper or import was used by that test alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
In a named removal, build_plan() runs P's gates on P's rows right after
match_target() and before worktree_rows() inspects any other row, as
design D13 orders, so a work deadline among the other rows leaves P
decided beside the note deadline-exceeded (lead ruling R-93 (d)). With
P's match undecided, single mode's non-blocking notes are left out and
the refusal speaks alone (R-93 (e)). Tests in tests/test_clean_single.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
The final writer of the test-hygiene lane merges the integration branch
first, so that R-81 (b)'s and R-82 (d)'s tests run against the code as it
stands: US8, the polish wave, and final-fixes lanes A and C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
R-90's R-81 (b) item. In process, the counting wrapper is the only git on
PATH; after the full re-inspection runs through it as built, the wrapper
is made not executable, or removed. The delegate's spawn then fails, and
the run refuses with the baseline's `Cannot run git: ...`, exit 2, prints
the plan and nothing relayed, and attempts no other child: subprocess.Popen
sees the delegate alone after the re-inspection, and the branch is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lane: openRepoProject-2
A revalidation reads the signal slot before each read it starts and ends
with interrupted, so no probe starts after a signal (pcl:931-933). The
revalidation deadline cut and its budget contrast are pinned. The twin
check refuses identity-changed on an entry whose lstat or realpath
cannot be read, except one the plan also read as failed (rs:89-93;
pcl:258-262). Two fake kinds are added to tests/clean_support.py (lead
rulings R-93 (e) and R-95).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…robe (R-82 (d))

Lane: openRepoProject-2
R-90's R-82 (d) item, as R-93 (a) corrects it. Between Git's exit 0 and
the rescan, a post-remove hook replaces a parent by a file: the target's
own parent reads path_present false and the target `removed`, exit 0;
the repository root's parent loses the identity, every field null,
`unknown` with `unconfirmed-removal`, exit 1; the contrast, the parent
made mode 000 (EACCES), is `reconciliation-incomplete`, exit 1. A second
test cuts the rescan's identity probe at its budget while the root is
still there: the marked all-null record gives `reconciliation-incomplete`,
exit 1, and no later rescan read runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 6 commits October 23, 2026 13:06
…ing change 1's tasks.md (R-377, L1)

Item 1, R-365 (ruled by Brett Heap on 2026-10-23, "Yes, warn (Recommended)" for F-1's (b), "Yes, add T9 (Recommended)" for T9; lead R-365). Beside a row whose path could not be read, the ceiling's prune note is TEXT partial_removal_note_prune_unread (print_result() passes unread_paths(plan) into partial_removal_text()), and repo_state()'s stale recommendation ends with the no-prune warning (status, doctor and update share it). Both texts are copied from project at d8724e2 by script (texts.py), and the three new tests are listed in T089, T091 and T115 and in traceability's tail. Sites: README's (b) paragraph; the contract's D18 note row, its "known residual" (now split: the omitted unlocking stays a residual, the missing warning is an accepted departure from FR-064) and a pointer in "The unread forms"; data-model's R-320/R-324 enumeration, the Classification text on status, the note selection and the status-state section; spec FR-028 and FR-064 take dated notes; T011, T085, T089, T091, T094, T115 and T117; traceability's tail and its head sentence at d8724e2; hand-runs.md's dated note names R-365 and the shortstat at d8724e2 (711 insertions, 118 deletions).

Item 2, L1 (lead R-377; wave 22 triage, section 5): openspec/changes/add-project-clean-all-safe/tasks.md restored to origin/main's copy (blob 8a87a4d; git diff origin/main on it is empty). Path Conventions, plan.md and quickstart.md say that PR #19's round (p) carries 004's four hunks and T133's Tasks line, taking analyze M8's first option in place of R-42's merge. T133 is rewritten: its verify passes on main's copy and on the copy that carries #19's Tasks line (blob 44c9101), and fails when this branch has changed the file. T131's verify no longer allows that file among the branch's changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…373, R-374); checkpoint at the account swap

One commit for items 1 to 7 of the brief, with their tests, made at the lead's
account-swap checkpoint instead of one commit per item.

- T7 (sr-evidence M-2, M-3): _fs() and _direct() also catch ValueError (a NUL byte
  in a `.git` or `commondir` file) and raise an `os-error` EvidenceError with no
  errno, so the row is inspection-error and status exits 0; head() of
  resolve_merge_target() and narrowed_ref_patterns() read a name with a lone
  surrogate as no local branch.
- T10: a `detached` flag from `record["branch"] is None` (WorktreeRow.detached),
  read by classify() and the delete-branch holders check, not the text
  `detached`. The push path keeps its baseline text match (test P3 pins it).
- T19 (W121 MEDIUM-2, R-374): a failed row 7 gives each present row on a branch
  row 7's error, as row 6's does, so it is inspection-error and never
  review-required (pcl:178-179); the pin in test_clean_evidence.py is updated.
- T14 (W120 L-1): unsupported-path-bytes' next step names git worktree move, or
  mv then git worktree repair NEWPATH.
- T15 (W115 LOW-2, LOW-3): row 116's test runs --all-safe --apply --yes; row 111
  gets a real-Git core.bare=true case with two targets.
- T16: the comment on TEXT partial_removal_note_locked cites "F-2: yes" (R-263).
- LOW-5 (W121): the above-128 report's excluded list, and --expect-plan above 128.
- Optional T17: not added; test_clean_an_unread_row_beside_a_redirected_row
  already pins a registration-mismatch P refused as inspection-incomplete with
  the unread remedy beside an unread sibling.

Verifies run (all pass): test_clean_evidence, test_clean_push, test_clean_paths,
test_clean_scrub and test_clean_bounds on Python 3.12, on Python 3.10 and on Git
2.36.6; test_clean_gates, test_clean_resolution, test_clean_revalidation,
test_clean_harness, test_project and test_status_doctor_update on 3.12; T131
(before this commit); the verifies of T012, T027 to T029, T031, T032, T051, T052.
The new behaviour tests fail against d8724e2's project (run in a copy).

NOT RUN at this commit: T030, T043, T072 to T074 and T127 verifies (the run was
stopped at the swap); T131 and the mutation checks against this commit; the
doc-site list for the next doc round.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s Q1, lead R-380)

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
Lane 2's wave 22 triage T1 (sr-classification H-2, sr-evidence H-1 and H-2,
sr-removal F1).

- project: STATUS_PINS gains "-c", "core.ignoreCase=false", after
  core.checkStat=default, so the status probe, the hidden-state probe and the
  removal child carry it (GIT_KINDS). Under core.ignoreCase=true on a
  case-sensitive file system Git left out of the status records an untracked
  file whose name differs only in case from a tracked one, and `git worktree
  remove PATH` resolved PATH to the first worktree whose path matched it
  ignoring case. Two docstrings name STATUS_PINS instead of "the two status
  pins".
- tests: every transcription of the pins gains the fifth status pin
  (test_clean_evidence, _apply, _result, _runner, _scrub with design D9's
  removal vector, _revalidation and test_status_doctor_update). Two new
  real-Git tests in test_clean_scrub's PinnedStatusTests:
  test_status_a_case_variant_untracked_file (the setting in the repository's
  config, and in GIT_CONFIG_GLOBAL alone: the row is dirty, nothing selected,
  the apply keeps the file) and
  test_clean_two_worktree_paths_that_differ_only_in_case (the eligible WT-X
  removed; the excluded wt-x, first in readdir order, and its HEAD-reflog-only
  commit remain). Both fail against e702a65's project and pass here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gistration-mismatch (Brett Heap's Q2, lead R-380)

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
Lane 2's wave 22 triage T2 (sr-classification H-1, sr-evidence H-3). Git's
directory walk skips every entry named .git and recurses into a directory the
index tracks without asking whether it holds a repository, so such an entry
showed in no status record, passed Git's own check, and the removal deleted it.

- Q2 (1): enclosing_rows(), a pure function of values already read: a present
  linked row whose raw path or realpath is the common directory, the main
  worktree, another registry entry's raw path or another built row's
  realpath, or an ancestor of one, is registration-mismatch, unprobed, as a
  shared path is (worktree_rows()). Its row is inspection-error, so the plan
  is incomplete and nothing is removed.
- Q2 (2): HiddenStateParser keeps `directories`, every directory above an
  entry read before the bound (the top level aside); hidden_state_probe(),
  after a gitlink and before the hidden flags (gate 9 before gate 10), runs
  nested_git_check(): one fs_call() through _fs(), OPERATION lstat, counted in
  the filesystem budget, an lstat of <dir>/.git per directory in byte order
  (nested_git_entry()). An entry of any type is contains-submodule; a failure
  other than ENOENT or ENOTDIR, or the call cut, is the row's error, so it is
  inspection-error. Being inside hidden_state_probe(), the check also runs at
  revalidation step 7 (a failed read there is state-changed, a cut one
  deadline-exceeded). The parser's result dict is unchanged.
- tests (each fails against e702a65's project and passes here):
  test_clean_gates: a repository nested in tracked tools/, a docs/.git
  directory and an a/.git file over a/b/c.txt, each contains-submodule beside
  a removed control, the inner commit kept; the nested lstat meeting
  PermissionError and a timeout in process; the main worktree inside W's
  tracked data/ (sr-evidence's mkcata): W registration-mismatch, batch and
  single apply refused, the repository whole; enclosing_rows() on literal
  values. test_clean_apply: a repository nested after the plan refuses its
  target at revalidation as contains-submodule. test_clean_parsers: the
  parser's `directories`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… warn of the dry run (Brett Heap's Q3, lead R-380)

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
Lane 2's wave 22 triage T3 (sr-removal F2, sr-evidence M-1). git worktree
prune acts on the whole repository with an expiry of now: it deletes the
administrative directory, and with it the HEAD reflog, of every unlocked
entry whose path is missing, a worktree moved or on a drive not mounted
included.

- Q3 (a): the ceiling's partial_removal_note_prune is "Probable partial
  removal: inspect, then delete {PATH} and its administrative directory
  {ADMIN} by hand." in every mode and beside any row, ADMIN being
  <common_dir>/worktrees/<admin_id> in the command form
  (partial_removal_admin(), from the target's identity and row; "unknown"
  where a plain dict carries neither). It supersedes R-365's
  partial_removal_note_prune_unread, which is removed with its selection:
  partial_removal_text(target) takes the target alone.
- Q3 (b): PRUNE_DRY_RUN, "; git worktree prune also deletes the entry, and
  so the HEAD reflog, of every other worktree whose path is missing, one
  moved or on an unmounted drive included, so run git worktree prune
  --dry-run first and remount or repair any it lists that still exists",
  ends the plain forms: the registration-mismatch and inspection-error next
  steps, both inspection-incomplete remedies, the inspect-cap remedy, and the
  stale row's recommendation (new TEXT key recommendation_stale_worktree,
  which row_evidence() and next_step() give a stale row; RECOMMENDATIONS
  keeps the baseline's text, which classify() still returns) and its locked
  form, each recommendation ending in ".". The forms beside a row whose path
  could not be read are unchanged.
- tests (each fails against e702a65's project and passes here; run 37,
  failures 24, errors 13 there): test_clean_stop's ceiling note on literal
  targets, ADMIN printed and "unknown" for a plain dict, the second note the
  same beside any row; test_clean_result's recovery on real Git following
  the new note beside a moved sibling whose commit lives only in its HEAD
  reflog, prune --dry-run naming the sibling and the commit surviving, and
  the second note beside an unread sibling naming no prune;
  test_clean_evidence, test_clean_gates, test_clean_bounds, test_clean_batch
  and test_clean_harness on the suffixed texts. tests/clean_support.py's
  fixtures keep step (item 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 23, 2026
…pository name that begins with a dot, such as .github, among the names the shared identity rule refuses though GitHub allows them (RC2-2); D5's paragraph on the two texts says that amendment A1 refuses the overview for the same four variables (W118's LOW-2, applied with change in that form, C-81); D5's lookup-failure paragraph says that the test of the three configuration variables is made first even when the lookup falls due past the deadline (RC2-1, C-81); D11's counting git wrapper scopes each control, change 2's hold among them, to a chosen argv in a chosen repository, the -C directory it records (RC's L1); Risks' Git 2.36 floor entry names feature 004's git-floor job and its -k list (RE-8's last sentence); Migration Plan step 3 names every job the tests workflow then runs, git-floor at PR #33's d8724e2 included (RE-8). The three edits before D7 keep their paragraphs' line counts (10, 25 and 16), so D5's residual 574-601 and D7 954-989 are cmp-identical to a529310; design.md 1560 to 1575 lines, all added after D7; no spec edit in this commit

Lane: openRepoProject-5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brett Heap and others added 22 commits October 23, 2026 14:49
…texts keep submodule commits (Brett Heap's Q4, lead R-380)

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
Lane 2's wave 22 triage T4 (sr-removal F3, F4, F5; sr-classification M-3).
Texts copied byte for byte from #19's round (q) at 818311a, design.md D18.

- Q4 (a) and (b): D4's report-row note judges the status records read, so
  work past the 4,096-record bound or an ignored file left it showing
  --force. Every detection rule stays; the note now names the check that
  shows such work:
  partial_removal_note "Probable partial removal: check that git -C {PATH}
  status --short --ignored lists only deleted files, then git worktree
  remove --force {PATH} by hand." and partial_removal_note_locked "...
  lists only deleted files, unlock it, then git worktree remove --force
  {PATH} by hand.".
- The ceiling's own --force text stays, as the new key
  partial_removal_note_ceiling ("Probable partial removal: inspect, then git
  worktree remove --force {PATH} by hand."), which partial_removal_text()
  now returns where the rescan shows the .git file and the entry present.
- Q4 (c): contains-submodule's step "push or otherwise keep each
  submodule's commits first, since --force deletes its repository with this
  worktree's git directory, then git worktree remove --force yourself", and
  gate 7's two parentheticals "(--force if it holds a submodule, once its
  commits are kept)".
- tests (fail against e702a65's project, pass here; 13 run there, 14
  failures, 13 errors): test_clean_stop's new
  test_clean_the_partial_removal_note_names_its_check_first (SC5: 4,100
  deletions, a modified and an untracked file past the bound; F4: one
  deletion beside an ignored local.env; the note printed, the check showing
  the work, the apply removing neither); the intact-deletion and locked
  notes with their check run first (only " D " lines); test_clean_gates's
  submodule row following its step (the commit pushed, then --force deleting
  modules/); and the tests that print the gate-7 and submodule steps.
  tests/clean_support.py's fixtures keep step (item 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in line defers to each group (Brett Heap's Q5, lead R-380)

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
Lane 2's wave 22 triage T5 (sr-classification M-2, sr-removal F6). Texts
copied byte for byte from #19's round (q) at 818311a, design.md D18.

- unstarted-branch: "no commit was made on this branch here since it was
  created; its later gates did not run, so check git log -g HEAD, git
  ls-files -v and git status there, then git worktree remove yourself
  (--force if it holds a submodule, once its commits are kept)", and
  reflog-unavailable the same after "the branch's reflog is missing,
  expired or undecidable; ". Gate 7 precedes gates 9 to 12, which do not
  run for its rows, and "review, then git worktree remove yourself",
  followed literally, deleted a HEAD-reflog-only commit or an
  assume-unchanged edit.
- non_drain_line: "  these deferred rows will not drain while the 16 rows
  ahead of them are {REASONS}; follow each group's next step for those
  worktrees", where it had told the person to remove the 16 rows that gates
  10 to 12 excluded for holding work.
- tests (fail against e702a65's project, pass here; 6 run there, 7
  failures): test_clean_reflog's new
  test_clean_gate_7_s_step_names_the_checks_of_the_gates_that_did_not_run
  (M-2's detached commit and an assume-unchanged edit, both
  unstarted-branch: the new step printed, the single removals refused, and
  git log -g HEAD and git ls-files -v showing each); test_clean_bounds's
  mixed seventeen-row drain (F6's shape) printing the new line beside each
  group's own step; the steps' other printers. tests/clean_support.py's
  fixtures keep step (item 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ule words it (Brett Heap's Q8, lead R-380)

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
Lane 2's wave 22 triage T9 (W115 MEDIUM-1, W121). #19's round (q) at
818311a states the rule in project-command: a probe that exits 0 having
written to standard error any line other than Git's advice lines (`hint:`
alone or `hint: TEXT`), or more than the 4 KiB kept, fails; so a
repository holding refs/heads/HEAD has every row inspection-error.

- Checked against that rule, status_warned() differed in one case: it
  skipped blank lines. A blank line is a line other than an advice line, so
  it now fails the probe; only the empty text after the last line end, or
  no output at all, is no line. The 4 KiB clause was already the rule.
- tests: test_clean_evidence's
  test_clean_evidence_the_status_warning_rule_counts_every_line, on literal
  outcomes (three blank-line cases fail against e702a65's project and pass
  here; the rest pin the advice forms, the 4 KiB edge, a line of spaces,
  `hint:a`, and Git's ambiguous-refname warning), and
  test_clean_evidence_refs_heads_head_makes_every_row_inspection_error, on
  real Git: the warning on standard error with exit 0, every row
  inspection-error with probe-failed, the plan incomplete, and nothing
  removed by --all-safe or by name. That consequence already held at
  e702a65, so this second test passes there too: it pins the rule's
  consequence and is no behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n-mismatch (Brett Heap's Q9 (b), lead R-380)

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
Lane 2's wave 22 triage T12 (sr-removal F7); #19's round (q) at 818311a
adds "or whose registered path is a symbolic link" to
registration-mismatch (project-clean-review-safety).

- _read_row(): a linked row whose registered path lstats as a symbolic link
  takes the verdict a shared path takes, registered false with no admin id,
  from the lstat already made; its realpath is still read. So the row is
  unprobed, inspection-error and excluded as registration-mismatch, and the
  plan is incomplete. Before, its registration agreed through the link, it
  was selected, and Git emptied the link's target, failed to remove the
  link (exit 255) and still deleted the administrative directory. The main
  worktree's row is unchanged.
- tests: test_clean_gates's
  test_clean_a_registered_path_that_is_a_symbolic_link (F7's fixture: a
  merged, pushed worktree moved by hand and a link left at its registered
  path, its status printing nothing): registration-mismatch,
  inspection-error with dirty null, the plan incomplete, and nothing
  removed by --all-safe or by name, the link, its target's files, the
  administrative directory and the control kept. It fails against
  e702a65's project (the preview exited 0 with the row selected) and
  passes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rett Heap's Q2 (2), lead R-380)

The lead asked whether clean's revalidation re-runs gate 10's listing for
the target, and whether Q2 (2)'s nested .git lstats ride it. They do:
revalidation_difference()'s step 7 calls hidden_state_probe(), which runs
the listing again and then nested_git_check() over its directories, one
fs_call() through _fs() under the same per-call budget. 5eeaedd tested a
repository nested after the plan (contains-submodule); this adds the other
outcomes, no code change.

- tests: test_clean_apply's
  test_clean_revalidate_a_nested_git_lstat_that_fails_or_is_cut, in
  process: the call raising PermissionError or cut at its budget gives
  state-changed (R-195), cut by the work deadline deadline-exceeded; the
  call made once, naming the target's one directory; the target kept. It
  errors against e702a65's project (no nested_git_entry) and passes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…502d (R-373, R-374, R-380, R-385)

Merged impl/004-q-build (da5502d; merge commit ff12f47, no rebase). Every text is copied from project at da5502d by script.

Part A, the A-items (e702a65; lead R-373, R-374, R-385): T14, the unsupported-path-bytes step now "move it to a valid UTF-8 path with git worktree move, or mv it and then git worktree repair NEWPATH" (README, contract, quickstart, spec FR-028, T110); T7, an os-error for a NUL byte in a .git or commondir file (Python's own text, no errno) and a lone-surrogate tracking_branch read as no local branch; T10, the detached flag; T19, a failed row 7 makes each present row on a branch inspection-error, never review-required; T15, the rows 111 and 116 arms; LOW-5, the two arms of the plain-report test above 128 rows. Each new test is in its task and in traceability's tail.

Part B, Q1 to Q3 (ruled by Brett Heap on 2026-10-23T13:30:27Z, "yes, recommended for Q1-Q9"; lead R-380; 9d5c2fb, 5eeaedd, da5502d). Q1: core.ignoreCase=false is a fifth status pin (the removal's sixth) on the status, hidden-state and removal children. Q2 (1): a present linked row that is, or holds, the common directory, the main worktree or another row is registration-mismatch (enclosing_rows). Q2 (2): gate 9 lstats <dir>/.git in each directory that holds an index entry, one fs_call, and again at revalidation step 7 (nested_git_check). Q3 (a): the ceiling's second note is "Probable partial removal: inspect, then delete {PATH} and its administrative directory {ADMIN} by hand." in every mode; it supersedes R-365's partial_removal_note_prune_unread, whose records stay, marked superseded, and partial_removal_text() takes the target alone again. Q3 (b): the dry-run warning ends the plain prune texts (the two inspection-incomplete remedies, the inspect-cap remedy, the registration-mismatch and inspection-error next steps, the stale recommendation and its locked form); the unread forms are unchanged.

Dated notes on FR-007, FR-011, FR-018, FR-023, FR-028, FR-050, FR-054, FR-055 and FR-064. Tasks: T002, T011 to T015, T028 to T032, T038, T045, T049, T052, T061, T063, T067, T073, T082, T084, T085, T089, T091, T094, T096, T097, T101, T110, T115, T116 and T126. Traceability: round (q)'s five scenarios (PR #19 at 818311a) are rows 118 to 122 (122 rows; T002 and T126 verifies count them), the gap notes for rows 111 and 116 name their new arms, and the tail lists every new and changed test; the head sentence reads da5502d. hand-runs.md's dated note names the changes (git diff --shortstat e69f6ef da5502d -- project: 960 insertions, 165 deletions).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… lstat tests documented at 703b58c (R-380, R-393)

Merged origin/impl/004-q-build at 703b58c (merge commit c81b948, no rebase). Every text is copied from project at 703b58c by script. Brett Heap's words, "yes, recommended for Q1-Q9" (2026-10-23T13:30:27Z; lead R-380), are cited in each dated note.

Q4 (c6daebc): a report or preview row's partial-removal note names its check first ("check that git -C PATH status --short --ignored lists only deleted files, then git worktree remove --force PATH by hand", and the locked form with "unlock it"); the ceiling's first note keeps its text under the new key partial_removal_note_ceiling; contains-submodule's step keeps the submodule's commits first, and gate 7's parentheticals read "(--force if it holds a submodule, once its commits are kept)". Q5 (c28c9ea): gate 7's two steps name the later gates' checks (git log -g HEAD, git ls-files -v, git status), and the non-drain line defers to each group's next step. Q8 (70345ee): a blank line counts as a line of the status probe's standard error, with the refs/heads/HEAD consequence. Q9 (b) (338edb0): a linked row whose registered path is a symbolic link is registration-mismatch. Q2 (2) (703b58c): the tests of step 7's failed and cut nested-.git lstat.

Sites: README (items 3 and 4, the next-step table, the non-drain sentence, the registration-mismatch scope and remedy sentences, the partial-removal paragraph); the contract (the non-drain row, the D18 partial-removal note row, the Codes variant sentence, gate 7's and gate 9's texts, the status-probe rule, a new Q9 (b) paragraph); data-model (D4's note text, gate 3, the status-warning sentence, the ceiling's note selection); dated notes on FR-009, FR-020, FR-028, FR-032, FR-055 and FR-057; hand-runs.md's note (shortstat e69f6ef..703b58c). Tasks: T011, T030, T031, T045, T052, T060, T061, T063, T084, T089, T091, T094, T097, T100 and T127. T127's verify tuple X exempts the two old gate 7 spans that main's design.md D18 still quotes, as doc round 18 did for the old drain line; the verify's static text check passes at 30 spans, and T127 itself was not run. Traceability: the new tests are listed in the tail, no scenario is added (122 rows), and the head sentence reads 703b58c.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s, and round (r)'s two scenarios are rows 123 and 124 (R-391, R-395, R-396)

Documented at project 703b58c, unchanged (git diff 703b58c HEAD -- project tests is empty). The wording of PR #19's round (r) (4512441) is copied by script from its project-clean-review-safety delta (spec.md:110-112 and :131-132): "the hidden-state probe, with the `lstat` of `<dir>/.git` for every directory that its listing shows holding an index entry" and "`contains-submodule` for a new gitlink, `modules` entry or `.git` entry"; the two scenario titles and their requirement titles are copied by script too.

Revalidation (lane 3's W133 MEDIUM-2; lead R-391 and R-395): step 7 (project:6550-6558) calls hidden_state_probe(), which runs gate 10's listing again (project:5590, the step's one Git child) and then nested_git_check() (project:5595), one fs_call() of the lstat of <dir>/.git for every directory the listing shows holding an index entry, counted in the filesystem budget and no Git child, so the six, seven, nine and ten children stand. A .git entry made after the plan refuses the target as contains-submodule; an lstat that fails, or hits its own budget, gives state-changed; one cut by the work deadline gives deadline-exceeded.

Sites: README (the contains-submodule paragraph and "What is guaranteed"); the contract (the hidden-state row of the Git children table and the gate 9 paragraph); data-model (gate 9's row and its paragraph, step 7's row, a new paragraph after the steps table, the filesystem-calls sentence); spec.md (dated notes on FR-046 and FR-047); tasks (T084, T067). Traceability: rows 123 ("A nested repository made after the plan", US4) and 124 ("A stale entry's recommendation", US2), each a named gap, 124 rows; the intro, the "twenty-four" and "Six of PR #19's scenarios" sentences, two gap bullets naming the covering tests, and a tail paragraph. T002's and T126's texts and verifies count 124 and list both titles; T126's gap list gains both. The nested-repository scenario has no test that runs it through --apply: its covering tests are in process (T045), and the exit status and the later target's not-attempted are pinned for contains-submodule only by test_clean_submodules_after_confirmation (a modules directory). No test is added; the head sentence stays 703b58c.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…A-items

Brett Heap, 2026-10-23T13:30:27Z: "yes, recommended for Q1-Q9" (lead R-380).
- Q1: core.ignoreCase=false joins the status, hidden-state and removal pins.
- Q2: (1) an enclosing row is registration-mismatch; (2) an lstat-found
  <dir>/.git in a directory holding an index entry is contains-submodule,
  counted in the filesystem budget, at planning and again at revalidation.
- Q3: (a) the ceiling's note names the administrative directory, superseding
  R-365's prune-unread text; (b) plain prune texts warn of the dry run.
- Q4, Q5: the --force checks first; gate 7's steps and the non-drain line.
- Q8: status_warned() counts a blank line; Q9 (b): a symbolic-link
  registered path is registration-mismatch.
The A-items (T7, T10, T19, T14, T15, T16, LOW-5; lead R-385) ride along from
e702a65. Lead acceptance: R-385, R-395, R-398.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Doc round 25 (ecd0a7e; lead R-379): R-365's docs, and change 1's tasks.md
restored to main's copy (L1, R-377). Doc round 26 (8e90096, 8abd901,
497f5a3; lead R-393, R-394, R-397): the A-items, Brett Heap's Q1 to Q5, Q8
and Q9 (b), revalidation's nested .git checks, and traceability rows
118 to 124 for #19's rounds (q) and (r).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y run (R-397)

test_clean_a_nested_repository_made_after_the_plan, in tests/test_clean_revalidation.py: a batch whose first target tracks tools/run.sh; at the confirmation question the hook runs `git init` in that tracked directory, and the run stops at the first target as contains-submodule (exit 2, the later target batch-stopped), no removal child runs, and every tree, the nested repository's .git included, is still there.

It pins Brett Heap's Q2 (2) (lead R-380) through a real --apply run, as lead R-397 asks; before it, only two in-process tests in tests/test_clean_apply.py pinned the revalidation step. The batch() fixture takes an optional setup(tree), passed to merged_worktree(). The test fails against project at e702a65 (both targets removed) and passes at 41759a4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…w 123, and W140's items (R-394, R-405, R-406, R-407)

Docs only (specs/004-project-clean-all-safe/). No code and no test change; the branch is first fast-forwarded to c0841aa, which adds R-397's test.

T127 (W140 MEDIUM-1): its static check failed on the tree 004 has once #19 lands (git merge-tree --write-tree of c0841aa and 4512441, git archive into an empty directory): 129 D14 and D18 spans, 32 with no literal in tests/clean_support.py. Each is classed in T127's text. (a) 26 are not a text 004 prints and go into tuple X: 14 Git and project commands, 4 messages of Git's own, 5 words of Brett Heap's, 2 cites (R-284's letter, the parity rule) and 1 superseded text quoted as history. (b) 1, ADMIN, joins the placeholder list. (c) 5 are printed texts that project holds outside TEXT and NEXT_STEPS (project:7308, :551, :2426, :7464-7466 at c0841aa), each pinned by a test with its own literal; widening the source to TEXT and NEXT_STEPS would find none, so they are entries of X with that reason. The check passes on both trees (30 spans on the branch's own, 98 on the merged one) and still fails on a changed D18 string.

LOW-1: data-model.md step 7's paragraph and traceability's row 123 note carry R-406's measured residual (one _fs() call under min(5 s, the work remaining); warm at 1,000, 10,000 and 50,000 tracked directories; cold cache and deep nesting unmeasured, both fail closed).

LOW-3: the Q8 notes (spec.md FR-057, the contract, data-model.md) say "under Git's default `core.warnAmbiguousRefs`", copied by script from #19 at 4512441 (project-command spec.md:268-271), and that with the setting false the probe exits 0 with empty standard error, so no row is inspection-error on that account.

OBS: spec.md's Q3 (b) note on status is the lane's reading under R-380, and M1 is still open; the three project-command cites (data-model.md, tasks.md, plan.md) are labelled as the ratified copy's lines at 0050f3e, with 4512441's.

R-397's test (R-405): traceability row 123 names test_clean_a_nested_repository_made_after_the_plan (T083), so the named gaps go from six to five; T126's text and verify, T083, T084, the gap list and the tail say so, and the head sentence reads c0841aa.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… MEDIUM-1, R-409)

Lane 3's W139 MEDIUM-1: enclosing_rows() (Q2 (1)) and _read_row()'s
symbolic-link branch (Q9 (b)) sat in the shared worktree_rows(), which
repo_state() uses, so status, doctor and update reported an enclosing
worktree and a symlinked registered path as inspection-error with dirty
null, and doctor exited 1, where f3724fc read both rows and exited 0. Both
rules are clean's review safety (rs:13-16 at 4512441); pc states neither,
so its parity rule (pc:311-312) keeps the report commands at the
baseline's values. Lead call R-409: confine both to clean's plan.

- worktree_rows() and _read_row() take for_clean (default false). Only
  clean sets it: match_target() and build_plan() (planning, and the
  re-plan of push and delete-branch through build_plan()), and
  registry_twin(), revalidation's step 5 re-read, so clean's behaviour is
  unchanged. repo_state() leaves it false.
- R-67 (c)'s shared-path and twin rows stay in every command, as at
  f3724fc: shared_paths() and shared_identities() are not gated.
- tests/test_status_doctor_update.py: a new test on W139's fixture (wt-a
  moved with a symbolic link left at its registered path; w1 holding
  w1/sub/w2) asserts f3724fc's entries for every row in status, doctor
  and update --json (report only), doctor exit 0, and that clean
  --all-safe --json still excludes both rows as registration-mismatch.
  It fails at c0841aa (status, doctor and update subtests) and passes
  with f3724fc's project for those three; report_env() puts recording
  owner tools first on PATH and XDG_CONFIG_HOME and TMPDIR in the
  fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… R-409)

Lane 3's W139 LOW-2: pc:391-395, "A stale entry beside a worktree whose
path cannot be read", names project status --json, project doctor --json
and project update --json, and the test looped over status and doctor
only. It now reads update's report as well (no --apply), with exit 0 in
every state, under report_env(): recording owner tools first on PATH,
XDG_CONFIG_HOME and TMPDIR in the fixture, and none of the tools run.
W139 found update conforming on Git 2.43.0 and 2.36.6; the test passes on
both. Test only; project is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…W140 LOW-2, R-409)

Lane 3's W140 LOW-2: main's review-safety requirement "Cleanup protects
the process worktree" names the current worktree from the working
directory "including a directory nested within it", and no test ran
clean from such a directory; every clean cwd= in the suite was a
worktree's top level. The code was right (row_evidence()'s `current`,
active.startswith(tree + b"/")).

The new case in tests/test_clean_single.py, not tests/test_project.py
(FR-066, R-32), runs from sub/deeper inside a merged, clean, published
worktree: the report shows it current and merged-current, --all-safe
selects nothing and excludes it as merged-current, and the explicit
removal refuses with "Cannot remove the current worktree.", exit 2, in
human and JSON output, removing nothing. On a scratch mutant without the
nested branch of `current` it fails: (False, 'merged-removable') !=
(True, 'merged-current'). Test only; project is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OW-4, R-409)

Lane 3's W140 LOW-4: the comment over TEXT_FIXTURES's
partial_removal_note_locked in tests/clean_support.py still said the
note "waits on Brett Heap's word". He ruled it on 2026-10-22, "F-2: yes"
(the lead's R-263; traceability.md:307-308), and T16 already fixed the
same sentence in project. The comment now says so. Comment only; no
fixture text or behaviour changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or shares another (R-413)

Brett Heap ruled on 2026-10-23, "yes, recommended for M1 and M3" (lead
R-413). M1 needs no code. M3 (a): a row clean excludes as
registration-mismatch because its path is, or holds, the main worktree,
the common directory or another row (Q2 (1)), or because it shares its
path with another row, printed the class's next step, whose "or remove
the row by hand" deletes what it holds (lane 3's W133 MEDIUM-3).

- TEXT "next_step_registration_mismatch_holds", beside
  next_step_registration_mismatch_unread, byte for byte #19's M3 (a) at
  4512441 (197 characters). It names no git worktree prune, so it has no
  PRUNE_DRY_RUN and no unread form; it is the same in every mode.
- WorktreeRow.holds, never serialized: worktree_rows() sets it for a row
  in enclosing_rows() (clean's plan alone), shared_identities() (R-67
  (c)) or shared_paths() (R-57 L3).
- excluded_groups() gives those registration-mismatch rows a group of
  their own, first, with the new step; the reason's other rows (the
  core.worktree and core.bare redirect, Q9 (b)'s symbolic-link row) keep
  the class's step, and its unread form where a path was unread.
- Tests (tests/test_clean_gates.py): a new test with w1 holding
  w1/sub/w2, a path listed twice (R-57 L3), a worktree under two
  spellings (R-67 (c)) and a symbolic-link row (Q9 (b)) checks both
  groups in the --all-safe preview, in --all-safe --apply --json's human
  text on standard error, in single mode naming w1, and beside an unread
  row; test_clean_the_main_worktree_inside_a_linked_worktree now checks
  W's step in batch and single mode and that no line says "remove the row
  by hand". Both fail at c0841aa, where all such rows print the class's
  step. tests/clean_support.py gains the fixture key.
- Not changed, for the lead: such a row is counted toward
  inspection-incomplete, whose remedy_inspection_incomplete_redirect
  (and its _unread form) still says "or remove the row by hand after
  review".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rett Heap's M3 (a) next step (R-409, R-413, R-416)
…eap's M1 and M3, and T127 on #19's 54e869b (R-409, R-413, R-415, R-416)

Docs only (README.md and specs/004-project-clean-all-safe/). The branch first merges impl/004-q-build at d640539 (the code round, c0841aa..d640539), with no rebase.

The code round (R-409, R-416). W139 MEDIUM-1: Q2 (1) and Q9 (b) apply in clean's plan alone (worktree_rows and _read_row take for_clean), so status, doctor and update report those rows as the baseline does. FR-055's two notes, the contract, the data model, T031 and the README now say so, with a corrective note citing R-409 and #19's design.md:591-598 at 54e869b (the brief's :588-596 are dab8a64's lines); T031's verify names the new worktree_rows signature. M3 (a)'s step, next_step_registration_mismatch_holds (197 characters, copied from project by script), is in the README's next-step table and preview text, the contract's next-step paragraph and preview layout, the data model's next steps, FR-028's note, and T011, T052 and T061. The code round's tests are listed in their tasks (T061, T071, T115) and in traceability's tail.

Brett Heap's M1 (a) and M3 (a) are RULED (R-413, 2026-10-23T21:10:31Z, "yes, recommended for M1 and M3"). FR-064's Q3 (b) note and the contract's matching paragraph record M1 in his words with the time; FR-028's note records M3.

W146's items (R-415): contracts/project-clean.md's note that #19 does not carry F-6 and F-10 says #19 now states both (project-clean spec.md:207-214 and :216-240, scenarios :296 and :308, at 54e869b); the cites of #19's revalidation sentence and reason list are spec.md:112-114 and :133-134 at 54e869b, with round (s)'s wording quoted (the contract, data-model.md, FR-046, FR-047, T084; W146's :113-116 and :133-135 are off by a line, checked by script).

T127 passes on this branch's own tree (30 spans) and on the tree that git merge-tree makes with #19's 54e869b (102 spans of 139), and still fails on a changed D18 string. Three spans had no literal: (a) the label "The main worktree's recommendation", a scenario title; (c) the two baseline recommendations round (s) quotes as the main row's former text, which project still prints for every other row (RECOMMENDATIONS, project:2451-2452) and tests pin with their own literals (the lead's R-412 called them (a)); all three are entries of tuple X. M3's step needs no entry: its fixture is a literal in tests/clean_support.py, and at c0841aa it would have been a fourth missing span.

Traceability gains rows 125 ("The main worktree's recommendation", round (s)) and 126 ("A worktree whose path holds the main worktree", round (t)), titles copied by script, each a named gap with its covering tests; T002 and T126 count 126 rows and seven gaps; the head sentence reads d640539.

Not done: the stale cite at project:775-776 (traceability.md:253-255) is in project's own comment, outside this round's files, as is tests/clean_support.py:647's (traceability.md:307-308). M4 is not documented.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… Heap's M3 (a), and four small items

- W139 MEDIUM-1 (lead R-409): Brett Heap's Q2 (1) and Q9 (b) apply only in
  clean's plan (worktree_rows and _read_row take for_clean); status, doctor
  and update report those rows as the baseline does, doctor exiting 0.
- Brett Heap, 2026-10-23T21:10:31Z, "yes, recommended for M1 and M3" (lead
  R-413): an enclosing or shared-path row prints
  next_step_registration_mismatch_holds in its own group.
- R-397's --apply test for a .git made after the plan; update --json in the
  T9 test; clean from a nested directory; the T16 comment.
Accepted as R-405 and R-419: the full suite gives 520 OK (skipped=1) at d640539.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Doc round 27 (22cddaa; lead R-407, R-411, R-412): W140's items, T127 on the
landing tree, R-406's measured residual, and row 123 covered. Doc round 28
(e3c10c6; lead R-409, R-413, R-415, R-416): Q2 (1) and Q9 (b) as clean's
alone, M3's step, Brett Heap's M1 and M3 rulings, W146's items, T127 against
#19's 54e869b (102 spans, none missing), and traceability rows 125 and 126.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenSpec: add-project-clean-all-safe (batch worktree cleanup from the maintenance design packet)

1 participant