feat(claude-ops): consume lane restart-requests via scheduled headless reader - #1720
Conversation
Deterministic reader of each configured lane's telemetry restart_request that relaunches stopped lanes through lane-launcher.sh restart, with a per-lane circuit breaker, a JSONL run ledger, its own morning-brief telemetry comment, and print-schedule emitting the operator-run OS scheduler registration (Task Scheduler / cron) with removal commands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRSgwJ7RN4vPZt4oJAADHy
…context doc valid_repo_slug's character class admits `.`, so `../evil` passed as an owner/name slug and reached the gh API path — test 49 caught it; reject dot-only segments explicitly. Adds the operator/reviewer-facing context/restart-consumer.md (design rationale, OS-scheduler registration with Verify/Reversal, labeled UNVERIFIED items, observability contract). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134vdV1jXJhzpSmeT7y5B8N
Route the consume-restarts action through SKILL.md to restart-consumer.sh (which now strips the leading skill-level action token so $ARGUMENTS forwards verbatim, matching the lane-launcher idiom), surface it in the plugin manifest description and README, and record the 0.22.0 release note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134vdV1jXJhzpSmeT7y5B8N
…relaunch Independent verification found the documented schedule inert: the headless command omitted the `run` sub-action, so a registered task ran the read-only default `check` forever and never relaunched a lane or refreshed the consumer's telemetry. The /TR payload also embedded MSYS-form paths cmd.exe cannot execute, and the backslash-continued schtasks lines were pasteable into no Windows shell. - carry `consume-restarts run` in every scheduled command (schtasks, cron, and the documented headless form) - emit Windows-form /TR paths via cygpath -w (which also restores the real claude.exe behind the extensionless shim) and single-line schtasks commands, with an explicit run-from-cmd.exe note (Git Bash MSYS path conversion mangles /-style options) - default the consumer's own telemetry comment onto the issue morning-brief.sh itself resolves (reusing that reader's title search) so the appears-in-the-brief claim holds without pinning; exact-title fallback and loud ledger-only degradation kept - note the --data-dir ledger-split hazard on the offline variant 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 1s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e27ec7cb7
ℹ️ 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".
… closed The emitted registration is TWO scheduled tasks — a 15-minute poll and an ONLOGON companion — and at logon both fire. Task Scheduler's instance policy is per task, so it cannot serialize them; lane-launcher.sh's running-lane guard is same-process; and a just-launched background session does not appear in `claude agents --json` instantly. Unsynchronized, both runs read the same breaker count, both relaunch, and one lane name ends up with two `claude --bg` sessions plus two `restarted` rows burning the budget for one effective restart. A `run` now holds an mkdir-atomic sentinel across the whole read -> decide -> relaunch -> append span, released on an EXIT trap — the idiom the observability prune's `.prune-in-progress` established. A run that cannot take it skips cleanly: exit 0, a `lock-held` flag, nothing launched and nothing written. A lock left by a hard-killed run (no EXIT trap) ages out after an hour, so an unattended schedule cannot wedge permanently; a lock with no stamp yet is dated rather than stolen, because its holder may have won the mkdir microseconds ago. The circuit breaker now fails CLOSED. `jq -s ... || printf '0'` meant one torn line silently restored the full restart budget — on exactly the ledger an unsynchronized or crashed writer leaves behind, turning corruption into an unbounded restart loop. An unparseable ledger reports the budget as spent, with a warning naming the file. Two more honesty fixes in the same surface: - A failed telemetry READ was swallowed into `[]` and surfaced as `no-state`, so a transient `gh` blip was indistinguishable from "the lane did not ask". It is now its own `api-error` decision, flagged and exit 5. - `append_ledger` was gated only on `--dry-run`, so a `check` wrote to disk despite the `--help` contract documenting it read-only. It is gated on the action being `run`, and only INCIDENT decisions are ledgered: the routine per-tick ones would add hundreds of rows a day, forever, to the file the breaker re-reads once per lane per tick, and none can change its verdict. Filter-on-write, not prune-on-write — the breaker's memory should not depend on a whole-file rewrite going right. `print-schedule` now substitutes a passed --data-dir into the offline form, which previously emitted a placeholder while warning that a split data dir would widen the restart budget. Tests: 39 new cases (53 -> 92), including the lock's concurrency behavior driven through the launcher stub, and argv-recording `gh` coverage for `upsert_own_telemetry` — pinned issue, search hit, exact-title fallback, and no issue found — which every prior case suppressed with --no-telemetry. A grep gate asserts the morning-brief discovery literals stay byte-identical in both files. 19 of the new cases fail against the pre-fix script. Refs #1653 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134vdV1jXJhzpSmeT7y5B8N
… exists to catch The documented Verify step told the operator to confirm "a fresh `last-cycle:` on the consumer's telemetry comment (or a fresh row in the local run ledger)". The disjunction's second branch was satisfied by the exact failure the step exists to catch: `upsert_own_telemetry` returns early unless the action is `run`, but `append_ledger` was gated only on `--dry-run` — so a `check`-only schedule, the misregistration commit 2e27ec7 fixed, produced no fresh `last-cycle:` and a fresh ledger row anyway, and passed. Both halves are fixed. The ledger alternative is dropped from the step: it answers "did something run", where the step must answer "did a `run` run", and `last-cycle:` is the only signal that carries that. The read-only contradiction is resolved in favour of the `--help` contract — `append_ledger` is gated on the action being `run` — because a `check` an operator runs by hand must never move the circuit breaker's memory. Also documents the cross-process lock and why it is load-bearing rather than defensive (the registration is two tasks that both fire at logon), the fail-closed breaker, the incidents-only ledger, and `api-error` as distinct from `no-state`. CATALOG regenerated for the lanes skill summary. Refs #1653 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 4s —— View job PR Review
|
|
Claude finished @kyle-sexton's task in 5m 14s —— View job Security Review — PR #1720Reviewed Core trust-boundary analysisThe PR's central security claim — "THE COMMENT IS A SIGNAL, NEVER A TARGET" — is accurate and well-enforced. Telemetry comment content is parsed exclusively through The cross-process lock is sound: Findings[IMPORTANT | CONFIRMED] Operator's home-directory path posted verbatim to GitHub body+="
Run ledger on this machine: \`$(ledger_path)\`"
The context doc already flags the ledger path as machine-local. The fix is to emit a generic label (e.g., [IMPORTANT | CONFIRMED] Circuit breaker does not bound failed relaunch attempts jq -s -r --arg l "$lane" --argjson c "$cutoff" \
'[ .[] | select(.lane == $l and .decision == "restarted" and (.epoch // 0) >= $c) ] | length' \
"$ledger" …
Consequence: when a lane's The PR itself acknowledges the Windows uncertainty ("UNVERIFIED") and says the consumer "degrades safely … records a loud An operator whose Windows task-object assumption does not hold, or a Non-findings worth recording
|
Merge: main landed claude-ops 0.22.1, so this branch's entry moves to 0.23.0 in both the changelog and the manifest, keeping the description that documents the new consume-restarts action. Fixes: - gh repo view takes an [<owner>/]<repo> argument and parses a leading path segment as a HOST, so passing the checkout path made the default (no --target-repo) path exit 4 before reading any request. Verified empirically: `gh repo view <path>` reports "error connecting to d" for a D: checkout, while the same command run in the directory resolves the slug. The repo is now selected by the working directory. - resolve_issue_by_title piped gh issue list into jq, so an unreachable forge read as "no issue carries this title" and an unattended consumer stayed apparently healthy while never observing that lane's request. It now returns non-zero on the list failure and the caller records api-error, matching what lane_comment_bodies already did for the comment read. - The circuit breaker counted only `restarted`, leaving it permanently closed on the failure it exists for: a launcher that fails, or that returns success while the lane never appears, retried every tick forever. It now counts relaunch attempts (`restarted` and `failed`); the pre-launch `error`/`api-error` reads stay uncounted so a transient forge outage never spends a lane's budget. - The published telemetry comment no longer carries the absolute ledger path, which embeds the operator's home-directory user name by default and can carry internal host names under --data-dir. Tests: the breaker case that asserted the old counting rule now asserts the new one, with added cases proving a `failed` row spends budget and `error`/`api-error` rows do not. The argument-parsing case passes --max-restarts 0 to isolate it from the shared ledger, which earlier sections leave `failed` rows in. 95 cases, 0 failures; shellcheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 3m 2s —— View job PR Review
The merge commit ( All four findings confirmed fixedFinding 1 — # was: gh repo view "$REPO" --json nameWithOwner -q .nameWithOwner
TARGET_REPO="$(cd "$REPO" && gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)"The fix correctly selects the repo by working directory rather than passing Finding 2 (Codex P1) — resolve_issue_by_title() {
local repo="$1" raw
raw="$(gh issue list --repo "$repo" ... 2>/dev/null)" || return 1
jq -r ... <<<"$raw" 2>/dev/null || return 1
}Returns 1 — never empty — on Finding 3 — circuit breaker counted only jq -s -r --arg l "$lane" --argjson c "$cutoff" \
'[ .[] | select(.lane == $l and (.decision == "restarted" or .decision == "failed") and ...) ] | length' \The fix counts relaunch attempts ( Two new test cases verify the new behavior: a Finding 4 — machine-local ledger path (including username) posted to GitHub ✅ Fixed # was: Run ledger on this machine: `$(ledger_path)`
body+="
Run ledger: \`<data-dir>/$(ledger_relpath)\` on the machine running this consumer."
Test coverageThe suite grew from 53 to 95 cases. The additions directly target the fixed paths:
No remaining findingsThe implementation is ready to merge. |
|
Claude finished @kyle-sexton's task in 4m 36s —— View job Security Review — PR #1720
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f03b81fb66
ℹ️ 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".
…hangelog order (#1758) ## Why The per-cycle usage-sample invariant (loop-lane 6.0.0, shipped in #1719) is self-contradictory as written. §4 permits reading the previous sample back **"for exactly one operation: subtracting its `five_hour_pct`"**, then states **"No other read is permitted"** — but the same invariant withholds a delta when the window rolled over, and deciding that requires a *second* comparison against that previous reading. No lane can satisfy both clauses. This is not theoretical. Both `babysit-loop` and `work-loop` were rewritten to hold the readback contract **by citation** ("Everything else — the single permitted readback … — is the convention's") while independently mandating the rollover comparison. The contradiction therefore has exactly one authoritative site, and every consuming lane inherits it. `chatgpt-codex-connector` raised this on #1719. The fix was drafted but never committed — the authoring agent was interrupted mid-edit and #1719 merged without it. ## What changed - **The permission is scoped by *purpose*, not by *operation*.** It now covers deriving `five_hour_delta_pct` — the subtraction **and** the rollover comparison — as one derivation. **The measure-only guarantee is unchanged**: the value still reaches no decision, at any threshold, in a lane or in any gate a lane runs. - **`at` disambiguated.** It is when the lane read the tee, not the snapshot's own `captured_at`, which the staleness rule permits to lag it. - **The delta's `null` condition widened.** "Either sample is missing" excluded a present sample carrying a `null` `five_hour_pct`; it is now `null` whenever either side's `five_hour_pct` is unavailable. ### Changelog version regression (separate defect, same file) `docs/conventions/loop-lane/CHANGELOG.md` on `main` read `6.0.0 → 3.1.1 → 5.0.0 → 4.0.0 → 3.1.0`. The `#1638` entry was authored against `3.1.0` and merged (#1686, 17:46:59Z) after `4.0.0` had already landed (17:44:23Z) — a stale-branch renumber miss, in a file with no CI gate for version order. Renumbered **`4.0.1`** and repositioned below `5.0.0`, which preserves both descending version order and the order entries actually shipped in. **Its wording is unchanged.** Verified by script — all three touched changelogs are now strictly descending with no duplicates. ## Deliberately not done The `source-control` `0.39.0` and `work-items` `0.29.0` entries describe the field as *"deliberately inert: no lane behavior reads it back"*, which the shipped contract contradicts. Those versions have already been published, so they are **left as shipped** and superseded by the new `0.40.2` / `0.30.2` entries rather than rewritten in place. ## Verification - `node scripts/validate-plugin-contracts.mjs` — 43 setup skills, 2150 files, pass - `bash scripts/check-changed-skills.sh origin/main` — 2 skills, 0 failures (`babysit-loop` 495/500, `work-loop` 434/500) - `npx markdownlint-cli2` over all 6 changed markdown files — 0 errors. Run standalone because `check-changed-skills.sh:67` sets `CHECK_SKILL_SKIP_MARKDOWNLINT=1` by design (documented at line 19; markdown is gated by the hygiene lane). - Changelog ordering verified by script against `sort -rV`. ## Related No linked issue — this corrects defects in already-merged work; both originating issues are closed. Refs #1651 (the usage-sample invariant this corrects, shipped via #1719) Refs #1638 (the changelog entry renumbered here, shipped via #1686) Refs #1720 (its post-merge review findings are tracked separately, not in this PR) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… merged (#1760) ## Why `chatgpt-codex-connector` posted six findings on #1720 at `19:24:03Z` — **46 seconds after it merged** at `19:23:17Z`. No ruleset gate could hold them (the threads did not exist at merge time), and nothing surfaces open threads on a merged PR, so they were never triaged. All six are real, all are in `lanes/scripts/restart-consumer.sh`, and none was addressed on `main`. ## What changed **Two that could silently disable an unattended consumer** - **A broken lock store read as a held lock.** An ignored `mkdir` failure fell through to the contention branch: the absent stamp read as zero and the run reported `lock-held` with exit 0. A mistyped data dir, a permissions problem, or an unavailable volume meant **Task Scheduler recorded healthy ticks forever while no lane was ever processed**. `acquire_lock` now returns a third state — store unusable (exit 4, loud) vs. race lost (exit 0, routine). - **An unwritable ledger was a warning.** The breaker counts attempts by querying that ledger, so an unrecordable attempt was invisible to `--max-restarts` and a failing launcher was retried on every tick forever. Writability is proved *before* the relaunch, and a failed append fails the lane. **Two concurrency races** - **A lock reclaimed on age alone.** A legitimate run outliving the one-hour bound had its live lock removed, letting a second run enter the relaunch span — reachable because `lane-launcher.sh` does an unbounded `git pull --ff-only` and marketplace update before launch. The holder now records a PID; age only decides *when to ask*, liveness decides the outcome. - **The relaunch predicate read a stale snapshot.** The session list loads once per run, so a lane started since by a concurrent operator invocation still read as stopped — and `lane-launcher.sh restart` **stops** a running lane before relaunching. A healthy session could be interrupted despite the documented "not currently running" predicate. Now rechecked against a fresh list immediately before mutating. **Two correctness gaps** - **Offline telemetry parse failures were swallowed** by an unconditional `return 0`, so an unreadable or malformed `--telemetry-json` reported `no-state` — indistinguishable from "the lane did not ask". The offline branch now carries the contract the network read already had. - **`print-schedule` dropped behavior-affecting options.** A non-default `--config`/`--target-repo` was missing from all four emitted forms (schtasks, logon, cron, offline), so the registered task would silently drive a different lane configuration and a different telemetry repository than the command that generated it. ## Also fixed while here Bash reports a failed redirection *itself*, before the command runs, so `2>/dev/null` on `printf` never suppressed it — `Is a directory` was leaking into the operator's report. The redirections now run in a subshell. ## Verification - `restart-consumer.test.sh`: **95 → 126 cases, 0 failures.** Every finding gets a regression case, each paired with a negative case so the fix cannot over-correct — a *dead* owner's lock is still reclaimed, a *defaulted* option is still omitted, and the report must not contain the leaked redirection error. - `shellcheck -x` on both scripts — clean. - `node scripts/validate-plugin-contracts.mjs` — 43 setup skills, 2150 files, pass. - `npx markdownlint-cli2` on all three changed markdown files — 0 errors. ## Docs `context/restart-consumer.md` claimed a lock "ages out after an hour", which is no longer the whole rule — updated to state that age never reclaims on its own. `SKILL.md`'s `argument-hint` gained `--target-repo`, which `print-schedule` now emits and the hint omitted. ## Related Closes #1759 Refs #1720 Refs #1653 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uests (#1781) ## Why A review that lands **after** a merge has nowhere to go: - the ruleset's `required_review_thread_resolution` is a **merge-time predicate** that already passed; - the babysit lane works *open* PRs, and a merged PR leaves that queue; - nothing on a merged PR surfaces its open threads — GitHub shows the merge, not the findings. Six findings — one **P1** — posted **46 seconds after #1720 merged** and sat unread for a day. They surfaced only because a later session happened to audit that merge batch. Nothing was bypassed; the gate was satisfied *because the threads did not yet exist*. The morning brief is the right home: read-only, unattended, and already where attention signals land. ## What it does Compares each unresolved thread's **first-comment timestamp** against the PR's `mergedAt`, and reports only threads the gate could never have seen. A thread that predates the merge was visible to the gate — that is an ordinary unresolved thread, not this failure mode, and it stays out. - **One line per PR, at that PR's worst severity, with a finding count.** Several findings on one PR are one thing to go look at; repeating the title per thread buries every other PR. Collapsing on the *worst* severity means a P0 sitting beside advisory findings can never be softened. - **Severity survives to the operator** — a stranded P1 must not read like a P3. - **`--stranded-days`** (default 3) — wide enough to cover slow bot review *and* an operator-absent weekend. ## It fails loud, not clear A GraphQL error document is well-formed JSON that simply carries no `data`. The extraction would yield an empty list and render **"every merged PR in the window is clear"** — an all-clear asserted from an answer never received, which is the same fail-open shape this section exists to catch. This is not hypothetical: a rate-limit error did exactly that during development. An API error now says explicitly that it is *not* an all-clear, and prints the message. Covered by a regression case. ## This is a standing leak, not a one-off Its **first live run** against this repository immediately surfaced four more stranded findings on other merged PRs — including a **P1 on #1694** (merged `05:04:45Z`, finding posted `05:05:20Z`, 35 seconds later) recording that a shipped `autonomy` cell **never reached installations**. ## Verification - `morning-brief.test.sh`: **30 → 63 cases, 0 failures.** - The **negative** cases carry the weight — a pre-merge thread, an already-resolved post-merge thread, and a merge outside the window must all stay silent, or the section is noise rather than signal. Plus: collapse-does-not-soften-severity, highest-severity-first, window-widening, and the API-error case above. - The fixture mirrors the real #1720 shape, including the 46-second gap. - `shellcheck -x` on script and test — clean. One `SC2016` is declared, not blanket-suppressed: the `$owner`/`$name`/`$endCursor` in the GraphQL query are server-side variables bound by `-F` and **must** reach the server unexpanded. - `node scripts/validate-plugin-contracts.mjs` — 43 setup skills, 2153 files, pass. - `npx markdownlint-cli2` on both changed markdown files — 0 errors. ### Live run — posted in full in the comments below A live run on the current branch found **44 merged PRs carrying post-merge findings in a five-day window: 0 P0, 10 P1, 34 P2.** Among the P1s: **#1503**, a guardrail-bypass fix whose own review landed unread, and **#1322** with 5 findings. Read the **second** comment for the authoritative figures — the first was produced by the pre-review severity logic and reported a false P0, which review then caught. No truncation warning fired, so the read is complete. The five-day window filter was spot-checked against `mergedAt` (a PR numbered #969 in a 5-day window looks wrong until you check: it merged `2026-07-25`, 4.2 days before the run). This is a far larger leak than the six findings that exposed it. ## Related Closes #1777 Refs #1720 Refs #1759 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Summary
A loop lane that hits its per-session cycle budget or the
/loopseven-day expiry writes arestart_requestinto its telemetry state block and stops cleanly — a running loop cannot relaunch itself, and nothing consumed that field, so every budget or expiry hit was a terminal manual-restart state. This PR ships the consumer: a deterministic bash reader (plugins/claude-ops/skills/lanes/scripts/restart-consumer.sh, newconsume-restartsaction on/claude-ops:lanes) that reads each configured lane's telemetry and relaunches the stopped lanes that asked, meant to run unattended on an OS-owned schedule (Task Scheduler / cron).Design record (three-option bake-off, doc citations, verified CLI surface): the decision comment on #1653. In short: a watchdog/wake lane is circular (itself a
/loopbound by the same expiry and budget); a Stop hook cannot fire when the failed thing is the process or the machine (deferred with a trigger, as a latency layer only); only an OS scheduler survives a clean stop, a crash, AND a reboot. Cloud/scheduleroutines stay rejected (no access to local checkouts) and were not re-litigated.Acceptance criteria
restart_requestrelaunches the lane without manual intervention — relaunch predicate: lane named in the operator's local config AND state block parses with non-nullrestart_requestAND lane not currently running (claude agents --json, name +kind: background) AND circuit breaker has room (default 3 restarts / rolling 24 h). The not-running condition makes the predicate self-clearing without editing another writer's telemetry comment. The telemetry comment is a signal, never a target: nothing read from a comment is interpolated into a command, path, or repo.morning-brief.shresolves (that reader's own title search, reused), so a schedule that stops firing surfaces as a STALE lane in the morning brief; honest exit codes (a failed/unconfirmed relaunch or tripped breaker exits 5, visible in Task Scheduler history).lane-launcher.sh restart <lane>, which carries the lane's prompt,--model,--effort, and--settingsfrom the same config.claude respawnis deliberately not used: it resumes the conversation intact, and a restart-request exists precisely because a fresh context is the only reset a lane gets.Operator registration (not performed by this PR)
print-scheduleemits the exactschtasks /Create(poll + ONLOGON cold-start,/RU "%USERNAME%" /IT /RL LIMITED— no elevation, no stored password) andschtasks /Deletereversal lines with Windows-form paths, plus cron/launchd/systemd-user equivalents and a zero-model-cost offline form. Verify and Reversal lines follow theClaudeCodeOtelPruneprecedent, including its Windows-first posture (the non-Windows lines are equivalents, not a parity claim). No scheduled task is registered by this change.Explicitly UNVERIFIED (labeled in
context/restart-consumer.md)/RU <user> /NP): nothing was registered on the authoring machine; documented coverage is "logged on or locked".claude --bglane launched from a scheduler-spawned process outlives that process on Windows (job-object process-tree kill): the consumer degrades safely instead of assuming — it re-pollsclaude agents --jsonafter each relaunch and records a loudfailedrow (exit 5) if the lane never appears.Not lane pacing
The consumer's polling tick is NOT lane cadence: lanes stay self-paced via
ScheduleWakeup, and a tick where no configured lane has a non-nullrestart_requestis a no-op that only refreshes the consumer's own freshness telemetry. Stated explicitly in SKILL.md and the context doc.Verification
restart-consumer.test.sh), offline via--telemetry-json/--agents-json/--nowinjection; all green. Shellcheck clean on the script and the suite.check-changed-skills.sh origin/main,check-changelog-parity.sh --checkand--check-bump origin/main,check-shell-portability.sh,check-silent-skips.sh, markdownlint.runsub-action, MSYS-form/TRpaths unusable from cmd.exe, morning-brief discovery mismatch) fixed and re-verified READY.Four review findings fixed on the branch
Each was checked against the current code rather than the thread's outdated flag; two of the four were still live.
gh repo viewtakes an[<owner>/]<repo>argument and parses a leading path segment as a HOST, so passing the absolute checkout path made the default (no--target-repo) path — the one every generated scheduled command uses — exit 4 before reading any request. Verified empirically on thisD:checkout: the positional-path form reportserror connecting to d, while the same command run in the directory resolves the slug. The repo now resolves from the working directory.api-error, butresolve_issue_by_titlepipedgh issue listintojq, so an unreachable forge became empty output and recordedno-telemetrywith no failure flag — an unattended consumer looked healthy while never observing that lane's request. It now returns non-zero and the caller recordsapi-error, matching the comment-read path.restarted, while a launcher that exits non-zero or returns success without the lane appearing writesfailed. Every tick re-attempted a pull, a marketplace refresh, and a launch indefinitely. It now counts relaunch attempts;errorandapi-errorstay uncounted so a transient forge outage never spends a lane's budget.--data-dir. It now names the file relative to whatever data dir the reader's own machine resolves.Related
claude-opsbumped to 0.23.0 with a CHANGELOG entry. The renumber the original body anticipated did happen:0.22.1landed onmainwhile this branch was in review, and this entry composes above it.restart_requestis untouched by all three), so there is no file overlap with this PR.Closes #1653
🤖 Generated with Claude Code