Repository navigation
feat(rate-limit-guard): stamp the tee snapshot with the account it describes - #3778
Merged
Merged
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aba2825f6a
ℹ️ 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".
kyle-sexton
force-pushed
the
feat/rate-limit-guard-tee-account-field
branch
3 times, most recently
from
September 5, 2026 23:51
94dfb1a to
e3b200d
Compare
…scribes
The tee file is machine-scope and last-writer-wins, so a snapshot could not be
attributed to the account that produced it. The drain now stamps
`account: {"email": "<address>"}` at the top level, read from Claude Code's own
`.oauthAccount.emailAddress` in `${CLAUDE_CONFIG_DIR:-$HOME}/.claude.json`.
Mislabeling is worse than absence, so the field is omitted in four cases: no
state file or no email-shaped value; a stdin `account*` key that wins instead;
a state file modified after the chosen record's spool file, which means a
switch may have happened between the observation and the flush; and a writer
path with no spool to date that comparison against (RLG_TEE_ASYNC=1, or bash
below 4.2). Statusline stdout and exit codes are unchanged in every case.
Cost is one process per 30-second drain and the render path stays fork-free.
Measured on the target desktop 2026-09-04 against an 88 KB state file: bash
`$(<file)` plus parameter-expansion extraction 3.6-4.0 s, unusable; `jq -r`
over stdin 35 ms; `claude auth status --json` 175 ms. Bash opens the file and
jq reads stdin, so the MSYS-path limitation does not apply.
The batch jq pass gained two lines: the chosen record's shard name, which is
what the staleness comparison dates against, and a structural `keys_unsorted`
test for an existing account key, asked the same way the window-bearing verdict
is asked with `has()` rather than as a substring scan.
Fixed alongside, because the two extra lines exposed it: every line after the
payload is now CR-stripped in `_rlg_absorb_jq_lines`. A native jq on Windows
ends its lines with CRLF and `read -r` splits on LF only, so a token arrived as
`true\r` and equalled nothing; only the last line was reliably clean, since
MSYS command substitution drops the trailing CRLF. Which verdict was correct
therefore depended on how many lines the pass emitted. The payload keeps its CR
deliberately.
Refs #1218
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF
… copies The operable floor's staleness bullet claimed the tee file "carries no account-identifier field", which the writer-side change makes false. Amended in the reader contract that owns the block and in all six inlined copies in the same change, so scripts/check-loop-lane-floor-drift.sh proves they moved together rather than catching it a release later. "Tee file shape" documents the key, its source, the four cases in which the writer omits it rather than risk mislabeling, and the recheck trigger: .oauthAccount.emailAddress is internal CLI state, not a documented surface. The untrusted-value rule is restated for the field. The invariants section and the loop-lane convention's section 6 both said the resolution was designed elsewhere; both now say the writer-side half has landed and name what remains. The plugin's version bumps to 0.8.0. docs/MIGRATION-PLAYBOOK.md, "Version pinning and update delivery", requires the bump as the only delivery vehicle but does not decide minor versus patch for a new additive field, so minor is the deliberate choice for a new consumer-facing contract field. Noted for the reviewer: 0.7.32 shipped an "Added" entry under a patch bump, but that was a manifest description rather than a field consumers read. Refs #1218 Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF
The CR strip added with the account field is invisible on a Linux runner, where jq emits LF, so CI would not have caught its removal. Two cases drive the suite's existing CRLF `jq` shim and assert the two tokens that silently break different halves of the contract: the window-bearing verdict, which decides whether a writer may proceed through a held lock, and the enablement verdict, which decides whether the tee runs at all. Both were checked non-vacuous by removing only the four strip expressions from a scratch copy of the writer: without them the window-bearing write is lost and a configured false no longer gates. Refs #1218 Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF
…field Both findings are the same failure the field exists to prevent: a snapshot carrying an address that does not describe the windows in it. The staleness guard demanded only that the state file not be NEWER than the chosen record's spool file, so an exactly equal timestamp admitted the value. Mtime resolution is coarse on several filesystems this writer runs on, and there a login that rewrites .claude.json inside the same tick as the observation is indistinguishable from one that happened later. The guard now demands the spool record be STRICTLY newer and treats equality as a possible switch. A missing spool file still omits, so the degenerate inputs stay on the same side. Validation ran after bash command substitution, which strips embedded null bytes and trailing newlines: an address spelled with a JSON-escaped control character arrived as a clean string and was injected under a name the state file never held. The value is now judged on its codepoints inside jq, before it leaves the parser: 3 to 254 of them, none below 32 and none equal to 34, 92 or 127, and at least one `@`. A non-object `.oauthAccount` makes jq exit non-zero and lands on the same omit path. The equivalent bash tests are kept as a second line of defense. Tests: an equal-mtime case built with `touch -r`, asserting the fixture really is bit-identical before asserting the key is absent, plus a control that one second of separation alone restores it; two fixtures added to the malformed-value loop for the control-character and trailing-newline shapes. The equal-mtime case was checked non-vacuous by flipping only the comparison back on a scratch copy of the writer, which then admits the address. The 0.8.0 entry is edited in place rather than superseded: it has not merged, so the four-case absence list and the validation description were simply wrong rather than out of date. No second version bump. Refs #1218 Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF
…arriers The account.email change edits the inlined rate-limit-guard "Operable floor" block in each of these plugins, and the repo rule is that any change under plugins/<name>/ needs that plugin's manifest bumped and a matching CHANGELOG entry. Without the bump, check-changelog-parity.sh reports PUBLISHED VERSION REUSE for all three. docs-hygiene 0.21.37 -> 0.21.38, source-control 0.55.54 -> 0.55.55, work-items 0.39.63 -> 0.39.64. Patch bumps: the floor's values are unchanged and only its staleness sentence moved, so no lane's obligations shift. source-control lands one higher than first drafted because main published its own 0.55.54 while this branch was in flight; that entry is preserved above ours in the changelog. Refs #1218 Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF
…1218 pointers Two lint-lane failures, both in prose this branch wrote. check-purged-em-dashes.sh declares plugins/rate-limit-guard/README.md a purged surface, and the rewritten multi-account paragraph reintroduced two em dashes. Restructured into sentences: the four absence cases are now an explicit list introduced by a colon, and the "no consuming lane acts on it yet" clause is its own sentence. No parentheses, en dashes, or spaced hyphens substituted, as the gate's own text forbids. Three TODO(#1218) pointers are replaced with present-tense statements of what is not built, in docs/conventions/loop-lane/README.md, the reader contract's invariants bullet, and the unreleased 0.8.0 changelog entry. Issue #1218 is closed as not planned, so a TODO aimed at it is a stale pointer rather than lineage, and #3770's audit had already replaced the reader contract's original pointer for that reason. The single (Refs #1218) lineage mention in the 0.8.0 entry stays; older entries and main's own text are untouched. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF
kyle-sexton
force-pushed
the
feat/rate-limit-guard-tee-account-field
branch
from
September 6, 2026 00:16
e3b200d to
69fbb19
Compare
kyle-sexton
added a commit
to melodic-software/claude-code-account-rotation
that referenced
this pull request
Sep 7, 2026
…#3) No related issue: Phase 1 of the plan this repository carries (`docs/topics/claude-subscription-rotation/PLAN.md`); no tracker issue exists for it yet. **Merge gate (met 2026-09-06):** the Phase 1.5a probe and the live acceptance in `tests/acceptance/README.md` ran on the operator's desktop with three sessions open, the security and concurrency reviews ran and their Critical and Important findings are fixed here, and the fresh-context verification confirmed the result. The deferred findings are #7 to #12. ## Summary The tracer bullet: one machine-wide switch of the live Claude Code account, driven from a loopback page, that parks the live credential pair, unparks the chosen one, patches only the `oauthAccount` block of the state file, and verifies the result through `claude auth status`, all under Claude Code's own `.oauth_refresh.lock`. Credential pairs are moved, never copied; nothing here calls the model API; every HTTP request the tool ever makes carries its own User-Agent. This is the plan's Phase 1 (items 1.1 to 1.9, the 1.5a probe, and the live acceptance), plus the review fixes. ## Fix - **Core** (`src/AccountRotation.Core`): `Result<TValue, TError>`; identity types (`AccountEmail`, `OAuthAccountBlock`, `CredentialPair`, `RefreshTokenFingerprint`, `ParkedProfile`, `LiveAccountState`); `SwitchPlanner.Plan` over one `SwitchPlanningInput` with eleven `SwitchRefusal` cases; `ManagedLoginPolicy` (a device-managed `forceLoginOrgUUID` refuses every switch, and a policy source that exists but cannot be read fails closed); the `ICredentialPairStore` and `IClaudeCliAuthStatus` ports; `AccountRotationConfiguration`. - **App adapters** (`src/AccountRotation.App/Adapters`): atomic writers, the state-file splice that touches only `oauthAccount` (staged first, byte-compared against the original, then renamed), `OAuthRefreshLock` (joins the CLI's `proper-lockfile` directory; bounded stale steal), the file-system credential store with the single-holder invariant and one containment rule, one shared reader that never blocks the CLI's own writes, profile folders with `profile.json` identity, the executable locator (npm shim through the system interpreter with a quoted path), the CLI status runner. - **Switching** (`src/AccountRotation.App/Switching`): `LiveDirectorySwitch` under the `CredentialMutationGate` and the instance lock; from the first journal write the moves run to completion regardless of the request's abort; the live pair is re-read under the lock and that fingerprint journaled; unpark follows park immediately; the owner record follows the CLI's token rotation (re-bound, released, or kept so the planner refuses); startup reconciliation quarantines duplicate lineages through the guarded rename and completes or unwinds a journaled switch from the folder layout when fingerprints have rotated; a state-file watcher (edge-triggered, one pass at startup) and every dashboard read repair a block a running session wrote back over the patch, judged by the owner record's timestamp (observed live eight minutes after a switch). - **Hosting, configuration, page**: composition root with explicit service types; `ConfigurationValidator` refuses a profiles root or app data directory on another volume, under a sync folder (matched by prefix), inside the live directory, or at the home root; same-origin plus custom-header mutation filter and loopback `Host` filtering; `GET /api/dashboard`, the switch route, and the embedded page (switch buttons disabled at click). - **Tests**: 40 Core and 101 App tests, every hosted one over temp directories through `AppFactory`; no real Claude Code file is touched. `tests/acceptance/`: `check-single-holder.sh`, `check-live-identity.sh` (token on curl's stdin), and the runbook with the log of the 2026-09-06 pass. - **Plan**: Phase 0 tagged DONE; 1.5a ticked with the probe's outcome (keep the patch; the `patchStateFile` key is gone) and the two consequences it found; the risk table aligned with what is built. ## Verification - CI (`ubuntu-24.04` and `windows-2025`): build, test, format, repository gates green on the merge head. - `dotnet test AccountRotation.slnx -c Debug` locally: 141 passed, 0 failed; `dotnet format whitespace --verify-no-changes` exit 0; `bash eng/check-no-machine-paths.sh` clean; `typos`, `markdownlint-cli2`, `shellcheck` clean. - Live acceptance on the operator's desktop (2026-09-06, CLI 2.1.263), logged in `tests/acceptance/README.md`: 1.5a probe (keep the patch), AC 9 (lock respected, 409), AC 2 (three-subagent fan-out survived the switch, parent billed the incoming account), AC 1 (three sessions followed the switch with no browser and no restart; `claude auth status` and the state file agreed), live identity (`billed_email` = incoming account), `check-single-holder.sh` `duplicates=0` before and after every switch. The state-file drift probe passed over eight hours, then the post-fix live check caught a session writing its block back eight minutes after a switch; the repair guard was built and checked live the same night (startup pass repaired the stale file, a further switch back verified clean). - Reviews: `review:security-reviewer` and a concurrency slice against `conventions/review/concurrency.md`; findings in `.work/reviews/` (memory tier); Critical and Important fixed in the `fix(...)` commits, Suggestions filed as #7 to #12. - Fresh-context verification (`implementation:phase-verifier`, nineteen binary criteria plus three defects it found itself, all fixed): CONFIRMED at the merge head. ## Related - Plan: `docs/topics/claude-subscription-rotation/PLAN.md` Phase 1; design notes under `docs/topics/claude-subscription-rotation/design/`. - #2 (skeleton, merged), #1 (first standards sync, merged), #5 (rename, after this merge), #7 to #12 (deferred review findings). - Phase 6 (writer-side `account.email` in the rate-limit-guard tee) is melodic-software/claude-code-plugins#3778; Phase 2's tee reader parses the key when present. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
kyle-sexton
added a commit
that referenced
this pull request
Sep 30, 2026
…hanges (#5448) Closes #5177 ## Summary A lane that latched a rate-limit pause never noticed an account switch, so it kept sleeping on the old account's `resets_at` after the operator changed to an account with headroom. The owner decision on #5177 chose Option 3: lanes read `oauthAccount.emailAddress` from `~/.claude.json` directly. ## Fix - `reader-contract.md`: new Account switch bullet in the operable floor. A paused consumer MUST read the account at pause entry and on every re-evaluation. - At pause entry the latched account is the `account.email` of the snapshot that tripped, not the account `.claude.json` names then. `paused_until` comes from that snapshot, which can be up to 10 minutes old, so latching the `.claude.json` account would pair the new account with the old account's reset. A snapshot with no `account.email` leaves the pause unattributed: with no latched account there is no switch to detect, so the lane stays paused and adopts the first account it reads on a later wake. When a latched account exists and `.claude.json` names a different account at entry, the switch evaluation runs at once. - When the account changes, the consumer takes the new account's windows from a fresh tee snapshot whose `account.email` equals the new account, and: - below 90: drops the latch and resumes (the owner's stated rule); - at or above 90: keeps pausing and re-latches against the new account's `resets_at`; - no fresh, attributable snapshot: treats the windows as unknown, drops the latch, and runs reactive-only. This branch goes past the owner's literal wording ("drop ... when the new account is under 90%"). It follows the existing staleness rule (unknown windows mean reactive-only), and it is needed on a headless-only machine, where the tee is never refreshed. It is recorded in the loop-lane README, its changelog, and the rate-limit-guard changelog, and flagged here so the owner sees it. - An unreadable or unattributable `.claude.json` (cannot attribute) is not a switch: the lane keeps its latch and continues the ordinary re-evaluation, so the latched pause still ends at `paused_until`. The email is never printed or logged. - The floor block is carried to all consumers (work-loop, attend-queue, babysit-loop, extract-ssot, and the two loop-lane prompts); the drift gate and its fixtures cover the new bullet. - work-loop and babysit-loop: `latched_account` (SHA-256 fingerprint, since the state block is a public comment) beside the latch, and the pause logic in `reference/paused-wait.md`, now with an "At pause entry" section. The `.claude.json` and tee reads feed `jq` on stdin, because a native Windows `jq` cannot open an MSYS-style path argument. The doc names the empty-input hash `e3b0c44298fc1c14` as never a fingerprint, and points to the reader contract's recheck trigger for the internal key instead of restating it. - Account read (`reference/paused-wait.md`): falls back to `shasum -a 256` where `sha256sum` is absent, rejects a value that is not email-shaped (the tee writer's shape whitelist), and reads the tee file once so the fingerprint and the windows come from the same snapshot. Step 4 applies the per-window rule: resume when every plausible window is below 90, re-latch when any plausible window trips, reactive-only when none is plausible. - Evals: work-loop cases 11 (resume on a wake, keep-latch on an unreadable file), 12 (switch found at pause entry), 13 (unattributed trip stays paused) and 14 (switch with one plausible window); babysit-loop cases 12 (unknown-windows drop on a wake), 13, 14 (the two entry cases) and 15 (one plausible window). babysit-loop case 11 is the promotion-evidence preflight case from main. - work-loop: the instance-collision block and the `instance:` line rule moved, unchanged, from `SKILL.md` to `reference/telemetry-upsert.md` to keep `SKILL.md` under the 500-line cap after the floor sync. `SKILL.md` keeps a pointer, and the work-items changelog records the move. - Loop-lane README section 6 and its changelog record reader-side invalidation as built, the unknown-windows branch, and the unattributed-entry gap; plugin versions and changelogs are updated. - No change in the account-rotation repo. ## Verification - `bash scripts/check-loop-lane-floor-drift.test.sh`: PASS=31 FAIL=0. - `bash scripts/check-loop-lane-floor-drift.sh --check`: 6 consumers match, no unregistered carrier. - `bash scripts/check-changelog-parity.sh --check --check-order`: passes on the merged tree. Versions above main: docs-hygiene 0.23.20 (main 0.23.19), rate-limit-guard 0.9.0 (main 0.8.38), source-control 0.65.0 (main 0.64.1), work-items 0.43.0 (main 0.42.1). `check-stale-base-overlap.sh --check main`: up to date. - `check-skill.sh` passes for work-loop (498 lines), babysit-loop and attend-queue; `check-evals-quality.sh` reports no warning on the new cases; `markdownlint-cli2` and `typos` are clean on the changed files. - CI on head `a911e6f4e`: `lint`, `lint-2`, `test-linux` (4 shards), `hook-utils`, `managed-files-guard`, `ci-status` and GitGuardian passed. `review`, `security-review` and `test-windows` were skipped (draft). An earlier run on the previous merge commit failed `typos` (`unparseable`) and the work-loop 500-line cap (505 lines); both are fixed. - Not run: the evals themselves (`claude plugin eval`); they are statically validated only. The head is one commit behind `origin/main` with no overlapping paths (`check-stale-base-overlap.sh`); the ready flow's base merge handles it. ## Related - #5177 (owner decision, Option 3); #1218 and PR #3778 (writer-side design). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <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.
No related issue: #1218 is closed as not planned; this change lands the writer-side half of the account-identity design that issue framed, driven by Phase 6 of the account-rotation plan (melodic-software/claude-code-account-rotation#3 carries the plan). Refs #1218.
Summary
The rate-limit-guard tee snapshot now says whose windows it carries. The drain stamps
account: {"email": "<address>"}at the top level, read from Claude Code's own.oauthAccount.emailAddressin${CLAUDE_CONFIG_DIR:-$HOME}/.claude.jsonwith onejq -rover stdin. Mislabeling is worse than absence, so the field is omitted whenever the writer cannot attribute the observation: the state file is missing, unreadable, or holds no email-shaped value; the stdin payload already carried a top-levelaccount*key (that one wins); the state file was modified after the chosen record's spool file, which means a switch may have happened in between; or the path has no spool file to date that comparison against (RLG_TEE_ASYNC=1, bash below 4.2). Statusline output and exit codes are unchanged in every case, and the render path stays fork-free.The "Operable floor" staleness bullet in the reader contract said the file carries no account-identifier field. This PR makes that false, so the bullet is amended in the source and in every registered carrier in the same change; the floor-drift gate proves the six copies moved together. Reader-side invalidation of latched state and the lane-floor re-audit remain out of scope (
TODO(#1218)); no consuming lane acts on the field yet.Fix
plugins/rate-limit-guard/scripts/statusline-tee.sh, drain path only: the batch jq pass emits two more lines (the chosen record's shard name, and whether its body already carries an account-shaped key, asked withkeys_unsorted);_rlg_account_emailresolves and whitelist-validates the address (@present, no",\, or control character, 3 to 254 characters);_rlg_inject_accountsplices it into the compact body by parameter expansion. Header comments document the three guards and the measurements._rlg_absorb_jq_linesnow CR-strips every token line after the payload. A native jq on Windows terminates output lines with CRLF whileread -rsplits on LF only, and MSYS command substitution strips only the trailing CRLF, so exactly one token line was ever clean: the last one. The suite passed by accident of line count; adding two lines made both existing tokens wrong at once and broke 14 assertions. The payload line keeps its CR (the snapshot's bytes stay what jq wrote). The merge base scores 125 pass, 0 fail on this desktop; the branch scores 138, 0. Two new CRLF cases guard the strip; they are inert on Linux and were proven non-vacuous by removing the strip expressions from a scratch copy of the writer.plugins/rate-limit-guard/scripts/statusline-tee.test.sh: six account cases (present, absent,CLAUDE_CONFIG_DIR, six malformed values with byte-identical stdout, stdinaccount*key wins, state file newer than the spool record) plus the two CRLF guards; the zero-fork render trace case still passes.plugins/rate-limit-guard/reference/reader-contract.md: floor bullet amended; "Tee file shape" documents the key, its source, the four absence cases, the staleness guard, and the recheck trigger (.oauthAccount.emailAddressis internal CLI state), with the untrusted-value rule intact.plugins/work-items/skills/work-loop/SKILL.md,plugins/work-items/skills/attend-queue/SKILL.md,plugins/source-control/skills/babysit-loop/SKILL.md,plugins/docs-hygiene/skills/extract-ssot/context/orchestrated-mode.md,prompts/loops/loop-lane-prompts.md,prompts/loops/loop-lane-profile-claude-code-plugins.md.docs/conventions/loop-lane/README.md§6 account-identity paragraph: "designed elsewhere" becomes "writer-side half has landed; the other two sides remain rate-limit-guard: same-machine account rotation is real operation — tee file's single-account assumption feeds wrong windows to running lanes (account-identity design) #1218 follow-up".plugins/rate-limit-guard/README.mdmulti-account paragraph,CHANGELOG.md0.8.0entry,.claude-plugin/plugin.json0.7.33to0.8.0(additive field, minor bump).Verification
bash plugins/rate-limit-guard/scripts/statusline-tee.test.shprintsPASS=138 FAIL=0and exits 0;bash scripts/check-loop-lane-floor-drift.sh --checkreports 6 consumers matching the floor and no unregistered copy, exit 0;bash scripts/check-loop-lane-floor-drift.test.shprintsPASS=28 FAIL=0; shellcheck is clean on both scripts; markdownlint, typos, and editorconfig-checker are clean on every changed file.git diff 39442ff67 HEAD: the four plan sanity checks; the scope fence (exactly the 13 allowed files, each carrier's diff a single floor hunk, the drift gate's registry unchanged); drain-only cost (the only newjqcall sits in_rlg_account_email, reached solely from_rlg_drain, and_rlg_tee_runreferences none of the new functions); each guard with its test case; the CR strip touching only token lines; comment hygiene; the version bump; shellcheck. All eleven PASS.jq -rover stdin 35 ms;claude auth status --json175 ms. Cost of this change: one process per 30-second drain.check-loop-lane-floor-drift.sh --list: sourceplugins/rate-limit-guard/reference/reader-contract.md; exactplugins/work-items/skills/work-loop/SKILL.md,plugins/work-items/skills/attend-queue/SKILL.md,plugins/source-control/skills/babysit-loop/SKILL.md,plugins/docs-hygiene/skills/extract-ssot/context/orchestrated-mode.md; valuesprompts/loops/loop-lane-prompts.md,prompts/loops/loop-lane-profile-claude-code-plugins.md. Every otherrate-limits.jsonreader at the merge base:plugins/disk-hygiene/CHANGELOG.md,plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py,plugins/rate-limit-guard/CHANGELOG.md,plugins/rate-limit-guard/README.md,plugins/rate-limit-guard/bench/README.md,plugins/rate-limit-guard/skills/setup/SKILL.md,plugins/rate-limit-guard/skills/setup/evals/evals.json,plugins/session-flow/skills/orchestrate/context/gotchas.md,plugins/source-control/skills/babysit-loop/evals/evals.json,plugins/work-items/skills/work-loop/evals/evals.json, andscripts/check-loop-lane-floor-drift.test.sh(the gate's own abridged fixture, excused as a data carrier). None of them states the floor text; none needed a change.Related
docs/topics/claude-subscription-rotation/PLAN.md, Phase 6) and the dashboard reader that consumesaccount.emailwhen present.🤖 Generated with Claude Code
https://claude.ai/code/session_01QKU99jG34EDYBz1y2g5vVF