fix(loop-lane): gate and verify the inlined telemetry upsert's body - #1850
Conversation
…ites Every lane that inlines the `gh api` telemetry upsert -- babysit-loop, work-loop, attend-queue -- writes `$BODY_FILE` whole, with no assertion that what it composed is usable. `claude-ops`'s `telemetry-upsert.sh` refuses a degraded body before it writes; an installed plugin cannot invoke a sibling plugin's script, so the three inlined copies inherited none of that. Each block now opens with a pre-write gate: a `$BODY_FILE` that is empty, begins with a literal `@`, falls under the sentinel-plus-16-byte floor, or is not sentinel-prefixed is refused with a visible notice and the cycle skips the upsert fail-closed -- no POST, no PATCH. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sent An independent review found the pre-write gate structurally blind to the failure #943 actually reports. The observed defect is a body-VALUE flag (`-f body=@FILE` instead of `-F body=@FILE`): the composed file is correct and the gate passes it, then `gh` transmits the literal path. Checking the file can never catch that. Each block now also re-reads the comment after the write and reports the cycle UNREPORTED unless the persisted body still opens with the sentinel and clears the payload floor. Three corrections in the same pass: - The byte floor was measured over the whole file; the wrapper's MIN_BODY_BYTES=16 is measured on the payload BELOW the sentinel. The two now agree exactly. - The `$BODY_FILE` sentinel-first-line contract was implicit in a trailing comment. A body composed without it is not merely rejected -- the lookup would never find it again and the next cycle would post a second comment. Stated in prose. - The prose and CHANGELOGs claimed `morning-brief`'s freshness check is what a degraded body deceives. Verified against its `print_telemetry`: it parses `lane:`/`last-cycle:` out of the BODY, so a degraded comment makes the lane vanish from the report rather than look healthy. The deceived check is a timestamp-based delivery check reading `updatedAt`. Attributed correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream released claude-ops 0.24.3 and source-control 0.42.3 after this branch bumped to 0.24.2 and 0.42.2, so both entries collided at the top of their CHANGELOGs. The branch's entries are renumbered to 0.24.4 and 0.42.4 above the upstream ones; work-items 0.30.4 already led main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 5m 58s —— View job Claude finished reviewingTodo list:
SummaryThe One correctness/completeness gap worth addressing: "Record it in durable state" has no addressable target in two of the three lanes.
The PR's own rationale for this instruction is that "stderr does not survive the session," i.e., the whole point is to make an UNREPORTED cycle visible to the next cycle through something more durable than a log line. As written, an agent following this prose has no schema field to write that fact into, so the guidance is currently a well-intentioned no-op rather than a mechanism — which undercuts the observability goal this PR exists to close. Since this is a prose-only PR, the fix is likely either (a) add an Everything else — the pre-write gate's fail-closed control flow, the |
|
Claude finished @kyle-sexton's task in 5m 20s —— View job Security Review
This PR is prose/documentation only (three skill/reference bodies with inlined I reviewed the three near-identical inlined telemetry-upsert shell blocks ( One logic-flaw finding on the newly-added IMPORTANT / PLAUSIBLE — In all three inlined blocks (e.g. gh api -X PATCH "repos/$REPO/issues/comments/$CANON" -F body=@"$BODY_FILE"
VERIFY "$CANON" ||
echo "telemetry: comment $CANON does NOT carry a well-formed telemetry body after the write ..." >&2The This is exactly the failure class the PR's own description calls out (Refs #502, "the observability fail-open this defect instantiates") and the PR's prose oversells Suggested fix: check the No other security issues found — no injection surfaces, no secret/token mishandling, and no authorization gaps introduced by this diff's changed content. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b805dc8011
ℹ️ 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".
|
Claude has reviewed this PR 5 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
…h silently The post-write read-back covered the PATCH path only. On the first-ever upsert the POST runs, the block re-lists, and everything downstream -- including VERIFY -- sits behind `if [ -n "$CANON" ]`. A POST that lands degraded carries no sentinel, so the re-lookup returns nothing, `$CANON` is empty, and the whole branch is skipped: no PATCH, no verification, no notice. A degraded PATCH reported UNREPORTED while a degraded create said nothing at all. The same asymmetry swallowed a failed re-list, which `|| LIST=""` already tolerated on the way to the same empty `$CANON`. Both now take the else branch and report the cycle UNREPORTED, so every path that ends without a verified telemetry comment is visible to the next cycle. The prose in all three inlined blocks, the loop-lane convention, and the two CHANGELOG entries state the create path explicitly rather than leaving "re-reads the comment after the write" to imply a coverage the block did not have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-ops released 0.24.4 upstream while this branch held the same number, so the branch's entry moves to 0.24.5 above it. source-control 0.42.4 and work-items 0.30.4 still lead main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
source-control released 0.43.0 upstream, so the branch's entry moves from 0.42.4 to 0.43.1 above it. claude-ops 0.24.5 and work-items 0.30.4 still lead main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 16s —— View job Security Review
This PR remains prose/documentation only (three inlined Since the last review ( IMPORTANT / PLAUSIBLE — the PATCH write's own exit status is still never checked; gh api -X PATCH "repos/$REPO/issues/comments/$CANON" -F body=@"$BODY_FILE"
VERIFY "$CANON" ||
echo "telemetry: comment $CANON does NOT carry a well-formed telemetry body after the write ..." >&2If the No other issues found in the delta or the unchanged portions: |
|
Claude finished @kyle-sexton's task in 3m 40s —— View job Code Review
Per this repo's 🔴 Important — unresolved merge-conflict markers committed into two CHANGELOGs
I traced this to merge commit This survived every stated verification gate because none of them parse changelog body content: 🟡 Suggestion — "record it in durable state" still has no schema field to write intoRestating and sharpening a gap flagged in an earlier review pass on this PR, which remains unaddressed as of Beyond just missing a field, there's a circularity worth calling out: the skill states the telemetry comment itself is the sole durable-state store ("the comment, not the conversation, is the source of truth for these counters," line 151-152). If the write to that comment is exactly what just failed Since this is prose-only, either add the field (and describe writing it on the next successful cycle, since this cycle's write is what failed) or soften the instruction to something achievable today, e.g. surfacing it in the next cycle's human-readable report line instead of a structured field. Everything else checks out: the |
…ng the read-back
An independent review found the read-back could not carry the guarantee its
own prose claimed. Three defects, all in the same seam:
- The PATCH's exit status was never checked. A write that failed -- 403,
422, secondary rate limit, dropped connection -- fell through to VERIFY,
which re-read the comment, found the PREVIOUS cycle's well-formed body,
and returned success. The most common write failure reported a verified
cycle over a stale comment. The wrapper exits 5 there before its own
read-back runs; the inline copies dropped that and kept the read-back.
- The duplicate-supersede pass ran even when VERIFY had already failed, so
a cycle whose own write was unproven still tombstoned every other
sentinel comment. If a racing session held the only good telemetry, the
cycle destroyed it and left nothing to recover from.
- The byte floor was `${#SENT} + 17` over the whole body, which assumes
exactly one separator byte. A CRLF body puts two there, so the effective
payload floor sat one byte below the wrapper's -- while the CHANGELOG
claimed it matched `MIN_BODY_BYTES` "exactly". The floor is now measured
on everything below line 1 and compared against 16 directly, which agrees
byte-for-byte on LF and CRLF alike and drops the magic number.
Prose corrected to what the block actually does. It no longer justifies the
read-back with `-f body=@FILE`, an invocation the block never emits; it now
names what the read-back does catch (a write that reported success and
stored something else) and states the limits it inherits rather than fixes:
a PATCH that succeeds while storing the previous body still verifies, the
read-back proves SOME well-formed telemetry is present rather than THIS
cycle's, and every inline branch exits 0 and reports through stderr alone.
`morning-brief` was correctly exonerated, but the replacement named "the
tower's timestamp-based delivery check", which exists nowhere in this repo.
Attributed to any consumer keying on the comment's timestamp instead of its
body -- which is what is actually true.
Also removes two stray `|||||||` diff3 markers left in the claude-ops and
source-control CHANGELOGs by the earlier merges; no gate catches those.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#1841 keys the telemetry marker per lane instance and adds a lane_instance validation gate ahead of it, in the same three inlined blocks this branch rewrites. Both sides kept: main's INSTANCE validation and instance-qualified MARKER, this branch's sentinel contract, payload floor, write-status check, read-back, and gated supersede pass. main also released claude-ops 0.25.0, source-control 0.44.0, and work-items 0.31.0, so this branch's entries move to 0.25.1, 0.44.1, and 0.31.1. Re-ran the six-scenario harness against all three merged blocks: the happy path supersedes duplicates silently; a failed PATCH, a degraded stored body, a degraded create, a literal @path body, and a 15-byte payload each report and none tombstone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 11s —— View job Code Review
Per this repo's Since the last code-review pass (
I hand-verified the new control flow and byte math in all three lanes ( 🟡 Finding —
|
|
Claude finished @kyle-sexton's task in 1m 20s —— View job Security Review
This PR remains prose/documentation only (three inlined The PATCH-exit-status gap flagged in the prior three review rounds (and by the Codex reviewer) is now fixed. All three inlined blocks — elif ! gh api -X PATCH "repos/$REPO/issues/comments/$CANON" -F body=@"$BODY_FILE" >/dev/null; then
echo "telemetry: the PATCH of comment $CANON failed - treat the lane as UNREPORTED ..." >&2
elif ! VERIFY "$CANON"; then
echo "telemetry: comment $CANON does NOT carry a well-formed telemetry body after the write ..." >&2This closes the exact class of failure identified: a I re-checked the analogous create/POST path for the same class of gap: it doesn't check the No other issues found: |
…e spoke `work-loop`'s SKILL.md sat at 499 of its 500-line hard cap on main, so the #943 gate, write-status check, read-back, and their prose pushed it to 548 and the skill-quality gate failed. The upsert moves to `reference/telemetry-upsert.md`, mirroring how the sibling `babysit-loop` lane already carries the identical block, with SKILL.md keeping the telemetry home and durable-state contract and pointing at the spoke for the mechanism. SKILL.md is 437 lines; the block, its lane-instance validation, the body gate, and the race reconcile move verbatim. The rationale for inlining rather than calling `claude-ops`'s wrapper is now stated once, in the spoke, instead of in both places. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 39s —— View job Code Review
Per this repo's Since the last code-review pass, one new commit landed: I verified the extraction directly:
No new findings on this delta. The still-open, lower-priority item from earlier passes on this PR — |
|
Claude finished @kyle-sexton's task in 1m 7s —— View job Security Review
The only change since the last security-review pass is commit This is a pure verbatim relocation, not a logic change. No new shell logic, no new variable interpolation, no new API calls, and no change to quoting, gating, or control flow was introduced by this move — the PATCH-exit-status check, No new security issues found in this delta. |
…angelog The file move is a visible change to the skill's shape, not just an internal tidy: a reader who knows where the upsert lived needs to be told it moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 43s —— View job Security Review
The only change since the last security-review pass on this PR is commit This is a pure prose addition — a new bullet in the No new security issues found in this delta. All substantive findings from earlier review rounds on this PR remain resolved as of this HEAD:
This PR remains prose/documentation only (inlined |
|
Claude finished @kyle-sexton's task in 2m 22s —— View job Code Review
Per this repo's Since the last code-review pass ( I checked the new bullet's factual claims directly rather than taking them at face value:
No functional/shell logic changed in this delta — it's prose-only, consistent with what the security lane already found for this same commit. Minor, non-blocking nit: this bullet documents a pure internal reorg (file split, no behavior change) but is filed under No new correctness issues found. All prior findings on this PR are already resolved as of this HEAD:
|
…hes they describe Follow-ups from the re-review, all wording or fail-safety, no behavior change on the happy path: - The claude-ops changelog still described two checks and two not-replicated items while the lanes skill it summarizes had moved to three and four. A reader of the changelog alone could not learn that an inline branch always exits 0, which is the deferral most likely to bite a caller. Mirrored, with the inherited limits named. - source-control's entry still said "both halves". - Two notices were false on a reachable sub-branch. An empty $CANON can also mean the create landed and the re-list missed it, so the notice no longer claims there is nothing there. A failed PATCH leaves a body that may be several cycles old, not "an earlier cycle". - The bare POST gets `|| true`. Under a `set -e` wrapper a failed create aborted the block before any notice, which would have made the "every branch exits 0 and reports" claim false exactly when it matters. - The race-reconcile paragraph now says tombstoning waits on the canonical write verifying, which is what the code does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 5s —— View job Security Review
The only change since the last security-review pass on this PR is commit This is a prose-and-diagnostics-only commit, not a logic change:
No new shell logic, no new variable interpolation, no new API calls, and no change to quoting, gating, or control flow was introduced by this delta. All prior findings on this PR remain resolved as of this HEAD:
This PR remains prose/documentation only (inlined No new security issues found in this delta, and no outstanding security findings remain open on this PR. |
Both sides bumped claude-ops to 0.25.1 and wrote under that heading: this branch a `### Changed` for the #943 inline-upsert guarantees, main a `### Fixed` for #1784's falsy-lane-field collapse. Neither supersedes the other and both ship in 0.25.1, so the release keeps one heading carrying both sections in Keep a Changelog order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 11s —— View job Security Review
The only change since the last security-review pass on this PR is merge commit Restricting to the files this PR actually owns ( No shell logic, no variable interpolation, no API calls, and no telemetry-upsert control flow were touched by this merge. This PR remains prose/documentation only (inlined No new security issues found in this delta, and no outstanding security findings remain open on this PR. All substantive findings from earlier review rounds remain resolved as of this HEAD:
|
Two textual conflicts, both mechanical: main shipped work-items 0.31.0 and 0.31.1 while this branch held 0.30.4, so the manifest takes 0.31.2 and this branch's entry is renumbered and stacked above main's shipped entries, which are untouched. Semantic check on the auto-merged side: main (#1841, #1850) moved work-loop's telemetry upsert body into reference/telemetry-upsert.md and made the sentinel marker lane-instance-suffixed. Both surfaces this branch cites still hold -- the "Telemetry and durable loop state" heading still states that the telemetry home is resolved from launch config before the default title, and the sentinel this branch matches on is written with an ellipsis after `marker=`, so the instance suffix does not falsify it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #943
Summary
The three lanes that inline the
gh apitelemetry upsert —source-control:babysit-loop,work-items:work-loop,work-items:attend-queue— wrote$BODY_FILEwith no assertion that whatthey composed was usable and no check that what they sent actually landed.
claude-ops'stelemetry-upsert.shrefuses a degraded body and reads the comment back; an installed plugin cannotinvoke a sibling plugin's script, so the three inlined copies inherited none of that. The observed
failure was a telemetry comment carrying a literal
@C:/…/telemetry_combined.txtas its entire bodyacross three sessions — the lane reported nothing while its
updatedAtkept moving, so any consumerkeying on the comment's timestamp rather than its body read it as fresh.
Fix
Each inlined block now carries three checks, because they catch different failures.
Pre-write gate. Before any API call, a
$BODY_FILEthat is empty, opens with a literal@, isnot sentinel-prefixed, or carries under 16 payload bytes below the sentinel is refused and the cycle
skips the upsert fail-closed — no POST, no PATCH. The floor is measured on everything below line 1,
so it matches the wrapper's
MIN_BODY_BYTESbyte-for-byte whether that line ends in LF or CRLF. Theprefix assertion compares bytes rather than a whole first line, so a CRLF body is not false-rejected.
Write-status check. The PATCH's own exit status is checked before anything trusts the result. A
failed write leaves the previous cycle's body in place, which a read-back running regardless would
accept — so the read-back alone cannot carry the guarantee.
Post-write read-back. The block then re-reads what the write stored and reports the cycle
UNREPORTED unless that body still opens with the sentinel and clears the same floor. This is the only
check that sees a write which reported success and stored something else, and the one that would have
caught #943 itself: the composed file was correct and the defect was the invocation (
-f body=@FILEtransmits the literal path; these blocks only ever use
-F body=@).Every branch that ends without a verified body — including a degraded create, which leaves no
sentinel-prefixed comment to re-read — reports and skips the duplicate-supersede pass, so a cycle
whose own write is unproven never tombstones a racing session's comment.
Supporting corrections in the same change:
$BODY_FILEsentinel-first-line contract was implicit in a trailing comment. A body composedwithout it is not merely rejected — the lookup matches on that prefix, so it would never be found
again and the next cycle would post a second comment. Now stated in prose.
morning-brief's freshness check is what a degraded bodydeceives. Verified against its
print_telemetry: it parseslane:/last-cycle:out of the body,so a degraded comment makes the lane vanish from the report rather than look healthy. Attributed
instead to any consumer keying on the comment's timestamp instead of its body.
states the limits it inherits rather than fixes: a PATCH that succeeds while storing the previous
body still verifies, the read-back proves some well-formed telemetry is present rather than
this cycle's, and every inline branch exits 0 and reports through stderr alone.
Deliberately not replicated from the wrapper: the 64 KiB cap, the body-file containment checks,
retries, and its distinct non-zero exit codes. Both surfaces now say which guarantees travel with
which.
Verification
The three blocks are byte-identical apart from their
MARKERline (checked mechanically, not byeye). Each was extracted and driven against a stubbed
gh, once per lane:@pathRow 2 is the one an independent review found silent before this change: the write failed, the
read-back re-read a well-formed previous body, and the cycle reported success over a stale comment.
The block was also driven under
set -ewith a failing create: it reaches the "no comment availableto write to" notice and exits 0, rather than aborting before any notice. That is what the bare POST's
|| truebuys, and it is what keeps the "every branch reports" claim true inside a strict-modewrapper.
SENTINEL_OKwas also driven directly across the floor boundary: sentinel-only, 15-byte and 16-bytepayloads, LF and CRLF, a non-sentinel 51-byte body, and a sentinel that is not the first line. LF and
CRLF now flip at the same 15/16 boundary, which is what "matches
MIN_BODY_BYTES" requires;lane: from-stdinis the same 16-byte stringtelemetry-upsert.test.shuses to pin that floor.Repo gates, run locally against
origin/mainand green in CI on this branch:scripts/check-changed-skills.sh origin/main— PASS (plugin-gate)scripts/check-changelog-parity.sh --check/--check-bump/--check-order— PASSmarkdownlint-cli2over the changed markdown — 0 issuesVersion bumps are re-derived on every merge from
main, which released all three plugins while thisbranch was open. The entries currently sit at
claude-ops0.25.1,source-control0.44.1, andwork-items0.31.1, each above the newest upstream entry in its own CHANGELOG. Ifmainreleasesany of them again before this merges, the top entry needs renumbering once more — the collision is in
the CHANGELOG heading and the manifest version, not in the change itself.
This branch merges
mainrather than rebasing, so the feature commits stay reachable and every pushis a fast-forward. #1841 landed mid-review and keys the marker per lane instance in these same three
blocks; both sides are kept — its
INSTANCEvalidation and instance-qualifiedMARKER, thisbranch's gate, write check, read-back, and gated supersede pass.
work-loop's SKILL.md sat at 499 of its 500-line hard cap onmain, so these additions tripped theskill-quality gate. Its upsert moves to
reference/telemetry-upsert.md— the same shape the siblingbabysit-looplane already uses for the identical block — leaving SKILL.md at 437 lines with thetelemetry home and durable-state contract, pointing at the spoke for the mechanism. The block moves
verbatim; the scenario table above was re-run against the spoke.
Follow-ups deliberately not taken here
exits (3 gate, 5 API, 6 read-back). Giving the inline blocks machine-readable outcomes is a
lane-driver change, not a babysit lane telemetry edit posts literal @path body instead of file content (#502 observability fail-open) #943 fix.
scripts/check-cross-plugin-source-drift.shstructurally cannot see this cluster —
skip_basenamesexcludesSKILL.md, and the third copysits at a different path-within-plugin. Registering the cluster is worth its own change.
Related
plugins/claude-ops/skills/lanes/scripts/telemetry-upsert.sh— the wrapper whose gate andread-back these inline blocks replicate, and whose limits they inherit.
docs/conventions/loop-lane/README.md— the loop-lane convention updated alongside.🤖 Generated with Claude Code
https://claude.ai/code/session_01FVoZoMYXqf8ZVbQYixPVPW