feat(loop-lane): add per-lane consecutive-no-progress detector with escalation - #1710
Conversation
…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 finished @kyle-sexton's task in 2m 53s —— View job PR Review
OverviewThis PR adds a consecutive-no-progress detector with stall escalation to both loop lanes ( Verdict: LGTM with two low-severity observations. What's correctConvention 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:
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.
Provider-neutrality is preserved. The babysit-loop stall issue is created with 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. The Observations (low severity)1. Schema version strings are not bumped Both state blocks remain {"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 2. The description reads:
The Checklist
|
|
Claude finished @kyle-sexton's task in 5m 8s —— View job Security Review — PR #1710
ScopeAll 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: HIGHStall detector disabled by CI check transitions — perpetually-stuck PRs are the primary use case it fails to catch
The babysit-loop lane's qualifying-progress definition includes "a checks transition" as foreign activity that resets
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: The work-loop lane ( 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
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., 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
|
There was a problem hiding this comment.
💡 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".
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 encountered an error after 1s —— View job I'll analyze this and get back to you. |
|
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 encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude finished @kyle-sexton's task in 4m 5s —— View job PR Review (re-review after new commits)
What triggered this re-reviewThree commits landed since the July 28 reviews:
Both fix commits directly respond to the Codex inline finding ( Codex finding — addressed correctlyThe fix is correct and the two-step refinement is important.
The asymmetry between the two lanes is correct and intentional:
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 — statusSecurity 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 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 Observation 2 — Still open. The VerdictThe 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 finished @kyle-sexton's task in 2m 49s —— View job Security Review — PR #1710
ScopeAll 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 Rate-limit guard held-cycle fix — RESOLVEDThe 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
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: HIGHRoutine CI activity on a perpetually-failing PR continuously resets the stall streak, so the babysit-loop detector is neutralized on any actively-tested repository
The babysit-loop's qualifying-progress definition includes "a checks transition" as foreign activity that resets
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: 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:
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
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:
No findings in these areas
|
…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>
… 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>
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 finished @kyle-sexton's task in 5m 11s —— View job PR Re-review (post-synchronize, HEAD 31e9b29)
What triggered this re-reviewTwo merge commits landed since the July 29 re-review:
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
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 integration defects found.
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 statusSecurity Finding 1 — CI check transitions neutralizing the babysit-loop detector Still open.
On any repository with active CI, a perpetually-failing PR produces continuous check lifecycle events ( 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 Observation 1 — Schema version strings not bumped Still open. Both state blocks remain Observation 2 — Still open. The
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. VerdictThe 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 finished @kyle-sexton's task in 3m 4s —— View job Security Review — PR #1710
ScopeAll 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 Rate-limit held-cycle fix — still RESOLVEDCommits Finding 1 — IMPORTANT | confidence: HIGHRoutine CI activity on a perpetually-failing PR continuously resets the stall streak — the babysit-loop detector is neutralized on any actively-tested repository
The merge lane's qualifying-progress definition lists "a checks transition" as foreign activity that resets
On any repository with active CI, a perpetually-failing PR generates a continuous stream of check lifecycle events: 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):
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
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):
No findings in these areas
|
…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>
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 healthysnapshot 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 thework-itemsREADME;babysit_loop_no_progress_thresholdrides the layeredsource-control.mdseam.Version bumps: loop-lane convention 5.0.0;
source-control0.36.0;work-items0.28.0 — eachcomposed above what
mainpublished 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_changeddelta → worker dispatched → push attempt B withCI 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
--checkand--check-bumppass, skill gate 0failed, markdownlint 0 errors, and both
evals.jsonvalidate againstplugins/skill-quality/reference/evals.schema.json.Related
mainas convention 4.0.0 /source-control0.35.0 /work-items0.27.0. Both features write into the same two lane bodies;this branch composes above it, and
babysit-loop'sSKILL.mdcrossed the 500-line hard cap onceboth landed, so this lane's detector binding moved to a
reference/no-progress-detector.mdspokebeside the existing
pre-escalation-dispatch.mdone. No contract changed in that move.flight on their own branches.
Closes #1648
🤖 Generated with Claude Code