Conversation
_undeclared_rows() caught (RuntimeError, OSError) around documents() and returned [] on either, folding an unreadable phases directory into "no undeclared files found" -- guard and bypass in one function. OSError now returns an unreadable row; RuntimeError (no spine) still returns [] since that is already reported by the spine's own missing row. No live route to the bypass was demonstrated at HEAD; the fix closes it before documents() grows an arm that reaches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wr8bkvu4ac3hpnHcG99fec
…589) The comment added by the previous commit claimed the RuntimeError arm's case is "already reported" by check()'s own missing row, unconditionally. That only holds when documents() raises RuntimeError because the spine is genuinely absent -- is_file() also folds a stat failure (e.g. a permission- denied parent directory) into the same False, and check()'s own spine read_bytes() call catches only FileNotFoundError, so that narrower case reaches this arm after check() has already raised uncaught rather than after a missing row was emitted. Found by both self-review spawns independently. Not fixed here -- it predates #589 and is not the fold this issue is about -- the comment now says so instead of overclaiming. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wr8bkvu4ac3hpnHcG99fec
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
scripts/skill_phases.py's_undeclared_rows()caught(RuntimeError, OSError)around its call tomanager_docs.documents()and returned[]on either -- folding an unreadable phases directory into the same shape as "no undeclared files found", the state the module's own docstring six lines above promises to preserve. Guard and bypass in one function.Fixes #589.
What changed
OSErrornow returns a single-item list with a fully-populatedunreadablerow (matching the sibling row shape emitted a few lines below fordocuments()'s ownunreadablemessages).RuntimeError(no spine at root) still returns[], since that case is already surfaced elsewhere as the spine document's ownmissingrow.Honesty about reachability
The release-gate audit that filed this issue could not demonstrate a live route to the bypass at HEAD, and neither could I:
documents()already catchesOSErrorinternally around its owniterdir()and returns it via theunreadablelist rather than raising, and a genuinely unreadable spine would already raise insidecheck()'s own unguardedread_bytes()call before_undeclared_rows()is ever reached. So this is a latent bypass with no reachable caller today, not a live defect -- the fix closes it beforedocuments()grows an arm that reaches it. Tests reproduce all three of the auditor's own control arms via monkeypatch: a healthy-tree positive control,documents()raisingPermissionError, anddocuments()returning a realunreadablemessage.Two below-bar items from the brief, and a third one self-review found
The brief named two adjacent observations below the filing bar and asked me to decide whether either belongs in this diff. Both stay out:
skill_phases.py:118-120'scheck()catching onlyFileNotFoundErroraround the spine's ownread_bytes()call, andmanager_docs.py:68'sFileNotFoundErrorhandling for the phases directory without the deepest-lookable-ancestor disambiguationdoctor._dir_statebuilt for exactly this (#380). Both are real, both are one sentence to state, and neither is reachable through this diff's own footprint without widening scope into a design decision (what state does an unreadable-but-not-missing spine get?) that #589 was not scoped to make. below-bar.Self-review (both the reviewer and the auditor spawn, independently) found the same class in the comment I wrote for the
RuntimeErrorarm: it claimed that case is unconditionally "already reported" bycheck()'s ownmissingrow, which only holds for a genuinely absent spine --Path.is_file()also folds a stat failure into the sameFalse, andcheck()'s narrowexcept FileNotFoundError(the same pre-existing gap named above) means that narrower case reaches theRuntimeErrorarm only aftercheck()has already raised uncaught, not after amissingrow was emitted. Fixed in a follow-up commit by rewording the comment rather than widening the except clause -- same design-decision reasoning as the two items above, so the comment now says so instead of overclaiming. below-bar.Docs
changelog.d/589.fixed.mdadded.README.md(the repo's onlydocs_targetsentry) opened and read against this diff -- it does not describeskill_phases.py's internal row states, sono-change-needed.scripts/doctor.pydoes not referenceskill_phases.pyat all, so the diagnostic is unaffected by construction --already covered, confirmed by grep.Tests
python3 -m pytest tests/test_skill_phases_undeclared_bypass_589.py -q --no-cov: red before the fix (documents() raised PermissionError but _undeclared_rows() returned [] -- the unreadable state was folded into 'nothing found'), green after (16 passed alongsidetest_skill_phase_split.py). Guard sweep vialane_setup.py --lanenamedtest_claude_md_currency.py,test_gate_state_consumers_328.py,test_unwired_scripts_253.py; all green.Verified by the maintainer
Re-ran the new test file against
origin/mainwith the fix absent, in a detached worktree:One failed, two passed — the three-arm control behaving as designed. Arms 1 and 3 pass before the fix, which is exactly right: arm 1 is the positive control that stops the assertion being vacuous, and arm 3 proves the
unreadablestate is reachable at all. Only arm 2 discriminates, and it is the one that went red.The honesty here is the part worth keeping
This closes a latent bypass, not a live defect, and the pull request says so. The round-2 auditor could not demonstrate a route to it at HEAD, both self-review spawns confirmed that independently, and the lane did not manufacture reachability to make the fix look more urgent than it is. That was the outcome I asked for and the easiest one to quietly not deliver — a lane wanting a stronger-looking diff would have written a reachable-sounding scenario and nobody reading the merge would have checked.
The value is that it stops being latent the moment
documents()grows an arm that raises, which is a cheap thing to buy now and an expensive one to discover later.The self-review round found a defect in the diff's own new prose
Both spawns independently flagged that the comment added on the
except RuntimeError: return []arm overclaimed — it said that case is unconditionally already reported bycheck()'s ownmissinghandling, which is not true for an unreadable-but-not-missing spine. Fixed in46e5176.That is the second lane in two days to ship a wrong sentence alongside a right fix and have its own reviewers catch it. Worth noting as a pattern rather than an incident: the prose explaining a subtle fix is written last, under the least scrutiny, and is the part that outlives the diff.
The two adjacent gaps stay out, correctly
skill_phases.py:118-120(except FileNotFoundErroronly, so aPermissionErroron a phase file escapescheck()) andmanager_docs.py:68(FileNotFoundErrorread as a genuine absence without the deepest-lookable-ancestor disambiguation #380 built for exactly that) were both judged below-bar and written up rather than fixed. I agree with the routing: each needs a design decision, andmanager_docs.py:68in particular is the WindowsMAX_PATHcase where an over-long name arrives as errno 2 withwinerrorNone — indistinguishable from a real miss, and not something to settle inside a lane scoped to oneexceptclause.Provenance: filed by the
v0.14.0release gate, round 2, dispatch tokenrel-0140-r2-6d21ef58c4, and carried past the tag deliberately as non-blocking.