Skip to content

fix: report the PR source as unavailable when gh pr list fails, never as empty - #247

Merged
topij merged 3 commits into
mainfrom
fix/pr-list-fail-closed
Aug 2, 2026
Merged

fix: report the PR source as unavailable when gh pr list fails, never as empty#247
topij merged 3 commits into
mainfrom
fix/pr-list-fail-closed

Conversation

@topij

@topij topij commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Found by CodeRabbit reviewing cs-toolkit's copy of this file, not this repo's —
which is the shared-workflow design paying for itself on its first day. An adopter
converting its forked /session-start adapter to a pointer put the fork's reviewer
in front of the kit's text, and it found a bug here.

The defect

The gather says what to do when gh pr list truncates and nothing about what
to do when it fails.

A non-zero exit, a partial response, or an absent gh all return nothing — and
nothing is indistinguishable from a repo that genuinely has no open PRs. The first
case renders an all-clear 🔴 bucket off a command that never ran: a briefing
that is confidently wrong rather than visibly incomplete, which is precisely what
the truncation caveat two lines above exists to prevent.

Why it is a defect and not a design choice

The tracker source in this same step already carries exactly this rule:

"(optional — if the script/key fails, note the gap and continue) … A missing/invalid
config or missing tracker credential should exit non-zero with a clear message —
treat any non-zero exit as the optional-tracker gap (note it and continue) — never
act on a partial payload."

Two sources in one step, one fail-closed and one not. They differ only in that one
said so.

The change

  • The gather bullet now says to report the PR source as unavailable, never as
    empty
    , and names the three ways it returns nothing.
  • The render template gains an explicit PRs: unavailable (<reason>) alternative,
    so the gap has somewhere to appear instead of silently becoming 0 open PRs.

Scope

Deliberately narrow, and deliberately not the search-route work. PR #244 tried
to give the Remediation check a route when no parameter-passing search tool
exists; both panel lenses returned HIGH on it and it was closed unmerged under a
bound declared before the second lens reported. #245 carries those findings and the
constraints for a next attempt.

This is a different paragraph, a different failure mode, and touches nothing #245
covers. It is separated because cs-toolkit's conversion PR is blocked on it and
should not wait on a design question.

Verification

make test → 721 passed. python scripts/kit_doctor.py → 32 unchanged, 0 differ,
0 missing.

What that does not establish. Neither executes this prose. The finding came
from a reviewer reading it, and I have not reproduced the fail-open live — "a
failed gh pr list renders as all-clear" is a claim about agent behaviour, and the
argument for the fix rests on the inconsistency with the tracker source rather than
on an observed briefing.

Summary by CodeRabbit

  • Documentation
    • Updated session-start guidance to clearly distinguish unavailable data from empty results.
    • Added handling for missing tools, credentials, failed commands, truncated pull request results, and unavailable CI or scheduled-task status.
    • Clarified when pull request data should be gathered again to ensure complete results.

… as empty

CodeRabbit, Major, found on cs-toolkit's copy of this workflow rather than here —
which is the shared-workflow design paying for itself on its first day: an adopter
converting its forked adapter to a pointer got the fork's reviewer aimed at the
kit's text.

The gather told you what to do when `gh pr list` TRUNCATES and said nothing about
what to do when it FAILS. A non-zero exit, a partial response, or an absent `gh`
all return nothing, and nothing is indistinguishable from a repo that genuinely
has no open PRs. The first case renders an all-clear 🔴 bucket off a command that
never ran — a briefing that is confidently wrong rather than visibly incomplete,
which is the same failure the truncation caveat two lines above exists to prevent.

What makes it clearly a defect rather than a design choice: the TRACKER source in
this same step already carries exactly this rule ("if the script/key fails, note
the gap and continue … never act on a partial payload"). The two sources differ
only in that one said so. The render template now has an explicit
`PRs: unavailable (<reason>)` alternative so the gap has somewhere to appear.

Deliberately narrow. This is not the search-route work: PR #244 tried to give the
remediation check a route when no parameter-passing search tool exists, drew two
HIGHs from both panel lenses, and was closed unmerged under a pre-declared bound
(#245 carries the findings). This touches a different paragraph, a different
failure mode, and nothing #245 covers.

VERIFIED: `make test` 721 passed; `python scripts/kit_doctor.py` 32 unchanged, 0
differ, 0 missing. Neither executes this prose. The finding itself came from a
reviewer reading it, and the fail-open it describes has not been reproduced live —
stated because "a failed gh pr list renders as all-clear" is a claim about agent
behaviour I have not executed.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@topij, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b580936-23b6-4fb7-bbee-2bad5e6534fb

📥 Commits

Reviewing files that changed from the base of the PR and between 3212ba4 and 271fdbf.

📒 Files selected for processing (2)
  • docs/agentic-dev-kit/workflows/session-start.md
  • kit-manifest.json
📝 Walkthrough

Walkthrough

The session-start workflow now reports failed or unavailable sources explicitly, distinguishes failed PR queries from truncated results, and supports unavailable PR and CI/cron statuses with reasons. The manifest contains the updated workflow hash.

Changes

Session-start reporting

Layer / File(s) Summary
Source availability rules and manifest
docs/agentic-dev-kit/workflows/session-start.md, kit-manifest.json
The workflow reports unavailable sources explicitly instead of treating them as empty. PR instructions distinguish failed queries from truncated results and require a rerun at the limit. Briefing statuses include unavailable PR and CI/cron results with reasons. The manifest hash is updated.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the central failure-handling change, although the implementation also covers CI/cron and other gather sources.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…the file's own bracket convention

Panel round 1 on this PR — both lenses, no HIGH. Three findings, all acted on.

1. BOTH LENSES: the render template broke the file's own alternation convention.
   Every other either/or wraps BOTH sides in one bracket group
   (`<clean | N uncommitted/untracked>`, `<all green | N failed/skipped>`,
   `<delegate ⇒ … | inline ⇒ …>`). Mine put the `|` outside:
   `<N> open PRs | PRs: unavailable (<reason>)`. An agent following the template
   literally emits `0 open PRs | PRs: unavailable (gh: command not found)` — a
   fabricated zero in front of the caveat, which recreates the exact fail-open the
   paragraph exists to close. Now `<N open PRs | PRs unavailable: reason>`.
   Verified: `grep -n '<[^<>]*|[^<>]*>'` returns every alternation in the file and
   all of them are fully wrapped; no bare `|` survives outside a bracket.

2. BOTH LENSES, and the correctness one found the sharper version: my consistency
   argument was selective. I claimed the PR and tracker sources "differ only in
   that one said so" — while the CI/cron field IMMEDIATELY TO THE RIGHT in the same
   rendered line carries the identical fail-open with no handling at all. A cron
   health command that never ran renders as `all green`.

   So the rule is not a PR-source rule. It is now stated once for the whole gather,
   and CI/cron gets the `unavailable: reason` alternative it lacked. This is the
   same rule applied at its correct scope, not a new mechanism — the finding was
   that my scope was wrong, and narrowing the claim instead would have left a known
   fail-open in the field next to the one I was fixing.

3. ADVERSARIAL: "a partial response returns nothing" was wrong. A truncated but
   parseable response returns FEWER rows, not none — no exit code reveals it, and
   it is the adjacent full-page check's concern, not this one's. The two failure
   shapes are now distinguished explicitly instead of being collapsed.

WHAT THE LENSES ALSO ESTABLISHED, worth carrying: both mutation-tested this change
in isolated copies and both got `720 passed, 1 deselected` with the mutation
SURVIVING. One reverted the whole paragraph, the other inverted its opening
sentence to say the opposite. Nothing behavioural caught either. This file has no
test coverage of its prose meaning at all, so the panel is the only check it gets —
which is the argument for having run one on a nine-line diff.

VERIFIED: `make test` 721 passed; `python scripts/kit_doctor.py` 32 unchanged, 0
differ, 0 missing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/agentic-dev-kit/workflows/session-start.md`:
- Around line 40-46: Update the config-drift rendering flow in the session-start
briefing to distinguish a failed or unavailable check from a genuine empty drift
result. When the drift command returns no usable result because of a non-zero
exit, missing binary, or absent credential, render “Config drift unavailable:
<reason>”; retain the existing all-clear rendering only for a successful check
with no outstanding drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0dfcb405-5d34-46e7-ba61-c388a54797e7

📥 Commits

Reviewing files that changed from the base of the PR and between e49ddf3 and 3212ba4.

📒 Files selected for processing (2)
  • docs/agentic-dev-kit/workflows/session-start.md
  • kit-manifest.json

Comment on lines +40 to +46
**A source that fails is reported as unavailable, never as its empty value.** A
non-zero exit, a missing binary, or an absent credential returns nothing — and
nothing is indistinguishable from a genuine zero: no open PRs, no failing jobs, no
drift. Rendering the empty value turns a command that never ran into an all-clear,
which is worse than a visible gap because nothing on the briefing says to look
again. This applies to every source below; the tracker bullet states it for itself
only because it was written first, not because it is the only one it covers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

file="docs/agentic-dev-kit/workflows/session-start.md"
rg -n -C 4 'config-drift|unavailable|All clear|handoff|friction-log|Tracker|CI/cron|PRs' "$file"

Repository: topij/agentic-dev-kit

Length of output: 10116


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
file="docs/agentic-dev-kit/workflows/session-start.md"
sed -n '34,72p;230,262p' "$file" | cat -n

Repository: topij/agentic-dev-kit

Length of output: 5324


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re

doc = Path("docs/agentic-dev-kit/workflows/session-start.md").read_text()

# Extract relevant sections by heading/index rather than line numbers.
gather = doc[doc.index("### 0 · Gather"):doc.index("### 1 · Classify")]
render = doc[doc.index("### 3 · Render the briefing"):doc.index("## Notes")]

unavailable_rule = re.search(
    r"A source that fails is reported as unavailable.*This applies to every source below",
    gather,
    re.S,
).group(0)
print("Unavailable general rule present:", unavailable_rule.strip())
print("unavailable_rule_mentions_early": unavailable_rule.strip()[:80])

def has_unavailable_alternative(section, source):
    if source in ("PRs", "open PRs"):
        return "PRs unavailable" in section or "unavailable" in section
    if source in ("CI/cron", "cron/CI"):
        return "unavailable: reason" in section
    if source == "Tracker":
        return "if the script/key fails" in section or "gap" in section
    return "unavailable" in section

for s in ["tracker", "PRs", "CI/cron", "config-drift"]:
    print(s, "has_unavailable_alternative:", has_unavailable_alternative(render, s))

print("\nConfig drift instructions:")
patterns = [
    "config-drift check only appears when your drift check reports something outstanding",
    "Config drift unavailable",
    "unavailable: reason",
]
for p in patterns:
    print("contains", p, "=>", p in render)
PY

Repository: topij/agentic-dev-kit

Length of output: 334


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re

doc = Path("docs/agentic-dev-kit/workflows/session-start.md").read_text()

gather = doc[doc.index("### 0 · Gather"):doc.index("### 1 · Classify")]
render = doc[doc.index("### 3 · Render the briefing"):doc.index("## Notes")]

match = re.search(
    r"A source that fails is reported as unavailable.*This applies to every source below",
    gather,
    re.S,
)
print("Unavailable general rule present:", bool(match))
print("Rule text:", match.group(0).strip() if match else "")

def has_unavailable_alternative(section, source):
    if source in ("PRs", "open PRs"):
        return "PRs unavailable" in section or "unavailable" in section
    if source in ("CI/cron", "cron/CI"):
        return "unavailable: reason" in section
    if source == "Tracker":
        return "if the script/key fails" in section or "gap" in section
    return "unavailable" in section

for s in ["tracker", "PRs", "CI/cron", "config-drift"]:
    print(s, "has_unavailable_alternative_on_render_page:", has_unavailable_alternative(render, s))

print("\nConfig drift instructions:")
for p in [
    "config-drift line only appears when your drift check reports something outstanding",
    "Config drift unavailable",
    "unavailable: reason",
]:
    print(repr(p), "=>", p in render)
PY

Repository: topij/agentic-dev-kit

Length of output: 1070


Add an unavailable render path for config drift.

config-drift is included in the “every source” rule and can return nothing on a failed non-zero command, but the briefing template only renders it when outstanding drift is reported. A failed drift check can omit the line and still render ✅ All clear. Render Config drift unavailable: <reason> when the drift check fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/agentic-dev-kit/workflows/session-start.md` around lines 40 - 46, Update
the config-drift rendering flow in the session-start briefing to distinguish a
failed or unavailable check from a genuine empty drift result. When the drift
command returns no usable result because of a non-zero exit, missing binary, or
absent credential, render “Config drift unavailable: <reason>”; retain the
existing all-clear rendering only for a successful check with no outstanding
drift.

…ift render slot, and an accurate enumeration

Delta pass, one MEDIUM regression, no HIGH. Declared before this round: it is the
last fix round on this PR; anything surviving is logged rather than patched again.

THE FINDING. Round 1's widened preamble claimed the rule "applies to every source
below" and named "no drift" among its examples — while the config-drift render note
was byte-identical to base and still binary: the line "only appears when your drift
check reports something outstanding". So a drift check that could not run rendered
exactly like one that found nothing. The paragraph named the failure shape it left
unfixed, which is round 1's own finding recurring one level up: widening the claim
created coverage I had not operationalised.

Fixed by giving config-drift the slot the other two already had, and by replacing
"every source below" with an enumeration that is checkable.

AND THE ENUMERATION WAS WRONG ON ITS FIRST DRAFT — caught by verifying it rather
than by a reviewer. The replacement sentence said all four named sources "each have
a slot in Render the briefing". The tracker does not; it carries the rule inline
and reports its gap in prose. Grepping for a slot per named source returned nothing
for the tracker, before push. That is the third overclaim in three rounds on this
paragraph, and the only one caught by its author.

Every claim the paragraph now makes was re-checked mechanically:

  PR list has a render slot        1
  CI/cron has a render slot        1
  config-drift has a render slot   1
  tracker states it inline         1
  file reads excluded, with why    1

Also from the delta pass, NOT fixed and logged instead, per the declared bound:
`<N open PRs | …>` puts its label inside the alternation while
`CI/cron: <all green | …>` puts it outside. Both render sensibly; it is
template-authoring inconsistency, not a defect, and a third round to restyle it
would cost more than it returns.

WHAT THREE ROUNDS ESTABLISHED ABOUT THIS FILE. All three lenses mutation-tested it
independently and all three got the mutation SURVIVING at `720 passed, 1
deselected` — reverting the paragraph, inverting its opening sentence, and
restoring the old render line respectively. There is no behavioural coverage of
this document's meaning. The panel is the only check it gets, and on a change this
size it found a real defect in every round.

VERIFIED: `make test` 721 passed; `python scripts/kit_doctor.py` 32 unchanged, 0
differ, 0 missing.
@topij

topij commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Round 2 — your Major is already fixed, and it's a clean independent confirmation.

The finding is real and stale

You reviewed 3212ba4 and flagged that config-drift was inside the "every source" rule while the render template only emitted its line when drift was outstanding — so a failed check omits the line and the briefing still reads ✅ All clear.

Correct. It was fixed in 271fdbf, which landed before your review posted:

head config drift: unavailable slot
3212ba4 (what you reviewed) 0
271fdbf (current) present at line 265

Verified by git show <sha>:<path> | grep at both revisions rather than assumed, since this repo's watch loop specifically says to check findings against current code because they go stale across rounds.

Two independent reviewers, same defect

The fallback panel's adversarial lens ran on 3212ba4 in parallel with you and reported the same thing — that my round-1 widening claimed to cover "no drift" while leaving the config-drift render note byte-identical to base.

That's worth recording rather than just noting: you were rate-limited across most of this PR, so the panel has been the substitute. Here the two overlapped on one head and converged on the same finding, which is the first direct evidence this session that the substitute is aimed at the same things the bot is.

What the fix also exposed, caught before push

The sentence replacing "every source below" claimed all four named sources have a render slot. The tracker doesn't — it carries the rule inline and reports its gap in prose. Found by grepping for a slot per named source before pushing, not by a reviewer.

Third overclaim in three rounds on that one paragraph, and the only one I caught myself. The pattern is consistent: each time I widened the scope sentence, it outran what I'd actually implemented. What finally held was replacing the sweeping claim with an enumeration that can be checked mechanically —

PR list has a render slot        1
CI/cron has a render slot        1
config-drift has a render slot   1
tracker states it inline         1
file reads excluded, with why    1

Bound

Declared before this round: 271fdbf is the last fix round on this PR, and anything surviving gets logged rather than patched. Your finding doesn't test that bound — it was already fixed. One item is logged and not fixed: <N open PRs | …> puts its label inside the alternation while CI/cron: <all green | …> puts it outside. Both render sensibly; restyling costs more than it returns.

make test 721 passed, kit_doctor 32 unchanged 0 differ at the current head.

@topij
topij merged commit 6bf4443 into main Aug 2, 2026
3 checks passed
@topij
topij deleted the fix/pr-list-fail-closed branch August 2, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant