Deliver: Gate Legibility Lints (#98, #99, #101) - #103
Merged
Conversation
The #79 advisory fires on this plan (4 waves + self-protected paths) and it carried no ## Program Design section. Capture the three artifacts before delivery: new/changed signatures, a control-flow sketch, and a file-tree diff. Sketching against the real tree surfaced four constraints the wave tasks did not state: - The committed-mode check cannot reuse lint-shell-safety.sh's existing scan loop, which skips test-*.sh and globs non-recursively — the sole #101 offender IS a test-*.sh, so folding it in would be structurally incapable of catching the bug. It needs a separate git ls-files -s pass. - That pass recurses into scripts/hooks/, making the *.sh.sample exclusion load-bearing rather than decorative. - check-scope.sh resolves its diff ref through a three-way fallback chain; the rename lookup must reuse the expression that succeeded, not recompute it. - plan_created_paths already exists and is already wired into the freshness filter, so Task 2.2 adds a second, broader suppression rather than building create-exemption from scratch. Also records the acceptance fixture: this plan's own lint emits three #98 false positives (spine.js, ci.yml, lint-shell-safety.sh) that must vanish after Wave 2 while the eleven self-protected warnings survive unchanged. Refs #98, #99, #101 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plan: .agents/plans/gate-legibility-lints.md (Status + approved event written by: node harness/cli.js approve)
Wave 1, Task 1.1 Validated: AC#7 — git ls-files -s reports 100755; direct invocation exits 0 (was 126) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ety lint Wave 1, Task 1.2 Validated: AC#8 — new cases 9/10/11 in scripts/test-lint-shell-safety.sh cover a 100644-committed script failing by name, .sh.sample + baseline .txt + README.md staying exempt, and a non-git scripts dir failing closed. Clean tree exits 0 with output byte-identical to pre-change. AC#9 — cases 1-8 unchanged in count and outcome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wave 2, Task 2.1 Validated: AC#1 — `spine.js:51` now resolves to `harness/spine.js` via resolve_anchor_path; AC#2 — an ambiguous basename (harness-ci.md, 3 tracked matches) emits nothing, a genuinely absent slashed path (harness/does-not-exist.js:12) still warns, and a git read failure returns 2 with a reason on stderr instead of resolving to an empty anchor set. scripts/lint-plan.sh on the plan: 3 stale-premise warnings → 0, 11 self-protected warnings unchanged, exit 0. test-plan-paths.sh 9/9 and test-lint-plan.sh 19/19 pass, counts unchanged.
…ed files Wave 2, Task 2.2 Validated: AC#3 — a Files-in-Scope path absent from disk now yields the "File in scope does not exist" warning only; at HEAD~1 the same fixture also emitted "stale premise: scripts/brand-new-thing.sh not found on origin/main". Existing plans with nothing missing produce byte-identical output (diff -q against the pre-2.2 run of the feature plan). AC#9 — test-lint-plan.sh 19/19 and test-plan-paths.sh 9/9 pass, counts unchanged. Across all 32 plans in .agents/plans/, stale-premise warnings drop 30 → 21 with zero new warnings introduced (comm -13 against the baseline run).
Wave 3, Task 3.1 Validated: AC#4 — fixture repo: git mv of a declared file to an undeclared path exits 1 naming both paths; an unrelated new file reports exactly as before; in-scope-only change still exits 0
Wave 3, Task 3.2 Validated: AC#5 — fixture plans: a git-mv Change cell with no destination row warns and names the missing path (exit 0, advisory only); adding the row silences it; a plan with no renames emits no new output
Wave 3, Task 3.3 Validated: AC#6 — both /rad-plan and /rad-adopt Files-in-Scope guidance now state that a rename declares source and destination as separate rows, and why (check-scope.sh reads the File column only)
Wave 3 execution log rows for tasks 3.1-3.3
Wave 4, Task 4.1 Validated: AC#4, AC#9 — 4 new cases (rename target names both paths + convention block; both rows declared passes; non-rename drift keeps the plain message with no hint; unresolvable diff range exits 2 with a logged reason) all assert an explicit exit code and an output substring against the real check-scope.sh; the 3 pre-existing cases are untouched in description, asserted code, and outcome (3 -> 7 cases, ALL PASS).
Wave 4, Task 4.2 Validated: AC#8, AC#9 — audited the 11 existing cases, added 4 that close real gaps (index-vs-filesystem in the passing direction, subdirectory recursion, *.mjs coverage, the scripts-dir path-shape guard); suite now 15/15 ALL PASS with all 11 pre-existing cases unchanged in description, asserted exit code, and outcome; scripts/lint-shell-safety.sh on the real repo still exits 0 with unchanged output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wave 4, Task 4.3 Validated: AC#1, AC#2, AC#3, AC#5 — test-plan-paths.sh +7 cases covering resolve_anchor_path (unique/ambiguous/zero/slash-bearing) plus the fail-closed git read error (exit 2 + stderr reason, asserted from a non-repo cwd under GIT_CEILING_DIRECTORIES) and plan_cited_anchors' sentinel propagation; test-lint-plan.sh +8 cases covering missing-in-scope suppression (one fact, one finding) with a byte-exact clean-plan parity assertion, and the rename advisory across all three phrasings (git mv | rename | →), silenced by a declared destination row, silent with no rename, and asserted exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Delivers the approved plan
.agents/plans/gate-legibility-lints.md, bundling threegate-legibility defects found while delivering #91.
Closes #98
Closes #99
Closes #101
Why these three ship together
Each is a deterministic check whose output misleads the reader — the same family
as #91. Two of them edit
scripts/lint-plan.sh, so splitting them would haveguaranteed a conflict.
The shared cost is calibration: a check that cries wolf, or fails for a reason the
author cannot infer, trains people to wave it through — which is exactly when a real
violation slips past. Every verdict and severity in this PR is unchanged. Only
messages, false positives, and documentation move.
What changed
#98 — bare-basename anchors false-positive.
plan_cited_anchorsqualified anypath:NNNtoken ending in a known extension, so prose citing`spine.js:51-53`yielded the bare path
spine.js, existence-checked at repo root, warning "stalepremise: spine.js not found" while the file sat at
harness/spine.js. Newresolve_anchor_pathhelper: slash-bearing tokens pass through unchanged; a barebasename resolves via
git ls-filesonly on a unique match — zero or ≥2 matchesemit nothing, because a guess is worse than no signal. Separately, a path already
reported by "File in scope does not exist" no longer also produces a stale-premise
warning. One fact, one finding.
#99 — renames read as scope drift.
check-scope.shbuilds its declared-scope setfrom the Files-in-Scope File column only, so a
git mvdestination described inthe prose Change column was invisible and every rename reported its own target as
drift. The check still fails — the message now names the deleted counterpart and
explains the two-row convention. A new plan-time advisory in
lint-plan.shcatchesthe same mistake at authoring time, and both
/rad-planand/rad-adoptnow documentthat a rename declares both paths as rows.
Deliberately not done: parsing the prose Change column into the scope set. Guessing
at natural language inside a fail-closed gate is worse than an honest failure
(plan Non-Goal).
#101 — a script committed non-executable.
scripts/test-check-scope.shwascommitted
100644while every sibling was100755; invoked directly it exited 126,and CI masked it via
bash "$t". Fixed as a true mode change, plus a newcommitted-mode pass in
lint-shell-safety.shthat reads modes from the git indexrather than the filesystem — a local
chmodthat was never staged is precisely thestate the bug lived in, and a filesystem stat would have seen nothing wrong.
Design note worth reviewing
The committed-mode check is a separate pass, not an extension of the existing scan
loop. That loop skips
test-*.shand globs non-recursively — and the sole offenderwas a
test-*.sh. Folding it in would have produced a check structurally incapableof catching the bug it was written for. The separate
git ls-files -spass recursesinto
scripts/hooks/, which makes the*.sh.sampleexclusion load-bearing rather thandecorative.
This was surfaced by writing the plan's
## Program Designsection against the realtree before delivery — the #79 advisory earning its keep.
Verification
The plan set its own acceptance fixture, and it is met: running
scripts/lint-plan.shon this plan's own doc emitted 3 false stale-premise warningsbefore (
spine.js,ci.yml,lint-shell-safety.sh— all three files exist) andemits 0 after, while all 11 self-protected warnings survive unchanged, exit 0.
Population effect: stale-premise warnings across all 32 plans drop 30 → 21,
with zero new warnings introduced. Noise reduction, not a reshuffle.
True positives preserved: a plan citing a genuinely absent path still warns.
Verdicts unchanged, verified in an independent fixture repo:
git mvof a declared file → undeclared pathIndex-not-filesystem proven: re-staging the original bug with
git update-index --chmod=-xwhile the file remained+xon disk is caught, exit 1.Tests: 42 → 65 cases across four suites (
check-scope3→7,lint-shell-safety11→15,
plan-paths9→16,lint-plan19→27). All 14 shell suites pass; everypre-existing case kept its description, asserted exit code, and pass status.
Harness:
npm test --prefix harness→ 216/216.Gates:
check-scope.sh→ 10 files, all in scope.check-tests-present.sh→ 4test files present.
All 14
scripts/test-*.share now committed100755; no content edit dropped a mode.lint-shell-safety-baseline.txtis byte-identical — no grandfathering needed.Out-of-scope finding, filed not fixed
#102 —
scripts/lib/plan-paths.shclaims "bash 3.2 (macOS stock) compatible" in itsheader but has not parsed under bash 3.2 since the freshness lint landed, so on a stock
macOS box
lint-plan.shexits 2 with a syntax error instead of linting. Pre-existing onmain, unrelated to #98, and outside this plan's approved scope — filed rather thanfolded in.
Review notes
scripts/lint-shell-safety.shnow holds itself to its own rule: the newgit -C "$SCRIPTS_DIR"call tripped the lint's tainted-input check, resolved with ashape guard (exit 2 on whitespace/metacharacters) rather than a baseline entry.
scripts/check-scope.sh's unresolvable-diff-range path now exits 2 with a loggedreason, where
set -epreviously aborted with git's raw 128 and no message. Stillfail-closed and non-zero; pinned by a test case asserting 2.
File:field— flagged at Gate 1 and corrected in the wave prompts. Both
rad-plan.mdandrad-adopt.md(AC#6) and bothtest-plan-paths.shandtest-lint-plan.sh(AC#5)landed. Ironic given check-scope.sh cannot express a rename: a plan's git mv target always reports as out-of-scope drift #99 is about paths that live only in prose.
Full execution log:
.agents/logs/gate-legibility-lints-2026-08-10.md