Skip to content

lane_setup: consult the problem sentence before building git argv, fold every receipt line, and give worktree_occupancy a reachable third state - #379

Merged
fdaviddpt merged 4 commits into
mainfrom
fix/368
Aug 20, 2026
Merged

lane_setup: consult the problem sentence before building git argv, fold every receipt line, and give worktree_occupancy a reachable third state#379
fdaviddpt merged 4 commits into
mainfrom
fix/368

Conversation

@fdaviddpt

@fdaviddpt fdaviddpt commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes #368
Closes #372
Closes #373

Three defects in scripts/lane_setup.py, all from the v0.7.0..be36015 release audit. One held the tag.

#368 -- resolve_base built git argv from a value oss_config had already refused

oss_config.load() deliberately returns an offending default_branch together with a sentence rather than stripping it. resolve_base() treated a loaded config as a usable one and passed the value into git fetch --quiet <remote> <branch>, where argv position 4 is read as an option when the name starts with a dash. Reproduced with _git stubbed, so nothing executed:

[['fetch', '--quiet', 'origin', '--upload-pack=true'],
 ['rev-parse', 'refs/remotes/origin/--upload-pack=true'], ...]

Three renderings disagreed: blocked() returned False and the process exited 0; commands/tick.md documents could-not-resolve as blocking with exit 3; and the config warn: line was rendered, after git fetch had already run. resolve_base now calls oss_config.default_branch_problem() before building any argv. No second validation rule -- the existing one is consulted, so one value keeps one rule (#345).

The judgment call the issue left open, checked rather than inherited. Every consumer of lane_setup.py was grepped and nothing shells it programmatically; its only callers are commands/tick.md and skills/manager/SKILL.md, both of which name the script for an agent to run and read the receipt from. So exit 3 is read as a stop, never as a crash by a script, and the existing state word was already right. No edit to commands/tick.md was needed and none was made.

#372 -- receipt() forged lines, and there were four fields, not three

Ranked forges; this is the one that held the tag. receipt() printed config-sourced values raw, so a newline forged receipt lines a reader cannot tell from the tool's own -- in output skills/manager/SKILL.md and commands/tick.md both instruct maintainers to paste verbatim into a developer brief. The file's own _one_line ("A newline in either forges a receipt line") was applied to detail and the board lines and skipped everywhere else.

The audit reached three fields. Measured here, four forge, each adding 2 lines:

field source
branch_pattern tracked .oss.json -- the filed case
worktree_root .oss.local.json
repo the --repo argv
an oss_config problem sentence a hostile JSON key

The fourth is the one that decided the shape of the fix. It needs no hostile value anywhere -- only a hostile key -- and oss_config cannot close it at its end without refusing to name the key that is wrong:

config warn: zz
base      : 1111111111111111111111111111111111111111 (origin/main): unknown key (typo, or a schema change nobody wrote down)

So the guard is at the single point where the receipt is joined, not on a list of fields: a per-field guard closes what somebody enumerated and leaves the next field added unguarded.

Deliberately not _one_line at that point. Its " ".join(text.split()) collapses runs of spaces, and every row is aligned by _row's {0:<10} padding -- folding the assembled line through it turns repo : x into repo : x and destroys the column the receipt is read by. Only the forging half is applied: every character outside printable ASCII becomes ?, which covers LF, CR, the C0 controls, DEL and U+2028/U+2029, and leaves spaces alone. _one_line still runs where it already ran, so this is additive.

A truncated line is marked. A cut line rendering as a complete one is this repository's own defect class pointed at its own receipt.

Whether oss_config should also grow a content rule for branch_pattern is a separate question and is deliberately not answered here (#345).

#373 -- worktree_occupancy's third state was unreachable

It rendered already exists / free / unknown and used os.path.exists, which swallows OSError, so an unreadable parent came back False and printed [free]. unknown was reachable only when path was falsy -- the one case it existed for could not produce it. Reproduced, deny confirmed by attempting the operation:

readable parent  -> True                     <- must-fire control
deny took? True  os.stat raises PermissionError 13
worktree_occupancy -> False                  (False renders as 'free')
doctor._dir_state  -> ('unreadable', ...)    same path, same run

Now os.stat is asked once and the exception in hand answers: FileNotFoundError/NotADirectoryError are absence, every other OSError is "could not look". No second question to the filesystem and no errno table -- Windows folds several Win32 codes onto ENOENT.

doctor._dir_state was deliberately not lifted into a shared module. It asks is this a directory where this asks is anything there; it has four call sites and its own tests inside a 5,000-line diagnostic; and the brief said to solve locally rather than refactor unreviewed. What holds the two together is test_the_two_classifiers_agree_on_the_same_path, which runs both on one fixture and fails if either changes its mind -- the second-measurement mechanism, not a test that states the same claim twice.

One gap written down and not closed

The audit spawn raised it and it is real: on Windows an over-long path arrives as FileNotFoundError, errno 2, winerror None -- no distinguishing signal -- so a derived path past MAX_PATH on a runner without LongPathsEnabled is classified as ordinary absence and printed [free]. That is #373's own shape, reachable through the one exception type the fix treats as safe. Not closed here, and not by a length check: MAX_PATH is conditional on a machine setting and a constant would be the errno table this function was rewritten to avoid. doctor._dir_state carries it identically, so it is one decision about two functions. Recorded in the docstring and handed back for filing; ranked misreports, which ships behind a filed issue.

Tests

27 new cases across three files. Assertions are on captured argv (#368) and on the rendered line count against a clean control (#372), not on exit codes or regex matches: a version that ran the fetch and then failed, or folded the value and printed it elsewhere, would satisfy the weaker form. Every "must not fire" case is paired with a "must fire" one in the same fixture, and #373's deny is confirmed by attempting the exact operation, skipping loudly with the platform and what went untested when it does not take.

Red and green were watched separately for each issue: #368 4 failed, 4 passed then 8 passed; #372 8 failed, 3 passed then 11 passed; #373 3 failed, 5 passed then 8 passed. In every red run the passing cases are the controls, which must pass in both states. Full suite 2637 passed, 2 skipped, coverage 92.52%.

Verified by the maintainer

Independent red re-run: all three new test files on origin/main in a detached worktree,
scripts/lane_setup.py untouched.

15 failed, 12 passed in 0.18s

That is 4+8+3 failed and 4+3+5 passed — exactly the three per-issue splits the report claims,
reproduced in one run rather than three, so the controls are the same twelve in both readings.

The misreports gap this PR records and does not close — an over-long path on Windows arriving as
FileNotFoundError, errno 2, winerror None and being classified as ordinary absence, in both
worktree_occupancy and doctor._dir_state — has been filed as #380, so the report-for-filing
item is discharged rather than left in a merged report nobody rereads.

fdaviddpt and others added 4 commits August 20, 2026 12:41
…reading the problem sentence oss_config already produced for it (#368)

`oss_config.load()` deliberately returns an offending `default_branch` together with a sentence rather than stripping it. `resolve_base` treated a loaded config as a usable one and passed the value into `git fetch --quiet <remote> <branch>`, where argv position 4 is read as an option when the name starts with a dash.

Three renderings disagreed about the same input: `blocked()` returned False and the process exited 0; `commands/tick.md` documents `could-not-resolve` as blocking with exit 3; and the `config warn:` line was rendered by `receipt()` after `git fetch` had already run -- accurate and late, the worst ordering.

`resolve_base` now calls `oss_config.default_branch_problem()` before building any argv, returning `could-not-resolve` carrying that sentence. No second validation rule is added: one value keeps one rule (#345). The state word and exit 3 are unchanged, so `commands/tick.md` needed no edit -- and no consumer reads the exit code programmatically; it is read by agents from the receipt.

`tests/test_lane_setup_368.py` asserts on the captured argv rather than on the exit code, since a version that ran the fetch and then failed would satisfy an exit-code assertion. Every silence assertion is paired with a positive control in the same fixture, including one that measures `branch_occupancy`'s prefixing rather than trusting the issue's prose about it.

Closes #368

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HuQoqn4iLmW2ULpvhKnwj
…ation the call site does not use

Reviewer finding on 6139a98. `resolve_base` passes an explicit 300 to `_one_line`; the comment beside the head-comparison said 200, which is the function's default and not the limit in play. The comparison itself was already written to be independent of the limit -- only the sentence describing it was wrong.

Co-Authored-By: Max <noreply>
…nd worktree_occupancy's third state was unreachable (#372, #373)

#372 (forges, held the tag). receipt() printed config values raw, so a newline in one forged receipt lines a reader cannot tell from the tool's own -- in output SKILL.md and tick.md both instruct maintainers to paste verbatim into a developer brief. The file's own `_one_line` ("A newline in either forges a receipt line") was applied to `detail` and the board lines and skipped everywhere else.

Four fields were measured forging, not the three the audit reached: `branch_pattern`, `worktree_root`, the `--repo` argv, and -- found while fixing those -- an oss_config **problem sentence** built from a hostile JSON *key*. That fourth one needs no hostile value anywhere, and oss_config cannot close it at its end without refusing to name the key that is wrong. So the guard is at the single point where the receipt is joined, not on a list of fields: a per-field guard closes what somebody enumerated and leaves the next field added unguarded.

Deliberately not `_one_line` at that point. Its `" ".join(split())` collapses runs of spaces and every row is aligned by `_row`'s `{0:<10}` padding, so folding the assembled line through it turns `repo      : x` into `repo : x`. Only the forging half is applied -- every character outside printable ASCII becomes `?` -- and a truncated line is marked, since a cut line rendering as a complete one is this repo's own defect class pointed at its own receipt.

#373 (misreports). worktree_occupancy used `os.path.exists`, which swallows OSError, so an unreadable parent came back False and the receipt printed `[free]`. It now asks `os.stat` once and lets the exception in hand answer: FileNotFoundError/NotADirectoryError are absence, every other OSError is "could not look". No second question to the filesystem, no errno table -- Windows folds several Win32 codes onto ENOENT.

doctor._dir_state (#363, same delta) answers `unreadable` on the identical path and was deliberately NOT lifted into a shared module: it asks "is this a directory" where this asks "is anything there", and it has four call sites and its own tests inside a 5,000-line diagnostic. What holds the two together is a test running both on one fixture that fails if either changes its mind -- not the prose saying they agree.

Tests assert the rendered line count against a clean control rather than that a regex matched, and every deny is confirmed by attempting the exact operation, skipping loudly with the platform and what went untested when it does not take.

Closes #372
Closes #373

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HuQoqn4iLmW2ULpvhKnwj
… does not close

Raised by the audit spawn on 8cb200e and judged real. CLAUDE.md's own measurement is that an over-long path arrives on Windows as FileNotFoundError, errno 2, winerror None -- no distinguishing signal -- so a derived path past MAX_PATH on a runner without LongPathsEnabled is classified as ordinary absence and printed [free], which is the confident absence #373 exists to close, reachable through the one exception type the fix treats as safe.

Not closed here, and deliberately not by a length check: MAX_PATH is conditional on a machine setting, and a constant would be exactly the errno-table antipattern this function was rewritten to avoid. doctor._dir_state carries the identical gap by the identical argument, so closing it is one decision about two functions and belongs in its own change. Filed for the maintainer rather than fixed; ranked misreports, which ships behind a filed issue.

Co-Authored-By: Max <noreply>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment