You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: search callers with no incomplete-results signal act on a false empty #9090
During the 2026-10-22 GitHub incident, the search API answered total_count: 0 with incomplete_results: true (repo:melodic-software/claude-code-plugins is:issue is:open returned 0; the real count was about 601). #9089 makes the one REST caller that gates admission (check-wip-cap) treat that as unknown. These callers read search through GraphQL or gh ... --search, which expose no incomplete signal, so a false empty reads as a real empty:
plugins/code-tidying/.../open-pr-count.sh:42: open-PR cap (a false 0 admits more)
plugins/playbooks/.../repo-sweep/scripts/state.sh:63: dedupe before filing (a false empty files a duplicate)
plugins/harness-ops/.../morning-brief.sh:1111: report only; reads issueCount, so a floor check is possible
plugins/harness-ops/.../restart-consumer.sh:866: report only
plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py:365,457: discovery; whether a PR missing from discovery releases a lease is unverified
For each caller that acts on a count or an empty set (the first two, and babysit discovery if a miss releases a lease): read through REST /search/issues and fail closed on incomplete_results: true, or cross-check a GraphQL issueCount against a non-search count before acting on an empty. Report-only callers say "count unavailable" instead of 0.
Proposed work class: work-class: scoped
Basis: automated intake triage; the lanes App applied this class.
Size: Medium
Complexity: Medium
Uncertainty: Known
Flags: none
Blocked by: none
Understood: scripts that read GitHub search through gh or GraphQL cannot see incomplete_results, so a false empty reads as a real empty. A recorded decision sets the fix per script. It covers open-pr-count, state, morning-brief and restart-consumer, each with its tests. Babysit discovery and list-items are out of scope.
Decision recorded. The owner answered in the Refinement and Issue Triage sessions (2026-10-25), deciding the open items below on the facts:
go with whatever the facts/data/metrics + /discovery:research says and ensure we're aligned with SOLID, DRY/SSOT principles
[quote truncated; the full decision is at the link]
Not admitted to the issue lane: no trusted-human acceptance criteria: a listed human must put the confirmed - [ ] criteria in the body or post them as their own comment, or, on an issue a listed human filed, intake's newest refinement comment must hold them unedited. Admission takes the issue once each of those is met, when intake triages it again (.github/actions/resolve-config/admission-rules.json).
melodic-automation-lanes commented on Oct 26, 2026
Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. On an issue a listed human filed, it counts as the issue's spec once posted, while unedited; it is still model text, so check it.
Intended outcome: The open-PR cap and the repo-sweep dedupe fail closed on an incomplete search, and report-only callers print count unavailable instead of 0.
Is babysit discovery in scope, i.e. does a PR missing from discovery release a lease?
RECOMMENDED: Out of scope: babysit_gh.py has no lease code, and discovery only builds a queue that is rebuilt on the next run. Basis: plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py:1473
Which mechanism should the two gating callers use: REST /search/issues with an incomplete_results check, or a non-search cross-check?
RECOMMENDED: Use REST /search/issues via gh api and fail closed on incomplete_results true or any error, as #9089 did. Basis: 9089
Which callers get the fail-closed fix and which only the count unavailable change, given the cited paths are stale (list-items.sh is under plugins/work-items/tools)?
RECOMMENDED: Fail closed in open-pr-count.sh and repo-sweep state.sh; make morning-brief, restart-consumer and github list-items.sh report unavailable after re-locating them. Basis: plugins/code-tidying/skills/tidy/scripts/open-pr-count.sh:42
How should the gh pr list --search callers be made safe, since gh pr list exposes no incomplete signal?
RECOMMENDED: List open PRs without --search and filter the head ref locally with jq, which avoids search entirely. Basis: plugins/playbooks/skills/repo-sweep/scripts/state.sh:63
Sources read
plugins/code-tidying/skills/tidy/scripts/open-pr-count.sh:42: counts open PRs with gh pr list --search 'head:chore/tidy-' and treats the result as the cap count
plugins/playbooks/skills/repo-sweep/scripts/state.sh:63: lists open PRs with gh pr list --search head:prefix --limit 1000 for dedupe
plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py:1448: notes gh search prs uses REST search; discovery merges search and list results; no lease code found in the file
plugins/work-items/tools/work-item-tracker/adapters/github/list-items.sh:26: labeled list comes from the search API
Automated refinement by the lanes App. Answer with a comment whose first line is **Decision recorded.**.
Decision recorded. The owner answered in the Refinement and Issue Triage sessions (2026-10-25), deciding the open items below on the facts:
go with whatever the facts/data/metrics + /discovery:research says and ensure we're aligned with SOLID, DRY/SSOT principles
I should only ever be in the loop ONLY when necessary (intent/vision changed, something came up during implementation that impacts trajectory/alignment, OR a high security risk) - I've covered this all before
No owner ledger (.work/*/interview-checklist.md, .work/gh-monitor-owner-decisions/) names this issue.
Is babysit discovery in scope?
Decided: No. A PR missing from discovery releases no lease. Leases live in babysit_lease.py, not babysit_gh.py. Reaping is keyed on expires_at alone (plugins/source-control/skills/babysit-prs/scripts/babysit_lease.py:275-283). The worktree prune drops a lease only after a per-PR gh pr view reads the PR as CLOSED or MERGED (prune_babysit_worktrees.py:408-423, :686-688), not from the discovery list. Discovery is also not search-only: repo mode lists each repository with gh pr list and no --search (babysit_gh.py:1356-1378, :1460-1464), and owner mode unions the search with that listing (babysit_gh.py:1466-1471).
Which mechanism for the gating callers?
Decided: Do not use search. Both gating callers already filter the head ref locally after the read (open-pr-count.sh:42 counts with --jq; plugins/playbooks/skills/repo-sweep/scripts/state.sh:64-65 re-checks headRefName against ^chore/repo-sweep-). So the smaller fix is to drop --search and filter the prefix locally, which avoids the search API, as the refinement's question 4 recommends. A listing without a prefilter returns every open PR, so it must set --limit and treat a full page as incomplete. Otherwise gh's default page of 30 (plugins/work-items/tools/work-item-tracker/adapters/github/list-items.sh:4) would read low and admit more, which widens the cap. The precedent is babysit's limit check (babysit_gh.py:1388-1391).
What does "fail closed" mean per caller?
Decided: For open-pr-count.sh, the tidy skill carries on when the count is unknown (plugins/code-tidying/skills/tidy/SKILL.md:21) and stops only when 3 or more are open (SKILL.md:111). So printing Throttle active: unknown is fail-open. A listing that reaches its limit prints Throttle active: yes, which stops tidy without editing the skill. For state.sh, exit 10 means no sweep PR (state.sh:29, :68), and a false 10 files a duplicate. A listing that reaches its limit exits 1, the error code (state.sh:28).
Report-only callers: how is an incomplete read detected?
Decided: Each one is checked against a non-search source the script already has:
morning-brief's stuck section reads a GraphQL search (morning-brief.sh:1119-1146) and prints (none ...) on an empty (:1268-1272). Its Queues counts go through gh issue list --label (:1020-1025), which gh serves from search (list-items.sh:25-28). The script already has a non-search counter, count_label_rest (morning-brief.sh:1027-1033, REST repos/.../issues?labels=). A 0 from search is cross-checked against it, and the label shows count unavailable when REST reads more than 0.
restart-consumer's resolve_issue_by_title (restart-consumer.sh:876-882) reads gh issue list --search. An empty becomes a no-telemetry record (:1082-1085). A failed read already returns 1 and becomes api-error with no restart (:870-875, :1077-1080). The lookup moves to REST search/issues and returns 1 when incomplete_results is not false, as check-wip-cap.mjs:100-101 does. The telemetry search at :1247-1249 is pinned byte-identical to morning-brief's (:1240-1246), so it is left unchanged.
list-items.sh?
Decided: No change. It prints no count. Its envelope's truncated flag is the completeness signal (plugins/work-items/tools/work-item-tracker/CONTRACT.md:268-276). Only a --label read uses search (list-items.sh:25-32), and no script in plugins/ passes --label to list-items or list-frontier (git grep -nE '(list-items|list-frontier)\b[^|]*--label' origin/main -- plugins, excluding tests, conformance and adapters, finds only docs). A false empty then means no candidates, which picks no work. Adding an envelope field would change five adapters for no consumer.
Acceptance criteria:
plugins/code-tidying/skills/tidy/scripts/open-pr-count.sh lists open PRs without --search, with an explicit --limit, and counts head refs starting chore/tidy- locally. A listing that reaches the limit prints Throttle active: yes.
plugins/playbooks/skills/repo-sweep/scripts/state.sh lists open PRs without --search and keeps its local headRefName filter. A listing that reaches its --limit exits 1, never 10.
morning-brief.sh prints count unavailable for a label whose search read gives 0 while count_label_rest gives more than 0, in both the Queues and stuck sections.
restart-consumer.shresolve_issue_by_title reads REST search/issues and returns 1 when incomplete_results is not false, so the lane records api-error, not no-telemetry.
Each changed script's test (open-pr-count.test.sh, state.test.sh, morning-brief.test.sh, restart-consumer.test.sh) covers its new case: a full page, a search 0 against a REST count above 0, and incomplete_results: true.
This records a re-fire of an earlier refusal, not a new refusal. The lanes kill switch stopped run 40441477886, the work-resolve-issue run that agent-ready started, before it did any work; that run ended at 2026-10-27T04:32:48Z. work-unblock has now removed and added agent-ready back to start a fresh run. A kill-switch refusal is no fault in this issue or the lanes, so it never counts against later re-fires; after limits.limit-stops-per-issue of them since a person last added agent-ready or a run last reported, work-unblock waits longer before each one, up to 24 hours.
Activity
github-actions commentedon Oct 22, 2026
Proposed work class: work-class: scoped
Basis: automated intake triage; the lanes App applied this class.
Size: Medium
Complexity: Medium
Uncertainty: Known
Flags: none
Blocked by: none
Understood: scripts that read GitHub search through
ghor GraphQL cannot seeincomplete_results, so a false empty reads as a real empty. A recorded decision sets the fix per script. It covers open-pr-count, state, morning-brief and restart-consumer, each with its tests. Babysit discovery and list-items are out of scope.Decision: #9090 (comment)
The decision settles every open question, so nothing further is asked.
Automated triage, applied by the lanes App.
github-actions commentedon Oct 26, 2026
The triage sweep dispatched intake triage for this issue (attempt 1 of 2).
melodic-automation-lanes commentedon Oct 26, 2026
Not admitted to the issue lane: no trusted-human acceptance criteria: a listed human must put the confirmed
- [ ]criteria in the body or post them as their own comment, or, on an issue a listed human filed, intake's newest refinement comment must hold them unedited. Admission takes the issue once each of those is met, when intake triages it again (.github/actions/resolve-config/admission-rules.json).melodic-automation-lanes commentedon Oct 26, 2026
The latest triage verdict holds nothing: the issue is at
status: ready.melodic-automation-lanes commentedon Oct 26, 2026
Refinement. A read-only run explored this repository, searched its issues and pull requests, and read the allowed docs to draft what follows. On an issue a listed human filed, it counts as the issue's spec once posted, while unedited; it is still model text, so check it.
Intended outcome: The open-PR cap and the repo-sweep dedupe fail closed on an incomplete search, and report-only callers print count unavailable instead of 0.
Related work
Open questions
RECOMMENDED: Out of scope: babysit_gh.py has no lease code, and discovery only builds a queue that is rebuilt on the next run. Basis: plugins/source-control/skills/babysit-prs/scripts/babysit_gh.py:1473
RECOMMENDED: Use REST /search/issues via gh api and fail closed on incomplete_results true or any error, as #9089 did. Basis: 9089
RECOMMENDED: Fail closed in open-pr-count.sh and repo-sweep state.sh; make morning-brief, restart-consumer and github list-items.sh report unavailable after re-locating them. Basis: plugins/code-tidying/skills/tidy/scripts/open-pr-count.sh:42
RECOMMENDED: List open PRs without --search and filter the head ref locally with jq, which avoids search entirely. Basis: plugins/playbooks/skills/repo-sweep/scripts/state.sh:63
Sources read
Automated refinement by the lanes App. Answer with a comment whose first line is
**Decision recorded.**.kyle-sexton commentedon Oct 26, 2026
Decision recorded. The owner answered in the Refinement and Issue Triage sessions (2026-10-25), deciding the open items below on the facts:
No owner ledger (
.work/*/interview-checklist.md,.work/gh-monitor-owner-decisions/) names this issue.Questions from intake's refinement (#9090 (comment)):
Decided: No. A PR missing from discovery releases no lease. Leases live in
babysit_lease.py, notbabysit_gh.py. Reaping is keyed onexpires_atalone (plugins/source-control/skills/babysit-prs/scripts/babysit_lease.py:275-283). The worktree prune drops a lease only after a per-PRgh pr viewreads the PR as CLOSED or MERGED (prune_babysit_worktrees.py:408-423,:686-688), not from the discovery list. Discovery is also not search-only: repo mode lists each repository withgh pr listand no--search(babysit_gh.py:1356-1378,:1460-1464), and owner mode unions the search with that listing (babysit_gh.py:1466-1471).Decided: Do not use search. Both gating callers already filter the head ref locally after the read (
open-pr-count.sh:42counts with--jq;plugins/playbooks/skills/repo-sweep/scripts/state.sh:64-65re-checksheadRefNameagainst^chore/repo-sweep-). So the smaller fix is to drop--searchand filter the prefix locally, which avoids the search API, as the refinement's question 4 recommends. A listing without a prefilter returns every open PR, so it must set--limitand treat a full page as incomplete. Otherwise gh's default page of 30 (plugins/work-items/tools/work-item-tracker/adapters/github/list-items.sh:4) would read low and admit more, which widens the cap. The precedent is babysit's limit check (babysit_gh.py:1388-1391).Decided: For
open-pr-count.sh, the tidy skill carries on when the count is unknown (plugins/code-tidying/skills/tidy/SKILL.md:21) and stops only when 3 or more are open (SKILL.md:111). So printingThrottle active: unknownis fail-open. A listing that reaches its limit printsThrottle active: yes, which stops tidy without editing the skill. Forstate.sh, exit 10 means no sweep PR (state.sh:29,:68), and a false 10 files a duplicate. A listing that reaches its limit exits 1, the error code (state.sh:28).Decided: Each one is checked against a non-search source the script already has:
morning-brief.sh:1119-1146) and prints(none ...)on an empty (:1268-1272). Its Queues counts go throughgh issue list --label(:1020-1025), which gh serves from search (list-items.sh:25-28). The script already has a non-search counter,count_label_rest(morning-brief.sh:1027-1033, RESTrepos/.../issues?labels=). A 0 from search is cross-checked against it, and the label showscount unavailablewhen REST reads more than 0.resolve_issue_by_title(restart-consumer.sh:876-882) readsgh issue list --search. An empty becomes ano-telemetryrecord (:1082-1085). A failed read already returns 1 and becomesapi-errorwith no restart (:870-875,:1077-1080). The lookup moves to RESTsearch/issuesand returns 1 whenincomplete_resultsis notfalse, ascheck-wip-cap.mjs:100-101does. The telemetry search at:1247-1249is pinned byte-identical to morning-brief's (:1240-1246), so it is left unchanged.list-items.sh?Decided: No change. It prints no count. Its envelope's
truncatedflag is the completeness signal (plugins/work-items/tools/work-item-tracker/CONTRACT.md:268-276). Only a--labelread uses search (list-items.sh:25-32), and no script inplugins/passes--labeltolist-itemsorlist-frontier(git grep -nE '(list-items|list-frontier)\b[^|]*--label' origin/main -- plugins, excluding tests, conformance and adapters, finds only docs). A false empty then means no candidates, which picks no work. Adding an envelope field would change five adapters for no consumer.Acceptance criteria:
plugins/code-tidying/skills/tidy/scripts/open-pr-count.shlists open PRs without--search, with an explicit--limit, and counts head refs startingchore/tidy-locally. A listing that reaches the limit printsThrottle active: yes.plugins/playbooks/skills/repo-sweep/scripts/state.shlists open PRs without--searchand keeps its localheadRefNamefilter. A listing that reaches its--limitexits 1, never 10.morning-brief.shprintscount unavailablefor a label whose search read gives 0 whilecount_label_restgives more than 0, in both the Queues and stuck sections.restart-consumer.shresolve_issue_by_titlereads RESTsearch/issuesand returns 1 whenincomplete_resultsis notfalse, so the lane recordsapi-error, notno-telemetry.open-pr-count.test.sh,state.test.sh,morning-brief.test.sh,restart-consumer.test.sh) covers its new case: a full page, a search 0 against a REST count above 0, andincomplete_results: true.melodic-automation-lanes commentedon Oct 27, 2026
This records a re-fire of an earlier refusal, not a new refusal. The lanes kill switch stopped run 40441477886, the work-resolve-issue run that
agent-readystarted, before it did any work; that run ended at 2026-10-27T04:32:48Z. work-unblock has now removed and addedagent-readyback to start a fresh run. A kill-switch refusal is no fault in this issue or the lanes, so it never counts against later re-fires; after limits.limit-stops-per-issue of them since a person last addedagent-readyor a run last reported, work-unblock waits longer before each one, up to 24 hours.