Skip to content

docs: Claude lane contract close-out (Phase 5) - #285

Merged
kyle-sexton merged 4 commits into
mainfrom
docs/claude-lane-contract-close-out
Jul 29, 2026
Merged

docs: Claude lane contract close-out (Phase 5)#285
kyle-sexton merged 4 commits into
mainfrom
docs/claude-lane-contract-close-out

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5's documentation close-out for the claude-review-lanes plan: bring
README.md's Claude lane contract and CLAUDE.md's security ground rules up to
what v0.9.1 (c136b27) actually ships.

README's lane contract gains the behavior Phase 2 added and Phase 5 owes the
consumer:

  • paths-file — where the security lane's pattern list lives, the
    paths-wins-when-non-empty precedence, the base-branch-only read that stops a
    PR editing the file to skip its own review, and the absent/unreadable
    fail-open.
  • Fail-closed execution contract — the security lane's security-review is a
    separate required context, not ci-status; its claim is that a pass RAN;
    success/neutral/skipped all satisfy a required check, so an in-scope
    non-run must go red.
  • What that check does NOT prove — four legitimate non-runs are name-stable
    job-level skips a ruleset reads as success (fork PR, out-of-scope PR,
    skip-listed actor, kill-switched lane), and a fifth is not a skip at all: a
    superseded-head run retires itself step by step and reports a green job
    having reviewed nothing.
  • Per-lane trigger cadenceclaude-review's draft-PR skip, and that
    claude-e2e-verify gates on nothing but its kill-switches, so the most
    expensive lane has the loosest trigger.
  • Bounded retry — two attempts, the zero-assistant-turns gate, the auth-class
    exclusion, the jittered backoff, and the review-lane/e2e divergence on an
    unparsable execution file.
  • Review-count cap, kill-switches, the all-repositories variable
    visibility deviation, and the sync-managed caller-component pointer.

CLAUDE.md's security section heading widened from claude-review alone to all
three lanes. That widening made load-bearing bullets assert rules a covered lane
violates by design, all corrected here:

  • No untrusted checkout before the action was never the real invariant — all
    three lanes check out before the action step and the default pull_request
    checkout is the merge ref, so PR-authored content is always in the workspace.
    Presence is not the hazard, execution is. Restated accordingly, with
    claude-e2e-verify's deliberate exception explicit.
  • The review degrades to a warning on fork PRs is false for
    claude-security-review, which skips at job level so fail-closed cannot pin
    every fork PR red.
  • Keep display_report off (defaults) flattened three different mechanisms:
    hardcoded on the security lane, unset on e2e, caller-flippable on
    claude-review.

The 2f-addendum record — that a kill-switched security lane leaves NO lane
reporting security findings — is present in both lane headers and in README.

Test plan

Every behavioral claim in the prose was checked line by line against the shipped
workflow source at the released SHA, not against main. A fresh-context
verifier then re-derived the same table independently, with the rationale
withheld; its findings are folded into 455a752.

  • git show c136b27:.github/workflows/claude-security-review.ymlpaths-file
    default '' and conventional path (:169-185), precedence (:342, :392-394),
    base-ref read (:349), fail-open with warning (:358-361, :395-399), both-empty
    no-filter (:401-404), job-level kill-switches (:508-515), fail-closed step
    (:858-864), fork skip (:508-515, rationale :128-135), auth predicate
    (:696-702), jitter RANDOM % 30 (:755).
  • The green-without-review path: every step gated on the freshness guard
    (:569, :576, :651, :800, :830) leaves review-failed unset, so the
    fail-closed step skips too (:860) and the job reports success.
  • git show c136b27:.github/workflows/claude-review.yml — draft skip (:239-240),
    review-count cap and its fail-open/soft-cap semantics (:160-172, :287-345),
    counter upserted only on review-failed == 'false' (:722-723), retry gate
    honoring both the freshness guard and the cap (:491-494), zero-turn proof
    requirement (:511-521), display-report as a caller input (:122-128).
  • git show c136b27:.github/workflows/claude-e2e-verify.yml — retries on an
    unparsable execution file (:460-469), never sets track_progress, job gate
    carries only the two kill-switches (:186-188), executes PR code before the
    action step (:277-318), fork degradation (:52-54).
  • All three headers cite CLAUDE.md for the security rationale
    (claude-review.yml:54, claude-security-review.yml:121,
    claude-e2e-verify.yml:41) — they cite the file, not the section, which is
    what the heading now says.
  • Org variable visibility verified live:
    gh api "orgs/melodic-software/actions/variables?per_page=100" returns all four
    CLAUDE_*_DISABLED variables at all against ten other org variables at
    selected — the deviation the README documents.
  • npx markdownlint-cli2@0.23.1 README.md CLAUDE.md → 0 issues.
  • Phase 5 Sanity Check greps: grep -c "max-reviews-per-pr" README.md == 1,
    grep -c "CLAUDE_LANES_DISABLED" README.md == 1.

Docs-only — no workflow, action, or test file is touched, so runtime behavior is
unchanged.

Related

No linked issue. Phase 5 of docs/topics/claude-review-lanes/PLAN.md; the
remaining Phase 5 items (medley REVIEW.md equivalence, standards repo-local
caller blob-hash equivalence, the #256 trailer-parsing comment, and the
do-not-break sweep) are a separate change.

Two source-side observations surfaced by verification, deliberately NOT fixed
here because this PR is docs-only:

  • claude-e2e-verify.yml:75-76 calls CLAUDE_CODE_OAUTH_TOKEN "an org
    credential scoped to selected repos in the UI", contradicting
    :161-163, claude-review.yml:196-201, claude-security-review.yml:266-271
    and CLAUDE.md, which all say all-repositories. The live value is
    all-repositories; that lane's persisted-credential residual analysis rests on
    the wrong premise until reconciled.
  • claude-security-review.yml:155-156 documents the paths input as "Actions
    paths: filter syntax", but :410-449 implements it with git init plus a
    root-anchored .gitignore and git check-ignore. Equivalent for ordinary
    globs; an exotic pattern (trailing /, negation ordering, character classes)
    follows gitignore rules, not Actions rules.

🤖 Generated with Claude Code

https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb

kyle-sexton and others added 2 commits July 28, 2026 23:09
Work-in-progress output from an in-flight worker that died with its
session before reporting or being verified. Committed as-is to make it
durable and diffable for a fresh-context verifier; not yet reviewed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
Reviews the unverified WIP commit against the shipped workflow source at
c136b27 (v0.9.1) and fixes what the prose got wrong or left out.

CLAUDE.md's security section heading was widened from claude-review alone
to all three lanes, which left two bullets asserting rules a covered lane
violates by design:

- "No untrusted checkout before the action" is false for
  claude-e2e-verify, whose whole purpose is to build, serve, and
  browser-drive the PR head. Scoped the rule to the review lanes and made
  the e2e exception explicit, pointing at the header that records the
  persisted-credential residual and its bound.
- "the review degrades to a warning" on fork PRs is true for
  claude-review and claude-e2e-verify but false for
  claude-security-review, which skips the job at job level so the
  fail-closed mapping cannot pin every fork PR red. Split the mechanism
  per lane and recorded that the resulting skip reads as success.

Also swept the remaining bullets for singular-voice leftovers.

README gains the fail-closed execution contract the new retry prose
forward-referenced but never established: the security lane's check is a
separate required context, its claim is that a pass RAN, and a
legitimate non-run (fork, out-of-scope, skip-actor, kill-switch) is a
name-stable skip a ruleset reads as success. Adds the draft-PR skip to
the cadence paragraph, ties the review counter to SUCCESSFUL reviews,
and rewrites the adoption pointer so it stops carrying in-flight PR
state that drifts.

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

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 1s —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Warning

Automated review did not complete — this is an infrastructure failure, not a review verdict.

Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."

Re-run the job to retry the review. A new push does not re-trigger this lane.
An automatic retry may already have run — it is skipped when a partial review could duplicate comments, or when the failure class needs an operator (auth).

Independent verification against c136b27 found the corollary paragraph
asserting something false: it enumerated the security lane's legitimate
non-runs as four name-stable job-level skips. There is a fifth path, and
it is not a skip. A run whose head was superseded while it queued gates
every step off the freshness guard (claude-security-review.yml:569, 576,
651, 800, 830), which leaves review-failed unset so the fail-closed step
skips too (:860) — the job reports GREEN having reviewed nothing. It is
safe in practice because the newer run for the current head reports the
same context, but a paragraph whose entire job is telling a consumer what
a green required check does not prove must not omit it.

Also corrected, all from the same verification pass:

- Two bullets still asserted of three lanes what holds for one.
  display_report is hardcoded false on the security lane, unset on e2e,
  and a caller-flippable input on claude-review; show_full_output is set
  by no lane. And "no untrusted checkout before the action" was never the
  real invariant — all three lanes check out before the action step and
  the default pull_request checkout is the merge ref, so PR-authored
  content is always in the workspace. Presence is not the hazard;
  execution is. Restated as the invariant that actually holds.
- The heading claimed each workflow header cites this section; all three
  cite the file. Corrected rather than left to imply a heading-level cite
  that reference-integrity does not protect.
- Narrowed "only the security lane reports a check a ruleset can require"
  to what is true: only its check is designed to be.
- Fork PRs skip the job rather than taking the pass-through.
- Recorded that claude-e2e-verify gates on nothing but its kill-switches
  — no draft skip and no skip-actors input — so the most expensive lane
  has the loosest trigger.

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

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 1s —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

The `paths` inputs are matched as root-anchored gitignore patterns via
`git check-ignore`, not as Actions `paths:` patterns. Identical for ordinary
globs; `!` negation cannot be honored as Actions defines it and is now
rejected by the workflow (ci-workflows#289).
@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude encountered an error after 1s —— View job


I'll analyze this and get back to you.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

No description provided.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 14s —— View job


Security review — PR #285

  • Gather context (PR diff, changed files)
  • Review CLAUDE.md changes for security implications
  • Review README.md changes for security implications
  • Confirm no workflow/action/script files are touched (docs-only diff)
  • Post findings

Scope: This PR touches only CLAUDE.md (+41/-16) and README.md (+126/-34) — verified via git diff c136b27...HEAD --stat. No workflow, action, or script file is added, removed, or modified, and no permissions:, secrets:, triggers, or pins change anywhere in the repo as a result of this PR. It is a pure documentation/prose update describing behavior that already shipped in c136b27.

No security issues found.

Given the docs-only nature of the change, the relevant risk isn't "does this diff introduce a vulnerability" (there's no executable surface to attack) but "does it misdescribe the security model in a way that could mislead a future change." I spot-checked the load-bearing claims against the actual shipped workflow source rather than taking the PR body's own verification at face value:

  • paths-file default/precedence/base-ref semantics and paths winning when non-empty — confirmed in claude-security-review.yml:162-171, 331-361.
  • display_report: false hardcoded on the security lane (claude-security-review.yml:600,772), matching the new CLAUDE.md claim that enforcement is uneven by design (hardcoded vs. caller-input vs. unset).
  • Fork-PR job-level skip specific to claude-security-review (claude-security-review.yml:493-515), consistent with the corrected claim that claude-review/claude-e2e-verify degrade to a warning while the security lane skips at job level.
  • "Never execute PR-authored code in a review job" restatement, with claude-e2e-verify called out as the deliberate exception — matches the checkout-then-build/serve pattern that lane requires.

No inline HTML/script, javascript: URIs, or unsafe link patterns were introduced in the prose (checked via diff grep). Nothing in the change touches secrets, credentials, permissions:, triggers, or action pins.

Per the review lane's scope: I did not re-check supply-chain/pin or trigger-pattern concerns, since none apply here (no workflow file changed) and those are zizmor's lane in any case.

@kyle-sexton
kyle-sexton merged commit 8b30988 into main Jul 29, 2026
37 of 38 checks passed
@kyle-sexton
kyle-sexton deleted the docs/claude-lane-contract-close-out branch July 29, 2026 13:20
kyle-sexton added a commit that referenced this pull request Jul 29, 2026
## Summary

Records Phase 5 close-out **evidence** in
`docs/topics/claude-review-lanes/PLAN.md`. One file, additive only — no
plan requirement is restated, reinterpreted, or amended, and the phase
tag stays `[TODO]` because three of the five bullets are not fully
closed.

Of Phase 5's five bullets: two are fully closed with evidence (trailer
note, do-not-break sweep), one is half-closed (equivalence — the medley
half done, the blob-hash half blocked), and two remain outstanding
(README/CLAUDE.md, and "tags advanced", which cannot close while the
others are open).

**The medley REVIEW.md equivalence result inverts the bullet's own
framing**, which is the substantive finding here. medley's `REVIEW.md`
is not a drifted copy of the managed source — it is an independently
authored 188-line document with its own slice taxonomy, first added in
`4a1d61a6`, never synced from standards. So the absent lane-scope
headings are not the finding. The restructure's mutual-exclusion rule is
*conditional* on a named file (standards `REVIEW.md:66-73`): it
suppresses security findings only on a repository that runs the security
lane, and otherwise directs "report security findings under this lane
too". medley has no `claude-security-review.yml` and no caller routing
to that reusable, so it sits in the else branch — folding security in is
correct, and adopting the mutual-exclusion text would have produced the
2f-addendum failure mode of **no lane reporting security findings**.

The real drift is the one always-check that is *not* security-gated:
multi-location atomicity (standards `REVIEW.md:82-86`) has no
universal-checklist line in medley, and its nearest coverage self-scopes
to "EF Core persistence in the modular monolith" while medley is
polyglot. Filed as melodic-software/medley#1671. medley's file was
**not** edited — `review-instructions` is `locally-owned` for medley,
which is the customization seam working as intended.

The standards repo-local caller blob-hash equivalence is recorded as
**NOT DONE and blocked** on standards#286 rather than quietly omitted —
a sanity block that hides an unrun check is worse than one that names
it.

## Test plan

No executable surface changes; this is a documentation-only diff to a
planning artifact. Verification was of the *claims recorded*, not of
code:

- Every citation in the evidence block was read from the live tree:
standards `REVIEW.md:66-73` and `:82-86`; medley `REVIEW.md:65`, `:66`,
`:67`, `:96`; `review/security.md:7`, `:11`, `:18`;
`review/logging.md:12`; `review/transactions-and-consistency.md` line 3
self-scope; `claude-review.yml:751`. Reading is not the same as
verifying, and the third verifier round proved it: an earlier revision
of this block mislabelled the security-lane always-checks as five when
there are four (`abeccc6`'s own commit message says four), and inverted
the `:65`/`:66` → `security.md` mapping. Both are corrected here.
- medley's lane inventory confirmed empirically: no
`claude-security-review.yml`, and `grep -rln "security-review"
.github/workflows/` returns nothing.
- Phase 1 sanity greps re-run against medley's copy: `grep -c "Do not
report"` == 0, `grep -c "Cap the nits"` == 0.
- Issue states re-read via `gh` at time of writing: #255/#258/#259 OPEN
with `updatedAt` unchanged at 2026-07-27T02:11Z; standards#286 OPEN,
`mergedAt: null`.
- All version claims verified against `git rev-list`: v0.6.1=`90f1c549`,
v0.8.0=`e77f0126`, v0.9.0=`cf666f67`, v0.9.1=`c136b27f`.
- lefthook pre-commit gates passed on commit.

**Verification honesty note.** Three fresh-context verifier rounds ran;
all three returned REJECT, and each caught a defect introduced while
fixing the previous one:

- Round 1 — deployed-state overreach on the #151 comment; false "uniform
across all three lane workflows" claim on #257.
- Round 2 — `create-github-app-token` call-site undercount (one
asserted, six actual), introduced while fixing round 1.
- Round 3 — on #257, an absolute "no other pin was assessed" disclaimer
contradicted by the comment's own coverage claims, and a substantively
false one: medley's `claude-code-action` pin was described as having no
Dependabot path when **medley#1668 was already open bumping it 1.0.174 →
1.0.180**, created ~15 hours before the comment. On this PR, the
four-vs-five security-check mislabel and the inverted citation mapping
above.

All are corrected in place with the error named rather than edited away.
This is recorded because the pattern — each fix round introducing a new
error — is the substantive risk in this work, not an incidental detail.

## Related

No linked issue — Phase 5 is tracked as a phase of the
claude-review-lanes Brief in `docs/topics/claude-review-lanes/PLAN.md`,
not as a ci-workflows issue.

Referenced, not closed by this PR:

- melodic-software/medley#1671 — the atomicity drift this evidence
records
- #256, #151, #257 — carry the record-only comments this block cites
- #285 — the remaining Phase 5 README/CLAUDE.md bullet
- melodic-software/standards#286 — blocks the outstanding blob-hash
equivalence

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 29, 2026
…l path matcher (#290)

## Summary

`claude-security-review.yml` advertised its `paths` / `paths-file`
inputs as taking "GitHub Actions `paths:` filter syntax", but the
`changes` job matches with `git init` plus a root-anchored `.gitignore`
and `git check-ignore --stdin --no-index`. Two different specifications,
diverging silently.

This PR makes the workflow honest about which one it implements, and
makes the one materially divergent class fail loudly instead of quietly
mismatching.

- **`!`, `?` and `+` are now rejected** with a `::error::` and a
non-zero exit. All three are documented GitHub Actions path-filter
syntax; all three mean something different under gitignore rules, and
all three fail toward matching FEWER files — i.e. toward skipping the
review.
- `!` — gitignore cannot re-include below an excluded directory, so
`.github/**` + `!.github/docs/**` still matches `.github/docs/notes.md`.
- `?` / `+` — Actions defines these as zero-or-one / one-or-more of the
PRECEDING character. The docs' own **path** example is `'*.jsx?'`
matching `page.js` and `page.jsx`; under gitignore it matches
**neither** (it matches `page.jsxx`). `v[0-9]+/**` matches no real path
at all.
- **Character classes stay allowed** — `[0-9]` is documented by Actions
*and* behaves identically here.
- **The input descriptions now state the real matcher**, including the
two remaining differences that are harmless because they over-match:
trailing-slash directory patterns, and `docs/*` matching at any depth
rather than root-only.
- The canonical-caller comment block carried the same claim and is
corrected.

The guards are a **separate pass ahead of the anchoring loop**, and they
read the pattern list through **here-strings**. Both details are
load-bearing — see the Test plan. The loop's `!` arm is dropped as
unreachable.

**This guard fails OPEN, not closed.** `security-review` gates on
`!cancelled()`, which overrides the default needs-failure skip, so a
failed `changes` leaves `relevant` unset and the review still runs
against everything — consistent with every other fault path in this job.

Closes #289.

## Test plan

**The divergence and the fix were both measured, not reasoned about.** I
reproduced the workflow's pattern loop (`:417-428`) verbatim into a
probe script and ran `git check-ignore` against representative paths.

Agreement on ordinary globs — including the obvious candidate defect,
which does **not** exist (`**/*.sh` does match a root-level
`install.sh`, because gitignore treats a leading `**/` as "any depth
including root"):

```text
FIRES     install.sh
FIRES     scripts/deploy.sh
FIRES     plugins/foo/hooks/x.js
FIRES     package.json
FIRES     sub/package.json
FIRES     .mcp.json
FIRES     REVIEW.md
FIRES     .github/workflows/ci.yml
NO-REVIEW docs/readme.md
```

The divergence, measured:

```text
patterns:  .github/**  then  !.github/docs/**
  FIRES  .github/docs/notes.md      <-- negation did NOT exclude

patterns:  src/file[0-9].ts
  FIRES     src/file1.ts            <-- gitignore extra, undocumented for Actions
  NO-REVIEW src/fileA.ts
```

The `?` / `+` divergence, measured (`NO-REVIEW` = the security review
never fires):

```text
patterns: *.jsx?   v[0-9]+/**   docs/*
  NO-REVIEW page.js       <-- Actions matches this; we do not
  NO-REVIEW page.jsx      <-- Actions matches this; we do not
  FIRES     page.jsxx     <-- we match this; Actions does not
  NO-REVIEW v1/x.ts       <-- Actions matches; '+' is literal here
  NO-REVIEW v10/x.ts
  FIRES     docs/sub/deep.md  <-- we over-match (safe direction)
```

**A reviewer found the first guard was defeated by its own input, and it
reproduced.** `grep -q` exits at its first match; on a list larger than
the pipe buffer that kills the still-writing `printf` with SIGPIPE, and
under `pipefail` the pipeline returns 141 — so the `if` read the
negation as ABSENT and the loop wrote the `!` through as an ordinary
pattern:

```text
list size: 170909 bytes (negation on line 1)
  printf | grep -qE  ->  *** MISSED negation, pipeline status 141
  here-string        ->  DETECTED negation (correct)
```

Both guards now use here-strings, which have no producer to kill; the
second is two steps rather than a chained pipeline for the same reason.

I also verified a claim the previous revision asserted and got wrong: an
`exit 1` inside the `printf | while` subshell **does** fail the step
under `set -e` (the pipeline returns the subshell's status). The guard
placement stands on clarity, and the comment no longer teaches a false
shell fact.

The guard suite — 13 cases, run under the step's own `set -euo
pipefail`:

```text
ordinary globs                                  ACCEPT  OK
leading-! negation                              REJECT  OK
indented negation                               REJECT  OK
? question mark                                 REJECT  OK
+ plus                                          REJECT  OK
char class alone                                ACCEPT  OK
trailing-slash directory                        ACCEPT  OK
comment mentioning ! and ?                      ACCEPT  OK
ALL lines are comments                          ACCEPT  OK
empty                                           ACCEPT  OK
live claude-code-plugins 26-entry file          ACCEPT  OK
LARGE list, negation first (SIGPIPE regression) REJECT  OK
LARGE list, ? first (SIGPIPE regression)        REJECT  OK
```

The live-file case is the regression that matters:
`melodic-software/claude-code-plugins` is the one repo in the fleet with
a tuned paths file **and** a required security check. Its file is 26
pattern entries; I checked every entry against all four divergent
classes and found **zero** — no negation, no character class, no
trailing-slash, no `?`. So this was a documentation defect rather than a
live behavior defect, and the new guard does not break the only live
consumer.

Also verified: YAML parses, all eight `workflow_call` inputs intact,
`paths` and `paths-file` defaults still `''`. `actionlint`,
`shellcheck`, and `zizmor` run in CI on this file.

## Related

Closes #289.

Split out of the Phase 5 docs close-out (#285), which carries the
matching README correction. Sibling finding #288 (`claude-e2e-verify`
persisted-credential acceptance) is deliberately **not** in this PR — it
needs a human re-ratification decision, so that file is untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013pLW2dybov9xvTFtx48Ueb
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.

1 participant