Skip to content

feat(ci): gate the inlined rate-limit floor against the contract that owns it - #3462

Merged
kyle-sexton merged 8 commits into
mainfrom
claude/coupling-loop-lane-floor-gate
Aug 28, 2026
Merged

feat(ci): gate the inlined rate-limit floor against the contract that owns it#3462
kyle-sexton merged 8 commits into
mainfrom
claude/coupling-loop-lane-floor-gate

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

No linked issue

Summary

docs/conventions/loop-lane/README.md §6 requires the rate-limit guard's "operable floor" to be inlined byte-identically into every consuming lane body, and claimed "fleet audits check conformance per consumer". No such check existed, and the identity had already broken. This is the top-ranked route-lane finding from the /coupling:reduce pass merged in #3457.

Three independent scan agents reached this finding separately, and implementation found the copy count was six, not the five originally reported.

Fix

The gate. scripts/check-loop-lane-floor-drift.sh extracts the floor block from its owner, plugins/rate-limit-guard/reference/reader-contract.md, and asserts it against an explicit registered consumer list. An explicit list is required: scripts/check-cross-plugin-source-drift.sh is structurally blind here twice over — it skips SKILL.md by basename (skip_basenames) and clusters only on identical path-within-plugin, while these consumers sit at six different paths. Modelled on plugins/plugin-quality/scripts/zones-inline-drift.test.sh, the gate this repo already built for the structurally identical context-guard case. Wired into .github/workflows/ci.yml as loop-lane-floor-drift-gate and into ci-status.needs.

Six consumers registered, not five. Beyond the three lane bodies (work-loop, attend-queue, babysit-loop) and prompts/loops/loop-lane-prompts.md, implementation found prompts/loops/loop-lane-profile-claude-code-plugins.md and plugins/docs-hygiene/skills/extract-ssot/context/orchestrated-mode.md — the latter self-declaring it is "inlined verbatim per the loop-lane convention's inline-floor rule (byte-identical across consumers)".

The drift, reconciled toward the consumers. Two sentence breaks had diverged (is stale — treatis stale. Treat; on every write — the fileon every write, the file), landed by two uncoordinated de-slop shards (dff09429 work-items, 67466227 source-control) that never touched the source. The source was moved to match the consumers rather than the reverse, on this evidence:

No value, path, threshold, or staleness window changed — only two sentence breaks.

§6 now names the real check instead of asserting an audit that did not exist. That false claim was itself part of the finding.

Verification

  • The gate fails on drift and passes clean, demonstrated both ways. Against a git archive of the pre-reconciliation tree: rc=1, three DRIFT (exact) reports naming exactly the two em-dash lines. Against the committed tree with used_percentage >= 90 mutated to >= 95 in babysit-loop/SKILL.md: rc=1 with the diff. Unmutated: rc=0, "6 consumer(s) match".
  • New suite: 16 cases, 16 pass, 0 fail — exact drift, source-side drift (fails all six), values-mode tolerating a re-wrap but not a changed value, a consumer that dropped the floor, a stale registration, a duplicate block, and the two blindness modes (marker stops matching, block missing a bullet) which exit 2 rather than passing forever.
  • scripts/affected-tests.sh --run: 121 shell suites passed, 0 failed (rc=3 is the documented "other ecosystems deferred" exit).
  • The 5 Python suites it names as NOT RUN, run directly: 41 passed; 434 passed + 116 subtests.
  • check-lane-coverage.sh (46 lanes reachable), check-changelog-parity.sh --check / --check-bump / --check-preserved, validate-plugins.sh, check-changed-skills.sh origin/main (9 skills, 0 failed), check-purged-em-dashes.sh, check-cross-plugin-source-drift.sh --check, check-silent-skips.sh — all pass.
  • markdownlint-cli2 (8 changed md), typos, editorconfig-checker, shellcheck -x, check-shell-portability.sh, shfmt -d, actionlint — all pass.

Stated limits, not glossed: plugins/knowledge/.../export-sheet-frame-index.test.js could not run (vitest suite importing @melodic/video-digestion; node_modules/@melodic is absent in this container — a pre-existing environment gap, and nothing here touches JS). And affected-tests.sh does not select the new suite from a lane-body edit, because SKILL.md is a structural basename R3/R4 skip; the CI job carries no path filter and runs on every push, so CI covers it.

Related

Follow-up to #3457, which applied that pass's apply lane and routed this finding. Refs docs/conventions/loop-lane, plugins/rate-limit-guard/reference/reader-contract.md.


Generated by Claude Code

… owns it

The loop-lane convention orders every consuming lane to inline the
rate-limit guard's operable floor byte-identically, because an installed
plugin cannot read a sibling plugin's files at runtime. Nothing enforced
that. Section 6 claimed "fleet audits check conformance per consumer" and
no audit existed, and the general copy-drift gate cannot cover this set:
it skips SKILL.md by basename and clusters copies by identical
path-within-plugin, while the six copies sit at six unrelated paths.

The gap had already been paid for. Two uncoordinated de-slop shards
rewrote two em dashes inside the staleness bullet of all three lane
bodies and touched neither the reader contract nor the other three
copies, so the lanes stayed identical to each other, which is the half a
reviewer notices, while all three drifted from their source.

Reconciles toward the consumers: the de-slop campaign is monotone, three
of the consumers are SKILL.md files the house-style rule names, and the
consumers' wording is already reviewed. The source and the two unpurged
copies take the de-slopped sentence breaks. No floor value, path or
threshold changed.

Adds scripts/check-loop-lane-floor-drift.sh, which extracts the floor
block from the reader contract and compares it against an explicit
registry of the six inlining surfaces. Prose copies match byte for byte;
the blockquoted launch-prompt templates are re-wrapped to a narrower
column, so they are registered to match after normalization, which still
asserts every value. Deduplication is deliberately not the fix: the
copies exist to survive plugin isolation. Runs as the new
loop-lane-floor-drift-gate CI lane, self-test first, with its own suite
covering every way the gate must go red including the two ways the
extractor could go blind and pass forever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 546a9c061d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/check-loop-lane-floor-drift.sh
claude added 2 commits August 28, 2026 07:37
…lane-floor-gate

# Conflicts:
#	plugins/docs-hygiene/CHANGELOG.md
#	plugins/rate-limit-guard/CHANGELOG.md
…registered copies

The registry alone only ever looks where it is told. A new lane could inline
the operable floor, its author forget the entry, the six registered paths still
agree, CI go green, and the next contract change strand that seventh copy. That
is the same blindness this gate exists for, one level up, and it is not
hypothetical: the first report of this coupling named five copies and building
the registry found six.

The check now scans every tracked file for the floor's opening bullet before
comparing anything and fails on a carrier its registry does not name. Discovery
reuses the anchored marker test the comparison uses, so the blockquoted
launch-prompt shape is not a hole. Tracked rather than on-disk is the corpus
every sibling gate reads: a scratch file ships to nobody, and staging it is what
makes it a copy this repository carries. The registry stays, because it carries
each consumer's comparison mode, which no scan can infer.

A file may carry the marker as data rather than as a consumer copy, and declares
that inline with a reason (loop-lane-floor-carrier-ok:) on the shape the CI
workflow's lane-coverage-ok and the hooks' silent-skip-ok already use. The
reason is mandatory, an annotation on a registered path fails as stale, and
--list enumerates every exemption. This gate's own suite is the first such file.

Also in this change:

- Merges current origin/main. #3461 claimed rate-limit-guard 0.7.17 and
  docs-hygiene 0.21.21 after this branch was cut, so both renumber to 0.7.18 and
  0.21.22 and the two changelog conflicts are resolved keeping main's entries.
- Corrects this branch's own miscount. The rate-limit-guard entry said all six
  copies had drifted; three had. The other three matched the contract byte for
  byte and moved with it here. Same correction where the loop-lane changelog
  said two unpurged copies rather than three.
- Corrects what values mode tolerates. Normalization strips backticks and
  emphasis as well as blockquote markers and wrapping, so markup is free in that
  mode too, not only line breaks. Every word is still asserted. Section 6 and
  both changelogs now say what the script header already said.
- Adopts the liveness-assertion convention properly: the script header names its
  taxonomy row (Gate / classifier) and how it satisfies fail-loud, and the
  convention gains its Adopters row and a 1.1.0 entry.
- Removes two restatements of floor values sitting outside the compared block:
  section 6 no longer names the 90% threshold, and rate-limit-guard's setup
  probe no longer names the 10-minute staleness window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
@github-actions github-actions Bot deleted a comment from claude Bot Aug 28, 2026
@github-actions github-actions Bot deleted a comment from claude Bot Aug 28, 2026
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Aug 28, 2026

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, or workflow_dispatch this workflow with the PR number, to retry the review. A new push re-triggers this lane only if the caller's pull_request triggers include synchronize (the canonical caller omits it).
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).

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Warning

Automated security 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."

The check is green on purpose, and it is not evidence. It certifies that a security pass ran, and this one did not complete — but the cause is outside this PR's control, so merging is deliberately left unblocked rather than locking every merge for the length of the outage. Nothing was reviewed at this head. Where this check is required, it is satisfied without that evidence; a human should review security-sensitive changes here before merging.

Re-run the job, or workflow_dispatch this workflow with the PR number, to retry the review (ci-workflows#227). A new push also retries it only if the caller's pull_request triggers include synchronize (the canonical security caller keeps it). 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.

Re-running does NOT help for every class:

  • rate-limit that persists across re-runs, or auth — the credential or usage budget needs an operator; retrying will not clear it.
  • a run that exhausted its turn budget ("subtype":"error_max_turns" above) will exhaust it again. As the PR author, split the change into smaller PRs; raising --max-turns is a change to the caller workflow, not something you can set on this PR.

claude added 4 commits August 28, 2026 08:15
The CI `exec-bit` lane flags every tracked file recorded 100644 whose content
starts with a shebang, and it is one of the ungated whole-repo linters, so this
turned `hygiene` and with it `ci-status` red regardless of the rest of the
branch. `scripts/check-loop-lane-floor-drift.sh` landed 100755;
`scripts/check-loop-lane-floor-drift.test.sh` landed 100644. Every sibling
suite under `scripts/` is 100755.

Mode change only, no content change. Checked repo-wide rather than only the
named file: a local reproduction of the action's rule (tracked, 100644,
shebang-prefixed) reports zero findings after this commit, and these two files
are the only ones this branch adds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
…idiom

Two things land here: the merge resolution, and the exemption rework that had to
happen on the merged tree.

## Merge resolution, per conflict

Main reconciled the floor block independently while this branch was in review,
so seven files conflicted.

- reader-contract.md, orchestrated-mode.md, loop-lane-prompts.md and
  loop-lane-profile-claude-code-plugins.md: all four conflicts are the same one
  line, this branch's comma against main's colon in the staleness bullet. Main's
  side taken in all four. Main's #3460 hashed the carriers, found the same
  divergence this branch found, and repaired it first; its form is grammatical
  where the comma left a splice. Running this branch's gate against origin/main
  unmodified reports 6 consumers matching and no unregistered copy, so main is
  already consistent across the owner and all six registered consumers. There is
  no live drift event to reconcile here, and re-imposing a second correct answer
  would be a third uncoordinated edit to the block.

- rate-limit-guard/CHANGELOG.md: main claimed 0.7.18. This branch's entry moves
  to 0.7.19 and drops its reconciliation bullet, which main's 0.7.18 now owns;
  what remains is the gate, the discovery scan, and the setup-probe change. That
  also removes the "other three copies matched byte for byte" line, which was
  wrong: two of those three are blockquoted templates matching under values-mode
  normalization, not byte for byte.

- docs-hygiene/CHANGELOG.md: same, 0.21.22 taken, this branch's entry moves to
  0.21.23 and now records only that the copy is gated rather than that it was
  reconciled here.

- loop-lane/CHANGELOG.md: main added 9.0.1, this branch 9.1.0. Both kept, in
  order; no content overlap.

Manifests follow: rate-limit-guard 0.7.19, docs-hygiene 0.21.23.

## The exemption idiom could silence a genuine consumer

The in-file `loop-lane-floor-carrier-ok:` annotation matched anywhere on any
line, in any context, so a real lane body could inline the floor, apply it,
mention the token in a sentence, and never be compared again. Demonstrated end
to end by review: an annotated lane passed at rc=0, then had its 90 changed to
80 and still passed. That is materially weaker than the two idioms it was
modelled on, for a structural reason rather than an implementation one:
`# lane-coverage-ok:` and `# silent-skip-ok:` anchor to a syntactic site, a job
key or a guard line, which bounds what they excuse, and a file has no such site.

The exception list moves into the gate as DATA_CARRIERS, path plus reason. No
file can now exempt itself by writing anything into itself; a new data carrier
costs an edit to the gate, which is the review the exception needs. Entries are
stale-guarded the way the consumer registry is, failing when the path is gone or
no longer carries the floor, and they are printed on the PASSING path of
--check, not only under --list: CI runs --check and nothing else, so an
exemption visible only under a listing flag would be invisible in the log of the
run that went green over it.

Corrects the claim this defect falsified. The gate header and section 6 said the
checked set was "provably equal" to the set of files carrying the floor, untrue
while an unbounded self-declared exemption existed two sentences away. Both now
state the guarantee exactly: the carried set minus a finite, enumerated,
reviewed exception list. In a change whose premise is that an unbacked doc claim
is a defect, that had to be fixed rather than narrowed.

Tests, now 28: a file naming the old token cannot excuse its own copy; the same
file still fails once drifted; --check names every excused carrier on the
passing path; every listed data carrier exists in the checkout. The fixture tree
seeds the data-carrier paths it reads back out of the gate, so both registries
are stale-guarded against the same tree.

Adds the missing test for the discovery self-proof. Gutting that guard
previously left the suite green while the gate reported a clean corpus over a
scan that never ran, the exact property the liveness-assertion Adopters row
advertises by name. Staging every fixture file EXCEPT the source reproduces a
search that returns nothing, and the gate must exit 2. Verified as a mutation:
with the self-proof condition forced false the suite reports PASS=27 FAIL=1, and
the one failure is that test.

Documents the shared anchored-marker limit: a copy indented under an outer list
or nested two blockquote levels deep is invisible to discovery AND to
comparison. The second half bounds the first, since such a path registered as a
consumer fails as MISSING FLOOR rather than passing silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
…nch to 0.21.24

The merge resolution was written against a read of main taken before its last
push. Main's newest docs-hygiene entry was 0.21.23, not 0.21.22, so resolving
the conflict "keep main's newest, renumber mine above it" put this branch's
entry under a heading main had already published and dropped main's body for it.
--check-preserved did not catch it, because it compares headings and the heading
still existed; --check-bump did, as a VERSION COLLISION.

Main's 0.21.23 body is restored verbatim, this branch's entry moves to 0.21.24,
and the manifest follows.

Audited the other three changelogs this branch resolved for the same class of
error rather than fixing only the one that failed: rate-limit-guard from 0.7.18
down, loop-lane from 9.0.1 down, and liveness-assertion from 1.0.0 down are each
byte-identical to origin/main. Only docs-hygiene was affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
Main moved twice more while the previous merge was being verified. Two
conflicts, both changelog ordering; no floor-carrying file was touched by
either commit, and the gate passes on the merged tree unchanged.

- loop-lane/CHANGELOG.md: main added 9.0.2, a second citation-shape correction
  in section 5. Both entries kept, in order; this branch's 9.1.0 stays on top
  and no content overlaps.

- docs-hygiene/CHANGELOG.md: main claimed 0.21.24 for the pipefail fix to the
  filtered-probe guard. This branch's entry moves to 0.21.25; main's 0.21.24 is
  kept verbatim. Manifest follows.

The previous merge made this exact mistake in the other direction, writing this
branch's entry under a number main had already published and dropping main's
body with it. --check-preserved compares headings, so it stayed green; only
--check-bump caught it as a VERSION COLLISION. Both resolutions are now checked
by a derived audit rather than by eye: for each changelog this branch has
touched, everything from main's newest heading down must be byte-identical to
origin/main. Four files, zero differences.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
@github-actions github-actions Bot deleted a comment from claude Bot Aug 28, 2026
@github-actions github-actions Bot deleted a comment from claude Bot Aug 28, 2026
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

The second merge resolution removed a conflict marker that had been carrying
the blank line between this branch's 9.1.0 entry and main's 9.0.2 heading,
which markdownlint reports as MD032 and MD022. Whitespace only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
@github-actions github-actions Bot deleted a comment from claude Bot Aug 28, 2026
@github-actions github-actions Bot deleted a comment from claude Bot Aug 28, 2026
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@kyle-sexton
kyle-sexton merged commit fa817ae into main Aug 28, 2026
60 checks passed
@kyle-sexton
kyle-sexton deleted the claude/coupling-loop-lane-floor-gate branch August 28, 2026 14:07
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.

2 participants