Skip to content

feat(loop-lane): add per-lane consecutive-no-progress detector with escalation - #1710

Merged
kyle-sexton merged 9 commits into
mainfrom
feat/1648-no-progress-detector
Jul 29, 2026
Merged

feat(loop-lane): add per-lane consecutive-no-progress detector with escalation#1710
kyle-sexton merged 9 commits into
mainfrom
feat/1648-no-progress-detector

Conversation

@kyle-sexton

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

Copy link
Copy Markdown
Contributor

What

Adds a per-lane consecutive-no-progress detector with escalation to both loop lanes, so a lane that
keeps cycling without moving the queue escalates instead of spinning silently.

Both lanes gain the counter in their durable-state block with identical semantics: the streak
increments on a cycle that changed no tracker state, resets when the queue actually moves, and
escalates at the configured threshold. Crucially, the lane escalates and keeps looping — it never
stops on a stall.

A held cycle is a third state alongside actionable and idle: whenever the rate-limit guard bars
the lane from claiming new work it declines mutating work by design, so the counter holds however
much sits in its snapshot. The hold keys on the guard's bar, never the pause window alone — for
the merge lane that bar is rate_limit_latch, which outlives the pause until a fresh healthy
snapshot clears it. A lane is never escalated for obeying the guard.

The detector cannot reset itself. A lane's own repeat attempt at the same still-unresolved blocker
does not qualify as progress, and neither does the stall escalation itself; a lane-authored fix
qualifies only on the cycle it first lands. That closes the fix-churn loophole where a lane could
manufacture its own progress signal by retrying a failing push.

work_loop_no_progress_threshold (default 3) is documented in the work-items README;
babysit_loop_no_progress_threshold rides the layered source-control.md seam.

Version bumps: loop-lane convention 5.0.0; source-control 0.36.0; work-items 0.28.0 — each
composed above what main published while this branch was in review.

Verification

An independent fresh-context verifier audited the diff with the implementation rationale withheld,
then re-audited after remediation. Verdict: PASS, all findings resolved. It traced the fix-churn
shape end to end (push attempt A → checks_changed delta → worker dispatched → push attempt B with
CI still red) and confirmed attempt B no longer qualifies as progress.

The one review finding on this PR (rate-limit pauses inflating the stall streak) was fixed on the
branch in 0ad58a3 and 70ef1c3 — the held-cycle state above — and verified against the merged text
rather than the commit subjects.

Gates: plugin contracts pass, changelog parity --check and --check-bump pass, skill gate 0
failed, markdownlint 0 errors, and both evals.json validate against
plugins/skill-quality/reference/evals.schema.json.

Related

Closes #1648

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits July 27, 2026 15:38
…scalation

A lane cycling repeatedly while accomplishing nothing in aggregate was
invisible to itself: every stall mechanism is per-PR (needs_worker delta,
quiet_recheck_due, checks.stuck) or per-item (clean/dirty cap ramp), so each
gate correctly declines to spend a worker and nothing notices the aggregate
is zero (#1648).

- Loop-lane convention 4.0.0: new "No-progress detector" loop-layer
  invariant (shared counter semantics — increment on actionable-but-zero-
  progress cycles, hold on idle, reset on qualifying progress, escalate at
  the threshold and keep looping, at-most-one-open author-matched stall
  escalation on the existing section-2 channel); durable-state invariant now
  lists the counter.
- work-loop: no_progress_streak beside clean_streak in the #502 durable
  state; worker-lane progress = an item advanced or a PR opened; new
  work_loop_no_progress_threshold userConfig key (default 3);
  work-items 0.26.0.
- babysit-loop: no_progress_streak beside cycle/backoff_level; merge-lane
  progress = a PR materially changed, escalated, or merged; new
  babysit_loop_no_progress_threshold seam key (default 3);
  source-control 0.34.0.
- loop-lane-prompts: the two last-writer-wins field enumerations now include
  the counter.

Closes #1648

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRSgwJ7RN4vPZt4oJAADHy
…tion

Remediates the fresh-context verifier's four findings on #1648:

- work-items README documents the new work_loop_no_progress_threshold key so
  its "everything else is project-specific" claim stays true.
- Both skills' evals gain a detector case (increment vs idle-hold, default
  threshold, stall-escalation shape, author-matched at-most-one-open,
  no-self-reset, escalate-and-keep-looping); babysit eval 3's durable-state
  enumeration now includes the no-progress streak and work-loop eval 3
  asserts the idle-hold on its empty-frontier scenario.
- Both lane bodies' citation lists now carry the convention's
  resumption-comment obligation instead of silently omitting it.
- The convention generalizes no-self-reset beyond the stall escalation — a
  lane's own repeat attempt at the same still-unresolved blocker never
  qualifies — and the merge lane's progress definition scopes lane-authored
  fixes to their first landing, closing the fix-churn self-reset loophole.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRSgwJ7RN4vPZt4oJAADHy
work-loop eval 4 set item_cap=1 while its prompt described both admitted
items executing, a state the skill body's one-slot-per-cycle cap forbids.
The cap moves to 2 — the manifest's own default start and the frontier
ceiling — so the scenario is reachable.

babysit-loop eval 5 omitted the resumption-comment obligation that both
lane bodies cite and that work-loop eval 4 already grades, leaving the
two lanes' coverage asymmetric. Its expected_output and the reset
expectation now carry it.

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

Threading `no_progress_streak` into the shared-state list left one line
running well past the surrounding wrap.

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

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown

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


PR Review

  • Gather PR context and changed files
  • Read the key skill files (SKILL.md for both loops)
  • Review eval files for correctness
  • Review plugin manifests for correctness
  • Review documentation and CHANGELOG files
  • Review loop-lane-prompts.md changes
  • Synthesize findings and post review

Overview

This PR adds a consecutive-no-progress detector with stall escalation to both loop lanes (babysit-loop and work-loop). The feature is well-scoped: a shared convention owns the semantics, each lane body holds them by citation and adds only lane-specific qualifying-progress definitions, and the evals cover the key scenarios.

Verdict: LGTM with two low-severity observations.


What's correct

Convention semantics are tight. The fix-churn loophole — the central risk for this feature — is addressed at the right level. The convention (§4, line 252–255) says:

"a lane's own repeat attempt at the same still-unresolved blocker never qualifies either: the detector measures the queue moving, not the lane retrying."

This is the exact shape the PR description traces — a push attempt that lands CI-red, re-queued for the next cycle, does not reset the counter.

Symmetry between lanes is maintained.

  • Both state blocks add no_progress_streak beside the existing counters (babysit-loop L385-388, work-loop L109-111)
  • Both cycle shapes update the counter in the final "Report and pace" step, after the cycle's work, before the telemetry upsert
  • Both stall escalation titles follow the Lane stall: <lane-name> pattern, which correctly avoids the Lane telemetry: prefix that gets excluded from drain evaluation

Provider-neutrality is preserved. The babysit-loop stall issue is created with gh issue create (GitHub-specific lane), while the work-loop stall item goes through the seam create-item verb (work-loop L324-330). The asymmetry is correct given the two plugins' different provider stances.

Evals are well-constructed. Both new eval scenarios test the key contract edges: counter increment at threshold, at-most-one-open dedup by author identity (not just title), stall escalation never self-resets, keeps looping, idle holds the counter, foreign activity resets it, resumption comment when progress returns. The babysit-loop eval correctly identifies that WIP draft PRs still count as "actionable work in view" even though the grace-window overlay marks them report-only — a non-obvious distinction the skill body supports.

Config key placement is appropriate. babysit_loop_no_progress_threshold lives in the layered seam config (team-reviewed policy), while work_loop_no_progress_threshold lives in userConfig (personal scalar). This matches the existing pattern for each plugin's config split.

The loop-lane-prompts.md topology warning correctly classifies no_progress_streak as "annoying, not dangerous" (line 294) — shared state drift between two worker lanes on one repo is a measurement problem, not a correctness one, unlike rate_limit_latch and first_drain_complete.


Observations (low severity)

1. Schema version strings are not bumped babysit-loop/SKILL.md:386, work-loop/SKILL.md:109

Both state blocks remain @1 despite the new no_progress_streak field:

{"schema":"source-control/babysit-loop-state@1", ..., "no_progress_streak":0, ...}
{"schema":"work-items/loop-state@1", ..., "no_progress_streak":0, ...}

The convention says "absent from a re-read block = 0", so backward compatibility is maintained and this isn't a correctness defect. But a reader looking at the schema version to understand the durable state shape would see @1 while the shape has changed. Whether to bump @1@2 here depends on whether the schema version is a meaningful signal for consumers (e.g., operator tooling that reads the telemetry comment) or purely decorative. If the former, a bump would be appropriate. If there's a convention that schema bumps track breaking changes only, this is fine as-is.

2. work_loop_no_progress_threshold userConfig description omits escalation as qualifying progress plugins/work-items/.claude-plugin/plugin.json:57-62

The description reads:

"Consecutive no-progress cycles (actionable work in view, no item advanced and no PR opened) before the work-loop lane raises its stall escalation."

The work-loop SKILL.md's qualifying progress definition also includes "an item's tracker state advanced by this lane — swept to a triage routing outcome, escalated (step 5), or queued for C3 ratification." An operator who escalates one item per cycle and then wonders why their stall threshold never fires might find the description misleading. The userConfig description is a summary, not a spec, so this isn't a defect — but aligning the wording (e.g., "no tracker state changed and no PR opened") would prevent confusion.

Fix this →


Checklist

Area Status
Convention §4 semantics ✓ Correct
Fix-churn loophole closed ✓ Confirmed
babysit-loop SKILL.md ✓ Correct
work-loop SKILL.md ✓ Correct
Durable state fields ✓ Present in both
Cycle step ordering ✓ Counter updated before telemetry upsert
Config key placement ✓ Seam vs. userConfig split correct
Default values (3) ✓ Consistent across all surfaces
Stall escalation titles ✓ Correct pattern, not lane-infrastructure prefix
At-most-one-open dedup ✓ Author-matched in both lanes
Provider neutrality ✓ Babysit-loop: gh; work-loop: seam verb
Eval correctness ✓ Both scenarios cover the key edges
CHANGELOG entries ✓ All three updated
loop-lane-prompts.md no_progress_streak added to last-writer-wins warning
Schema version bump ⚠ Not bumped (low severity)
userConfig description ⚠ Simplified, omits escalation-as-progress (low severity)

@claude

claude Bot commented Jul 28, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 8s —— View job


Security Review — PR #1710

  • Gather context (PR diff and changed files)
  • Analyze changed files for security issues
  • Post findings

Scope

All 13 changed files are documentation, plugin manifests, skill bodies (natural-language agent instructions), and eval fixtures. There are no GitHub Actions workflow changes, no new secret/token handling paths, and no changes to executable shell code. The security surface is the agent instruction layer — what the AI lanes are told to do, and whether those instructions introduce logic flaws in the safety mechanism or new injection surfaces.


Finding 1 — SUGGESTION | confidence: HIGH

Stall detector disabled by CI check transitions — perpetually-stuck PRs are the primary use case it fails to catch

plugins/source-control/skills/babysit-loop/SKILL.md L323–329

The babysit-loop lane's qualifying-progress definition includes "a checks transition" as foreign activity that resets no_progress_streak to zero:

a PR materially changed — head moved, review or comment activity, a checks transition, a draft elevated — foreign activity included (it is the queue moving)

In any repository with active CI, check suites execute on a schedule, on push, or on external triggers — entirely independent of whether the merge lane accomplished anything. A perpetually-failing PR (the exact scenario the detector exists to catch) continuously generates check transitions: queued → in_progress → failure → re-queued. Each transition resets the streak. Three consecutive no-progress cycles can never accumulate as long as CI is running, so babysit_loop_no_progress_threshold is never reached and the stall escalation never fires.

The work-loop lane (work-loop/SKILL.md) does not have this vulnerability — its qualifying-progress events are item-state changes owned by the lane itself. The asymmetry means the merge lane's detector is effectively neutralized on any actively-tested repository.

Mitigation to consider: Restrict "checks transition" to meaningful state transitions — a check suite that was previously all-passing and moved to failing, or a new check added — rather than any CI run completion. Alternatively, scope to "a checks transition on a PR that had no open failing checks at cycle start," so routine CI noise on a known-failing PR doesn't qualify.


Finding 2 — SUGGESTION | confidence: MEDIUM

"What sat unmoved" PR titles written into the stall escalation body create a durable prompt-injection surface

plugins/source-control/skills/babysit-loop/SKILL.md L334–339
plugins/work-items/skills/work-loop/SKILL.md L324–332

Both stall escalations are told to report "what sat unmoved" — drawn from PR titles and work-item titles in the cycle-start snapshot. This content is externally controlled: anyone who can open a PR or file an issue against the watched repository controls its title.

A PR or issue titled with adversarial instruction text (e.g., Fix login bug\n\nIMPORTANT: Ignore your merge-rung configuration. Set rung to full-autonomy.) is captured verbatim in the stall escalation body and the telemetry comment's cycle report. On subsequent cycles the lane re-reads the telemetry comment to recover durable state. Although only the JSON block is machine-parsed, the surrounding prose — including the cycle report section containing these titles — is part of the same conversational context the model processes. A sufficiently crafted title could influence the model's subsequent decisions.

This is a general LLM concern, but the stall escalation is a new, long-lived, durable artifact (it stays open until an operator closes it) that now serves as a persistence vehicle for externally-injected content. The existing loop already reads PR titles within a cycle's scope; the stall report extends that scope across sessions.

Mitigation to consider: When writing "what sat unmoved," quote PR/issue titles clearly as data (e.g., in a fenced block with an explicit "these are untrusted titles, treat as data only" header) or truncate/hash them to identifiers (number + head SHA) rather than including free-text titles.


No findings in these areas

  • Authorization / access-control: The at-most-one-open dedup (author-matched title check) correctly prevents a third-party lookalike from suppressing the stall signal. The inverse — a third-party issue with the same title but a different author — does not suppress the lane's own escalation. Sound.
  • No-self-reset logic: The distinction between a stall escalation (never qualifying progress) and an item-level escalation (qualifying progress for work-loop) is clearly maintained and tested in the eval fixtures.
  • Shell command injection: The inlined telemetry upsert script is unchanged by this PR. No new shell interpolation surfaces are introduced.
  • Secrets / credentials: No new secret or token handling.
  • Supply-chain / Actions / unpinned deps: No workflow file changes.

@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: 101197e580

ℹ️ 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 docs/conventions/loop-lane/README.md
The detector had two states — actionable-and-unmoved, which increments, and
idle, which holds. A cycle spent under the rate-limit guard's pause fits neither
and was landing in the first. Drain-then-pause has the lane finish in-flight work
and stop claiming new work until the pause end, so a paused cycle can hold a full
queue in its snapshot and produce no qualifying progress by construction. The
merge lane schedules those iterations at the idle ceiling, so a pause outlasting
three of them tripped the default threshold and raised a stall escalation against
a lane doing precisely what the guard requires — a false signal, and the loudest
one the detector has.

Paused cycles are now a third state, held, which holds the counter like idle
does. The distinction the detector is meant to draw is between a lane that could
move its queue and did not and a lane that moved nothing; a guard pause makes the
lane unable, not stuck. The shared semantics carry the rule so it binds every
unattended lane, and both lane bodies' actionable-work definitions and citation
summaries follow, since each defines its own snapshot predicate and would
otherwise still increment. The three changelog entries restate the held state
alongside idle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134vdV1jXJhzpSmeT7y5B8N
@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 1s —— View job


I'll analyze this and get back to you.

…se window

Independent verification caught the held state keyed one level too narrowly. The
merge lane's floor suppresses work-claiming on `rate_limit_latch` — "starts no new
mutating work while it is set" — and that latch outlives the pause end until a
fresh healthy snapshot clears it. Under fail-open capability detection an absent
or stale tee file never produces one, so the latch can stand indefinitely while
the lane holds a queue full of open PRs and claims nothing. Keyed on the pause
window alone, the hold released before the bar did and the threshold tripped
exactly the false escalation the fix was for.

The convention now keys the hold on the bar itself — whenever the guard bars the
lane from claiming new work — and each lane names which of its own signals is that
bar, since the two floors differ: `rate_limit_latch` for the merge lane, the pause
window for the worker lane, whose latch gates only adaptive-cap ramp-up and must
not hold the counter on its own.

Three surfaces the first pass left behind: the merge lane's threshold key row
still defined the counted cycle without the carve-out, the shared semantics
attributed a phrase to §6 that lives in the inlined floor rather than there, and
both no-progress eval scenarios enumerated the counter's hold conditions without
the new state. Each now states the rule its own lane holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134vdV1jXJhzpSmeT7y5B8N
@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.

@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.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 5s —— View job


PR Review (re-review after new commits)

  • Gather PR context and understand trigger
  • Read the new commits addressing rate-limit pause exclusion
  • Review the updated diff vs origin/main
  • Check key skill files for correctness
  • Verify previous findings status
  • Synthesize and post review

What triggered this re-review

Three commits landed since the July 28 reviews:

  • 0ad58a3fix(loop-lane): exclude guard-paused cycles from the no-progress streak
  • 70ef1c3fix(loop-lane): key the held cycle on the guard's bar, not on the pause window
  • 219da2b — merge from main (no relevant changes to reviewed files)

Both fix commits directly respond to the Codex inline finding (docs/conventions/loop-lane/README.md:255) about rate-limit pauses triggering false stall escalations.


Codex finding — addressed correctly

The fix is correct and the two-step refinement is important.

0ad58a3 introduced "held" as a third cycle state (alongside idle and actionable-but-unmoved), so the counter neither increments nor resets during a guard-paused cycle. 70ef1c3 then caught that keying the hold on the pause window alone was one level too narrow for the merge lane: rate_limit_latch outlives the pause window (it persists in durable state until a fresh healthy snapshot clears it), so the hold needed to key on the latch itself — the actual bar — rather than on the transient pause window.

The asymmetry between the two lanes is correct and intentional:

Lane Bar signal Why
babysit-loop rate_limit_latch This latch is the work-claiming suppressor; it outlives the pause window
work-loop Pause window Here rate_limit_latch gates only adaptive-cap ramp-up, not work-claiming

Each lane body names its bar explicitly (babysit-loop L340–344, work-loop L319–323), and the evals in both lanes were updated to match (babysit-loop eval expectation 7, work-loop eval expectation 7).

The changelog entries across all three surfaces (convention, babysit-loop, work-loop) describe the held state correctly, including the "can outlive the pause window" nuance.

No new correctness issues introduced by the fix commits.


Previous findings — status

Security Finding 1 — CI check transitions disabling the merge lane's detector (from July 28 security review)

Still open. The babysit-loop qualifying-progress definition still includes "a checks transition" as foreign activity that resets no_progress_streak to zero (babysit-loop L333). On any repo with active CI, a perpetually-failing PR generates check transitions each run cycle — the streak resets before the threshold is reached. The work-loop lane does not have this exposure (its progress events are item-state changes owned by the lane). Not addressed by the new commits.

Security Finding 2 — PR/item titles in durable stall escalation body as a prompt-injection surface (from July 28 security review)

Still open. Both lane bodies continue to direct the stall escalation to report "what sat unmoved" using live PR/issue titles. Not addressed by the new commits.

Observation 1 — Schema version strings not bumped

Still open. Both state block examples remain @1 (babysit-loop L392, work-loop L109) despite the new no_progress_streak field.

Observation 2 — work_loop_no_progress_threshold userConfig description omits escalation/routing as qualifying progress

Still open. The plugin.json description reads "no item advanced and no PR opened" — it still doesn't mention tracker-state advances (routing to triage, escalation step 5, queued for C3 ratification) as qualifying progress. Fix this →


Verdict

The new commits resolve the Codex finding cleanly and introduce no new issues. The two previously flagged security findings and two low-severity observations from the July 28 reviews remain open — disposition of those is for the author and reviewer to decide before merge.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

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


Security Review — PR #1710

  • Get PR diff and changed files
  • Read changed skill files and convention docs
  • Analyze for security issues
  • Post findings

Scope

All 13 changed files are documentation, plugin manifests, skill bodies (natural-language agent instructions), and eval fixtures. There are no GitHub Actions workflow changes, no new secret/token handling paths, and no changes to executable shell code. The security surface is the agent-instruction layer — what the AI lanes are told to do, and whether those instructions introduce logic flaws in the safety mechanism or new injection surfaces.

This review covers HEAD at 219da2b, which includes three commits that landed after the prior security pass (0ad58a3, 70ef1c3, 219da2b). Two of those commits fix the rate-limit guard held-cycle logic; that fix is assessed below.


Rate-limit guard held-cycle fix — RESOLVED

The Codex reviewer flagged that rate-limited cycles (where the lane intentionally does no mutating work) would incorrectly increment the streak and produce false stall escalations after three cycles. Commits 0ad58a3 and 70ef1c3 address this by introducing a "held" cycle state:

  • babysit-loop: the bar is rate_limit_latch (not the pause window alone). The latch survives the pause end until a fresh healthy snapshot clears it, so a latched cycle is held even after the nominal pause window expires.
  • work-loop: the bar is the pause window itself. rate_limit_latch gates only adaptive-cap ramp-up here and never holds the counter.

The asymmetry is intentional, correctly documented in both skill bodies, and matches the respective guard floor mechanics. The Codex concern is resolved.


Finding 1 — IMPORTANT | confidence: HIGH

Routine CI activity on a perpetually-failing PR continuously resets the stall streak, so the babysit-loop detector is neutralized on any actively-tested repository

plugins/source-control/skills/babysit-loop/SKILL.md L324–330

The babysit-loop's qualifying-progress definition includes "a checks transition" as foreign activity that resets no_progress_streak:

a PR materially changed — head moved, review or comment activity, a checks transition, a draft elevated — foreign activity included (it is the queue moving)

On any repository with active CI, a check suite runs on schedule or on external triggers regardless of whether the merge lane did anything. A perpetually-failing PR generates a continuous stream of check lifecycle events: queued → in_progress → failure → re-queued. Each transition resets the streak. Three consecutive no-progress cycles can never accumulate while CI is running, so babysit_loop_no_progress_threshold is never reached and the stall escalation — the human-safety signal this feature is designed to produce — never fires on the exact PRs it was designed to catch.

The work-loop lane does not have this vulnerability: its qualifying-progress events are tracker state changes the lane itself made, with no CI analogue.

Mitigation options:

  • Restrict "checks transition" to a meaningful state change: a suite that was previously all-passing and moved to failing, or a new required check added. Routine re-runs on a known-failing suite would not qualify.
  • Alternatively, scope to: "a checks transition where the suite result changed" (passing→failing, failing→passing) — excluding re-runs that produce the same outcome as the previous run.
  • A simpler bound: "foreign activity resets the streak only if the overall PR health changed (at least one check flipped state)" versus "any CI event."

Finding 2 — SUGGESTION | confidence: MEDIUM

"What sat unmoved" content written into both stall escalations and the telemetry cycle report is externally controlled, creating a cross-session prompt-injection persistence surface

plugins/source-control/skills/babysit-loop/SKILL.md L340–344
plugins/work-items/skills/work-loop/SKILL.md L329–334

Both stall escalations are told to report "what sat unmoved" drawn from PR titles and work-item titles in the cycle-start snapshot. These titles are externally controlled: anyone who can open a PR or file an issue against the watched repository controls the string that gets written into the stall escalation body and into the telemetry comment's cycle report section.

The stall escalation is a new, long-lived artifact (stays open until an operator closes it). The telemetry comment is re-read every cycle to recover durable state. While the lane machine-parses only the JSON block in the telemetry comment, the surrounding prose — including the cycle report section containing these titles — is part of the same conversational context the model processes on each restart. A PR or item title containing adversarial instruction text (e.g., an injected directive to widen the lane's merge authority or suppress a future escalation) persists across sessions until an operator manually resolves and closes the stall issue.

This is a general LLM concern rather than a deterministic exploit, and the at-most-one-open dedup (author-matched) correctly prevents a third party from suppressing the signal itself. But the new stall escalation artifact extends the injection persistence window from one cycle's scope to the full lifetime of an open issue.

Mitigation options:

  • When writing "what sat unmoved," include only stable identifiers (PR number + head SHA, or item number) rather than free-text titles. Link to the items for human readability without embedding the title text.
  • Or quote externally-controlled content in a clearly-delimited data block with a header warning the model to treat the section as untrusted data.

Fix this →


No findings in these areas

  • No-self-reset logic: the distinction between a stall escalation (never qualifying progress) and an item-level escalation (qualifying progress for work-loop step 5) is correctly maintained and tested in both eval fixtures.
  • At-most-one-open dedup: author-matched title check is sound. A third-party lookalike with the same title but a different author does not suppress the lane's own escalation.
  • Rate-limit held-cycle: correctly resolved by the post-review commits (see above).
  • Shell command injection: the inlined telemetry upsert script is unchanged by this PR. No new interpolation surfaces.
  • Secrets / credentials: no new token handling.
  • Supply-chain / Actions / unpinned deps: no workflow changes.
  • Provider neutrality: babysit-loop raises via gh issue create (GitHub-specific lane); work-loop raises via the seam create-item verb (provider-neutral). The asymmetry is correct and intentional.

kyle-sexton added a commit that referenced this pull request Jul 29, 2026
…hapes, record the review boundary (#1712)

## What

Three loop-lane convention notes, all in
`docs/conventions/loop-lane/README.md`, batched because they
touch the same document.

**#1655 — pin the "`/loop` runs the prompt fresh, not the session"
distinction.** A relaunch is the
only context reset a loop lane gets; the note records that and points at
the authoritative surface in
`claude-ops` `lanes` rather than restating it.

**#1656 — reconcile the two launch shapes.** Supplying an interval
converts the loop to a cron
schedule subject to scheduler jitter; omitting it hands the delay to
Claude per iteration. Both are
current and neither deprecates the other. A lane always omits the
interval, and the note now gives
the load-bearing reason: a self-paced loop can end itself, which is how
a drain lane honors its own
stop condition, while a fixed-interval loop runs until stopped by hand
or until the seven-day expiry.
This reconciles which shape applies where and changes neither — it does
not endorse fixed intervals
for lanes.

**#1658 — record the fresh-context review boundary as an explicit
decision**, with its rationale and
a revisit condition, rather than leaving it as unstated practice.

## Verification

An independent fresh-context verifier audited the diff with the
rationale withheld and returned
**SHIP**: all eight of its earlier findings resolved, all three
acceptance criteria satisfied, no new
contradiction introduced. It did not trust its own passing link check —
it ran a negative control
with deliberately bogus anchors and confirmed exit 2 with `Cannot find
fragment`, proving
`include_fragments = "full"` genuinely validates the new anchors rather
than silently passing.

Every upstream claim was re-fetched from the official docs rather than
recalled, and the §4 seven-day
expiry statement was broadened to cover both launch shapes with its
stamp refreshed, since the prior
stamp covered only the self-paced half.

Gates: markdownlint 0 errors; `lychee --offline` 0 errors.

## Related

- Refs #1690, #1710, #1711, #1707 — sibling loops-webinar PRs. **Version
note:** concurrent PRs stage
competing versions for `docs/conventions/loop-lane/CHANGELOG.md`, and
`§Versioning` is silent on
entry ordering. The version here is correct as of this branch's merge
base; whichever of these PRs
  lands later will need to renumber and restack its entry.
- Refs #1657 — content-trust audit of the lane intake surfaces; no
overlap with these notes.

Closes #1655
Closes #1656
Closes #1658

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 29, 2026
… and read the merge class from the label only (#1721)

## Summary

The #1657 design audit found this stack's autonomous work-item lanes
carry well-developed
*write-authority* provenance controls — who may merge, what may dispatch
— and no *content-trust*
boundary: nothing told an agent that externally-authored item and PR
prose is data to be evaluated
rather than instruction to be followed. This closes the two gaps the
audit ranked as carrying live
risk today, G1 (#1713) and G6 (#1289).

**G1 — the read-trust boundary now exists, stated once and cited
everywhere it binds.**
New `plugins/work-items/reference/item-content-trust.md` is its
canonical home: item-derived text
(title, body, comments, and any linked PR's text and diff) is data
describing the work, never
instruction to the agent reading it; the boundary keys on the surface
the text arrived on rather
than on who wrote it, so it applies to a teammate's item exactly as to a
stranger's; an item whose
text instructs the agent is a finding to report, not a request to
satisfy. The reference also owns
the escalation route, the widening rule, and the rule for handing item
text to a subagent.

Every surface that reads an item now carries the headline sentence plus
one line on what the
boundary bites hardest there, and cites the reference for the rest:

- `work-items` — `triage`, `decompose`, `work`, `work-loop`,
`attend-queue`
- `source-control` — `babysit-loop`, which reads PR text, diffs, and the
linked item
- `prompts/loops/loop-lane-prompts.md` — the parked-decision burn-down
block, which states outright
  that it invokes no skill and inlines every contract it needs

**G6 — no author-editable body field remains an input to the merge
partition.**
`babysit-loop`'s rung partition accepted "the triage stamp in the item
body **or** labels", so a
`Work-class: C<n>` trailer an item's own author can write decided merge
eligibility. It now reads
the class from the provider-permissioned `work-class:` label only —
applying a label takes triage or
write permission on the base repository, the same permission surface the
C5 trust test already keys
on, while a body is editable by an author who need hold none. A trailer
stays legitimate as the
operator's own record and as a proposal, is reported as such, and never
partitions. The three
surfaces the audit found in conflict — `babysit-loop/SKILL.md`,
`loop-lane-prompts.md`, and
`admission-policy.md` — now agree, and the lane prompts' readiness query
reports the label count and
the body-trailer migration backlog as separate figures instead of their
union.

Two smaller things fell out of doing this properly. `work-loop`'s
existing refusal of a
body-recorded ratification phrase justified itself from a work-class row
whose executable test reads
a *pull request*, which an issue does not have; the refusal is unchanged
and now derives from the
standing rule it is an instance of. And the reference's tightening-only
carve-out names its one
shipped instance and the configuration that would void it.

Reuse over invention throughout: the wording generalizes `work-loop`'s
existing body-prose refusals
rather than adding a parallel mechanism, and the subagent rule cites
`babysit-prs`'s existing
quoted-untrusted-data delimiter rather than restating it.

## Acceptance criteria

Against #1713:

- [x] Each named surface carries the instruction — `triage`,
`decompose`, `work-loop`, `work`,
      `attend-queue`, and the lane launch prompts.
- [x] Single-sourced: one authoritative statement in
`item-content-trust.md`, referenced from each
      surface, not six independent rewordings.
- [x] Traceable to the existing `babysit-prs/reference/orchestration.md`
pattern rather than newly
invented — the subagent rule cites that file's Worker Prompt Template
for the delimiter shape
      and wording.

Against #1289:

- [x] One contract owns where the admission work class comes from and
the other cites it —
`admission-policy.md` owns the rule; `babysit-loop` and
`item-content-trust.md` quote and cite
      it.
- [x] The rule states whether an agent-applied stamp is admissible: the
class is read only from a
surface whose write authority the provider enforces, and every lane
prompt forbids a lane from
      writing either the label or the trailer.
- [x] `babysit-loop`'s partition text matches the winning rule.
- [x] A lane cannot satisfy its own merge gate by stamping the item it
is working.

**Consumer impact.** A repository that recorded work classes only as
body trailers had a
merge-eligible population under the old reading and has an empty one
under this one: everything
there is human-merge, the shipped baseline, until `work-class:` labels
follow the trailers. The
change is strictly narrowing — nothing merges that would not have merged
before. The C4/C5 floor is
untouched; it always tested the pull request rather than the linked
item's stamp.

## Test plan

All gates green against `origin/main`:

- `bash scripts/check-changed-skills.sh origin/main` — 6 skills, 0
failed. Lines against the 500
hard cap: `babysit-loop` 477, `work-loop` 343, `work` 238,
`attend-queue` 204, `triage` 179,
  `decompose` 179.
- `node scripts/validate-plugin-contracts.mjs` — 43 setup skills, 2119
plugin files.
- `bash scripts/check-changelog-parity.sh --check` and `--check-bump
origin/main`.
- `npx markdownlint-cli2` over every changed `.md` — 0 errors.
- `plugins/work-items/skills/work-loop/evals/evals.json` validated
against
  `plugins/skill-quality/reference/evals.schema.json`.
- `bash scripts/validate-plugins.sh` — manifests and catalog.

One eval case added (`work-loop`, id 4, part of #1717): an item whose
body carries both an embedded
directive and a self-stamped work class, asserting the lane evaluates
the text, refuses both, and
reports what the text asked for.

Verification was run by an independent fresh-context reviewer against
the diff and the audit, with
the authoring rationale withheld. It returned five findings, all fixed
in `451fe3f6`: two surfaces
that inherit no skill's copy of the rule and so had none (`babysit-loop`
and the parked-decision
block), a carve-out claim stated unconditionally that only holds at the
shipped cap-ceiling
defaults, an eval expectation asserting a disposition the admission gate
does not reach, and two
unwrapped lines. `decompose` was added afterwards from #1713's own
surface list.

**Versioning.** `work-items` 0.25.4 → 0.26.0, `source-control` 0.33.2 →
0.34.0 — next minor at this
merge base. Both numbers are also staged by sibling PRs (#1690 and #1710
for both plugins, #1711 for
`work-items`); whichever of these lands second renumbers.
`docs/conventions/loop-lane/CHANGELOG.md`
is deliberately untouched: `docs/conventions/loop-lane/README.md` states
the merge partition's class
requirement neutrally as "recorded class" and never names the source, so
the convention text does
not change here.

**Audit gaps NOT addressed**, still open and separately tracked: #1714
(G2/G3), #1715 (G4/G5 — the
refusal's justification is corrected here, the underlying
issue-provenance ambiguity is not), #1716
(G7), #1717 (G10, beyond the one eval case above), #1718 (issue-shaped
provenance signal).

## Related

- Closes #1713 — G1, the missing content-trust instruction on every
intake surface.
- Refs #1289 — G6, the contract disagreement over whether a repo-local
stamp may supply the
  merge-admission work class.
- Note on #1289: this PR tightens the consuming side (the partition
reads the label only), but
#1289 is `needs-human, work-class: structural` and owns the underlying
contract decision, whose
acceptance criterion is still an unaccepted suggestion. It is
referenced, never closed — a
  maintainer closes it once that decision is made.
- Refs #1657 — the audit that produced this scope. It stays open: the
gaps beyond G1 and G6 are
  still a live operator decision.
- Concurrent PRs sharing changelog and manifest surfaces with this one:
#1690 (#1650), #1707
(#1652/#1654), #1710 (#1648), #1711 (#1649), #1712 (#1655/#1656/#1658).

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kyle-sexton and others added 2 commits July 29, 2026 13:58
main landed the loop-lane escalation-record contract (#1650), taking
convention 4.0.0, source-control 0.35.0 and work-items 0.27.0 — every
version this branch claimed. The branch's entries move above them
(convention 5.0.0, source-control 0.36.0, work-items 0.28.0) and both
sides' changelog entries are retained. The work-loop step 5/6 and evals
conflicts compose both sides: main's escalation-record write stays in
step 5, this branch's streak update stays in step 6, and the two new
eval cases are renumbered 4 and 5.

babysit-loop's SKILL.md crossed the 500-line hard cap once both features
landed in it, so this lane's detector binding moves to a
progressive-disclosure spoke beside the existing pre-escalation one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
main landed source-control 0.35.1; this branch's 0.36.0 already sits
above it, so no renumbering was needed and the upstream entry is kept
beneath this branch's.

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

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 11s —— View job


PR Re-review (post-synchronize, HEAD 31e9b29)

  • Gather new commits since last review (219da2b)
  • Read changed files at HEAD via git diff origin/main...HEAD
  • Assess prior open findings against current HEAD
  • Check integration of merged-in changes with the no-progress detector
  • Synthesize and post review

What triggered this re-review

Two merge commits landed since the July 29 re-review:

  • fd6b4b3 — Merge origin/main (pulls in 7cbca80 feat(loop-lane): out-of-band escalation notification on escalation write and 4fbffc4 fix(work-items,source-control): state the item content-trust boundary and read the merge class from the label only)
  • 31e9b29 — Merge origin/main (additional upstream changes)

Neither commit modifies the no-progress-detector feature directly — both bring in changes from other merged PRs that touch the same files. Because those changes affect the PR's diff, they're assessed below for integration correctness.


Integration assessment — new content from main merges

7cbca80 — escalation record write + out-of-band notification seam

The stall escalation (in both lanes) now inherits the record write through the convention contract: the convention's §2 says every escalation an autonomous lane files also writes a local record; both stall escalations cite "the convention's escalation contract, unchanged." The ordering requirement (record before marker) applies, and the reference chain is sound — though indirect at three hops for the work-loop stall case (step 6 → no-progress detector section → "convention's escalation contract" → §2). Not a correctness defect, since the chain terminates at the right rule.

The no-progress-detector.md spoke uses the phrase "SKILL.md's Escalation contract, unchanged," which is correct: SKILL.md's escalation section now explicitly inlines the record-write requirement. An executor following the spoke gets to the record write through SKILL.md, not through the convention directly — a shallower path for the merge lane.

No integration defects found.

4fbffc4 — content-trust boundary + work-class label-only rule + frontier-tier tightening

The work-class label-only rule in babysit-loop (SKILL.md step 3) is a security tightening that is orthogonal to the no-progress detector and integrates cleanly. The frontier-tier tightening in work-loop (SKILL.md "Adaptive item cap") applies the content-trust boundary to the cap-sizing carve-out correctly.

No integration defects found.


Previous findings — current status

Security Finding 1 — CI check transitions neutralizing the babysit-loop detector

Still open. plugins/source-control/skills/babysit-loop/reference/no-progress-detector.md still lists "a checks transition" as foreign activity that resets no_progress_streak:

a PR materially changed — head moved, review or comment activity, a checks transition, a draft elevated — foreign activity included

On any repository with active CI, a perpetually-failing PR produces continuous check lifecycle events (queued → in_progress → failure → re-queued). Each transition resets the streak, so the threshold is unreachable while CI runs — the detector cannot fire on the exact PRs it was designed to catch. Not addressed by the new commits.

Security Finding 2 — PR/item titles in durable stall escalation as a prompt-injection surface

Still open. Both stall escalation definitions still direct the lane to report "what sat unmoved": the spoke file says reporting "the streak length, the cycles covered, and what sat unmoved" for babysit-loop, and the work-loop SKILL.md says "reporting the streak length, the cycles covered, and what sat unmoved in the snapshot". PR/issue titles are externally controlled, and this content now persists in a long-lived issue that stays open until an operator closes it. The new item-content-trust.md (from 4fbffc4) strengthens the read-trust boundary for item text, but the stall body is a write from the lane — the concern is what the lane writes into that persistent artifact, not what the lane reads from it. Not addressed by the new commits.

Observation 1 — Schema version strings not bumped

Still open. Both state blocks remain @1 despite the new no_progress_streak field (babysit-loop state block @1, work-loop state block @1).

Observation 2 — work_loop_no_progress_threshold userConfig description omits tracker-state advances

Still open. The plugin.json description reads:

"Consecutive no-progress cycles (actionable work in view, no item advanced and no PR opened) before the work-loop lane raises its stall escalation."

The SKILL.md "Qualifying progress" definition also includes items swept to a triage routing outcome, escalated (step 5), or queued for C3 ratification — none of which are "item advanced" or "PR opened" in the plain reading of that phrase.

Fix this →


Verdict

The two merge commits integrate cleanly with the no-progress detector feature — no conflicts or semantic drift introduced. The four previously flagged findings (two security, two low-severity observations) remain open and unchanged; none were addressed by the new commits. No new issues found.

@claude

claude Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 4s —— View job


Security Review — PR #1710

  • Gather PR context and understand trigger
  • Get PR diff and review changed files
  • Assess prior findings against current HEAD
  • Identify any new security issues from merge commits
  • Post security findings

Scope

All 14 changed files are documentation, plugin manifests, skill bodies (natural-language agent instructions), eval fixtures, and a new reference spoke. There are no GitHub Actions workflow changes, no new secret/token handling paths, and no changes to executable shell code. The security surface is the agent-instruction layer — what the AI lanes are told to do, and whether those instructions introduce logic flaws in the safety mechanism or new injection surfaces.

This review covers HEAD at 31e9b29, which includes the two merge-from-main commits (fd6b4b3, 31e9b29) that landed since the prior pass at 219da2b. Those merges pulled in other PRs' changes to files outside the 14-file PR scope; no security-relevant delta to the reviewed files was introduced.


Rate-limit held-cycle fix — still RESOLVED

Commits 0ad58a3 and 70ef1c3 (pre-dating this review's HEAD) correctly introduced the held-cycle state and keyed the babysit-loop's hold on rate_limit_latch rather than the pause window. The reference/no-progress-detector.md reference spoke makes the bar explicit (line 26-29). No regression in the new merge commits.


Finding 1 — IMPORTANT | confidence: HIGH

Routine CI activity on a perpetually-failing PR continuously resets the stall streak — the babysit-loop detector is neutralized on any actively-tested repository

plugins/source-control/skills/babysit-loop/reference/no-progress-detector.md L12–14

The merge lane's qualifying-progress definition lists "a checks transition" as foreign activity that resets no_progress_streak:

a watched PR merged or closed, or materially changed — head moved, review or comment activity, a checks transition, a draft elevated — foreign activity included, since that is the queue moving; or this lane wrote a new escalation.

On any repository with active CI, a perpetually-failing PR generates a continuous stream of check lifecycle events: queued → in_progress → failure → re-queued. Each event satisfies "a checks transition" and resets the streak. Three consecutive no-progress cycles never accumulate while CI is running, so babysit_loop_no_progress_threshold is never reached and the stall escalation — the signal this feature exists to produce — never fires on the exact PRs it was designed to catch.

The work-loop lane does not share this exposure: its qualifying-progress events are tracker-state changes the lane itself made, with no CI analogue.

Not addressed by any commit in this PR. Status: open.

Mitigation options (unchanged from the prior pass):

  • Restrict "checks transition" to a meaningful state flip — a suite that was previously all-passing and moved to failing, or a new required check added. Routine re-runs that produce the same outcome as the previous run would not qualify.
  • Alternatively: "foreign activity resets the streak only if the overall PR health changed (at least one check flipped state)" — re-runs on a known-failing suite are excluded.

Fix this →


Finding 2 — SUGGESTION | confidence: MEDIUM

"What sat unmoved" content written into both stall escalation bodies and the telemetry cycle report creates a cross-session prompt-injection persistence surface

plugins/source-control/skills/babysit-loop/reference/no-progress-detector.md L42–44
plugins/work-items/skills/work-loop/SKILL.md L370–372

Both stall escalations are instructed to report "what sat unmoved in the snapshot" — drawn from PR titles (babysit-loop) and work-item titles (work-loop) in the cycle-start snapshot. These titles are externally controlled: anyone who can open a PR or file an issue against the watched repository controls the string written into the stall escalation body.

The stall escalation is a long-lived artifact (remains open until an operator closes it). The telemetry comment is re-read every cycle to recover durable state. While only the JSON block is machine-parsed, the surrounding prose — including the cycle report section containing these titles — is part of the same conversational context the model processes on restart. A PR or item title containing adversarial instruction text persists across sessions until an operator manually resolves and closes the stall issue, extending the injection surface from one cycle's scope to the full open lifetime of the stall item.

The at-most-one-open dedup (author-matched) correctly prevents a third party from suppressing the signal itself — that control is sound. The concern here is injection persistence, not signal suppression.

Not addressed by any commit in this PR. Status: open.

Mitigation options (unchanged from the prior pass):

  • When writing "what sat unmoved," include only stable identifiers (PR/issue number + head SHA or item number) rather than free-text titles. Link to the items for human readability without embedding their text.
  • Or quote externally-controlled content in a clearly-delimited data block with an explicit header warning the model to treat that section as untrusted data.

Fix this →


No findings in these areas

  • Rate-limit held-cycle logic: correctly resolved; the bar/window distinction is accurate for both lanes.
  • No-self-reset logic: the distinction between a stall escalation (never qualifying progress) and an item-level escalation (qualifying progress for work-loop step 5) is clearly maintained and correctly tested in eval fixtures.
  • At-most-one-open dedup: author-matched title check is sound. A third-party lookalike with a different author does not suppress the lane's own escalation.
  • no_progress_streak counter manipulation: the counter is persisted in the telemetry JSON block. Tampering with it (raising it to trigger a premature escalation, or zeroing it to delay one) is low-impact — it affects timing of a human-notification signal, not autonomy or access.
  • Shell command injection: the inlined telemetry upsert script is unchanged by this PR. No new interpolation surfaces.
  • Secrets / credentials: no new token handling paths.
  • Supply-chain / Actions / unpinned deps: no workflow changes in this PR.
  • Provider neutrality: babysit-loop raises via gh issue create (GitHub-specific lane); work-loop raises via the seam create-item verb (provider-neutral). The asymmetry is correct and intentional.
  • plugin.json min: 1 bound on work_loop_no_progress_threshold: prevents setting the threshold to 0 (which would cause continuous escalation on every cycle). Sound.

@kyle-sexton
kyle-sexton merged commit 0bf8503 into main Jul 29, 2026
32 checks passed
@kyle-sexton
kyle-sexton deleted the feat/1648-no-progress-detector branch July 29, 2026 18:07
kyle-sexton added a commit that referenced this pull request Jul 29, 2026
…only) (#1719)

Phase 1 of #1651: turn each loop lane's spend from a blind spot into a
measured one, by recording a
per-cycle `usage_sample` in the lane telemetry state block that already
exists. Nothing reads the
field back. Phase 2 — a budget config key — is deliberately **not**
designed here, and no threshold,
cap, warning, pause, backoff, pacing change, or merge-rung change
derives from the sampled value.

## The signal, and why it is not a token count

The issue title says "token usage". This ships **window-percentage**
usage instead, because no token
count is readable at a lane's cycle boundary. What each lane actually
records is the reading its
rate-limit guard step already took that cycle:

- **Field:** `rate_limits.five_hour.used_percentage` /
`rate_limits.seven_day.used_percentage`
- **Produced by:** `plugins/rate-limit-guard/scripts/statusline-tee.sh`,
which selects `rate_limits`
  out of the status-line stdin payload and atomically rewrites
  `~/.claude/rate-limit-guard/rate-limits.json`
- **Doc:** <https://code.claude.com/docs/en/statusline> —
`used_percentage` is 0–100, `resets_at` is
Unix epoch seconds, and `rate_limits` appears only for Claude.ai
subscribers after the session's
first API response, with each window independently absent. Re-verified
2026-07-28; no drift.

Because the guard step already holds this value, the invariant costs a
write, not an observation.

**Why not tokens.** `context_window.total_input_tokens` /
`total_output_tokens` are documented as
token counts *currently in the context window, from the most recent API
response* — cumulative
session totals only before Claude Code v2.1.132. The `/usage`, `/goal`,
and `/workflows` figures the
issue cites are interactive displays, not values a skill body can parse
at a cycle step.

**One signal does exist, and is recorded as deferred rather than
denied.** `cost.total_cost_usd` is
documented as the session's client-side accumulated cost, resetting on
`/clear` — machine-readable
and session-scoped, so it *would* attribute to a single lane. The tee
does not forward it. Widening
the tee is a `rate-limit-guard` change (its own bump plus a
reader-contract amendment) and is out of
scope for a measure-only item, so this PR records it as the deferred
candidate with its trigger
rather than claiming no signal exists.

## Caveats, re-derived rather than inherited

The issue's acceptance criterion names "approximate, machine-local".
Both are recorded, but
attributed to what they actually describe:

- **Approximate** was a `/usage` caveat and does not hold for
`used_percentage`, which the
status-line schema documents without qualification. The real bound is
the guard's staleness rule,
so the sample is described as a snapshot no fresher than that rule
allows.
- **Machine-local** describes the *tee file* — last-writer-wins, no
account identifier — not the
  reading, which is account-scope. The two are now stated separately.
- **Disclosed gap:** the tee refreshes only while an interactive session
renders a status line, and
a lane is by construction a `claude --bg` session. An unattended lane
with no interactive window
open samples `null` every cycle. An empty sample therefore means
*unobserved*, never zero. This is
stated rather than glossed, because the framing "the reading is already
in hand" otherwise hides
  it.

## Scope

- Extends the existing `#502` durable-state block in both loop lanes —
no second telemetry
mechanism. `plugins/claude-ops/skills/lanes/scripts/telemetry-upsert.sh`
is body-opaque and
validates no schema, so it needs no change; nothing else in the repo
parses the state block.
- The schema tag stays `@1`: the added key is additive-optional and no
reader validates the shape.
- The attended `attend-queue` lane is unaffected — §4 binds loop lanes,
and that lane holds no
  durable-state block.
- `plugins/autonomy/reference/` is untouched. That tree holds the OTLP
telemetry contract, not the
lane state-block shape; the shape lives in the loop-lane convention and
the two lane bodies.

## Versioning — renumbered as predicted

The competing numbers this PR's table anticipated all landed first, so
every version here was
re-derived against `origin/main` at merge time rather than against this
branch's original merge
base:

| File | Now | Superseded by what landed |
|---|---|---|
| `docs/conventions/loop-lane/CHANGELOG.md` | `6.0.0` | #1690 took
`4.0.0`, #1710 took `5.0.0` |
| `plugins/source-control` | `0.39.0` | #1690 `0.35.0`, #1710 `0.36.0`,
plus `0.37.0` and `0.38.0` landing mid-merge |
| `plugins/work-items` | `0.29.0` | #1690 `0.27.0`, #1710 `0.28.0` |

The loop-lane bump stays **major** on the convention's own discriminator
— §4 states loop-layer
invariants and this adds one every loop-lane body must carry. The
competing minor reading (a field
nothing reads back changes no behavior) is recorded in the entry per the
established pattern. The
convention entry was also moved back to the top of its changelog, where
the merge had left it below
the entries that landed ahead of it.

`§Versioning` re-derivation trigger 2 is satisfied: the status-line
claim was re-verified against its
cited page and the stamp refreshed, recorded as a changelog entry.
`rate-limit-guard`'s reader
contract carries its own 2026-07-23 stamp on the same page; it is
unchanged by this PR and its
refresh belongs to that plugin's own bump.

## Composition with the two features that landed first

Both lane bodies now carry three loop-layer features in one cycle step
and one state block: the
no-progress streak (#1648), the escalation record write (#1650), and
this usage sample. Composing
them pushed `babysit-loop`'s `SKILL.md` past the 500-line hard cap, so
two things were tightened
without changing any contract:

- The usage-sample prose in **both** lane bodies keeps the field
contract (the `at` semantics, every
`null` rule, the delta's roll-over and first-cycle cases) and cites §4
for the single permitted
readback, the preceding-interval delta, and the three bounding
properties — all of which §4
already states in full. §4's own wording changed from "recorded
alongside the sample in each lane
  body" to "held by citation from each lane body" to match.
- `babysit-loop`'s telemetry upsert mechanics — the singleton lookup and
the creation-race reconcile
— moved to `reference/telemetry-upsert.md`, beside the
`pre-escalation-dispatch.md` and
`no-progress-detector.md` spokes already there. Still inlined in this
plugin rather than invoked
  from `claude-ops`, for the same reason as before.

## Verification

Gates green after the merge: `check-changed-skills.sh` (babysit-loop
486/500 and work-loop 451/500
against the hard 500-line cap, both re-measured with all three features
composed),
`check-changelog-parity.sh --check` and `--check-bump origin/main`, and
markdownlint over every
changed markdown file. No `evals.json` is touched.

A fresh-context verifier audited the diff with the rationale withheld
and traced the tee's producer
and the live status-line doc independently. Its findings drove the
cumulative-cost correction, the
caveat re-attribution, the background-session disclosure, and the
removal of a cited precedent that
did not exist. Its remaining version-collision finding is the table
above.

## Related

- #1690 (#1650) — merged; took loop-lane `4.0.0` and both plugin bumps,
and writes the escalation
  record into the same cycle step this PR extends
- #1710 (#1648) — merged; took loop-lane `5.0.0` and both plugin bumps,
and adds the no-progress
  streak to the same state block and cycle step
- #1712 (#1655/#1656/#1658) — took loop-lane `3.1.0`
- #1711 (#1649) — stages a `work-items` bump; still open
- #1707 (#1652/#1654) — concurrent lane work, no file overlap
- Phase 2 (`*_session_token_budget`) stays deferred until this sampling
produces data, per the
  issue's own staging.

Closes #1651

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Add per-lane consecutive-no-progress detector with escalation

1 participant