Skip to content

Deliver: Gate Legibility Lints (#98, #99, #101) - #103

Merged
seanrreid merged 20 commits into
mainfrom
rad/gate-legibility-lints
Aug 10, 2026
Merged

Deliver: Gate Legibility Lints (#98, #99, #101)#103
seanrreid merged 20 commits into
mainfrom
rad/gate-legibility-lints

Conversation

@seanrreid

Copy link
Copy Markdown
Owner

Delivers the approved plan .agents/plans/gate-legibility-lints.md, bundling three
gate-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 have
guaranteed 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_anchors qualified any
path:NNN token ending in a known extension, so prose citing `spine.js:51-53`
yielded the bare path spine.js, existence-checked at repo root, warning "stale
premise: spine.js not found" while the file sat at harness/spine.js. New
resolve_anchor_path helper: slash-bearing tokens pass through unchanged; a bare
basename resolves via git ls-files only on a unique match — zero or ≥2 matches
emit 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.sh builds its declared-scope set
from the Files-in-Scope File column only, so a git mv destination described in
the 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.sh catches
the same mistake at authoring time, and both /rad-plan and /rad-adopt now document
that 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.sh was
committed 100644 while every sibling was 100755; invoked directly it exited 126,
and CI masked it via bash "$t". Fixed as a true mode change, plus a new
committed-mode pass in lint-shell-safety.sh that reads modes from the git index
rather than the filesystem — a local chmod that was never staged is precisely the
state 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-*.sh and globs non-recursively — and the sole offender
was a test-*.sh. Folding it in would have produced a check structurally incapable
of catching the bug it was written for. The separate git ls-files -s pass recurses
into scripts/hooks/, which makes the *.sh.sample exclusion load-bearing rather than
decorative.

This was surfaced by writing the plan's ## Program Design section against the real
tree before delivery — the #79 advisory earning its keep.

Verification

The plan set its own acceptance fixture, and it is met: running
scripts/lint-plan.sh on this plan's own doc emitted 3 false stale-premise warnings
before
(spine.js, ci.yml, lint-shell-safety.sh — all three files exist) and
emits 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:

    Scenario Exit Output
    git mv of a declared file → undeclared path 1 names both dest and src
    Both source and destination declared 0 passes
    Unrelated new file 1 plain message, no rename hint
  • Index-not-filesystem proven: re-staging the original bug with
    git update-index --chmod=-x while the file remained +x on disk is caught, exit 1.

  • Tests: 42 → 65 cases across four suites (check-scope 3→7, lint-shell-safety
    11→15, plan-paths 9→16, lint-plan 19→27). All 14 shell suites pass; every
    pre-existing case kept its description, asserted exit code, and pass status.

  • Harness: npm test --prefix harness216/216.

  • Gates: check-scope.sh → 10 files, all in scope. check-tests-present.sh → 4
    test files present.

  • All 14 scripts/test-*.sh are now committed 100755; no content edit dropped a mode.

  • lint-shell-safety-baseline.txt is byte-identical — no grandfathering needed.

Out-of-scope finding, filed not fixed

#102scripts/lib/plan-paths.sh claims "bash 3.2 (macOS stock) compatible" in its
header but has not parsed under bash 3.2 since the freshness lint landed, so on a stock
macOS box lint-plan.sh exits 2 with a syntax error instead of linting. Pre-existing on
main, unrelated to #98, and outside this plan's approved scope — filed rather than
folded in.

Review notes

  • scripts/lint-shell-safety.sh now holds itself to its own rule: the new
    git -C "$SCRIPTS_DIR" call tripped the lint's tainted-input check, resolved with a
    shape 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 logged
    reason, where set -e previously aborted with git's raw 128 and no message. Still
    fail-closed and non-zero; pinned by a test case asserting 2.
  • Two wave tasks named a second required file only in prose, not in their File: field
    — flagged at Gate 1 and corrected in the wave prompts. Both rad-plan.md and
    rad-adopt.md (AC#6) and both test-plan-paths.sh and test-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

seanrreid and others added 20 commits August 10, 2026 08:58
Adopted-From: #98, #99, #101 — gate-legibility defects found while delivering #91
Author: architect
Waves: 4
Tasks: 10
Out-of-scope deps: no

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 2 execution log: Task 2.1 (8a8e9f3) and Task 2.2 (8fc17b8), the
acceptance-fixture before/after counts, the population check across all 32
plans, and one pre-existing bash 3.2 parse concern in plan-paths.sh.
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>
@seanrreid seanrreid added the rad:deliver RAD delivery PR label Aug 10, 2026
@seanrreid
seanrreid merged commit 16a5f91 into main Aug 10, 2026
7 checks passed
@seanrreid
seanrreid deleted the rad/gate-legibility-lints branch August 10, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rad:deliver RAD delivery PR

Projects

None yet

1 participant