Skip to content

fix: search callers with no incomplete-results signal act on a false empty #9090

Closed

Description

@kyle-sexton

Problem

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
  • plugins/work-items/skills/work-item-tracker/adapters/github/list-items.sh:48

Fix

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.

Related

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 22, 2026
  2. github-actions commented on Oct 22, 2026

    @github-actions
    Contributor

    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]

    Decision: #9090 (comment)

    The decision settles every open question, so nothing further is asked.

    Automated triage, applied by the lanes App.

  3. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 22, 2026
  4. github-actions commented on Oct 26, 2026

    @github-actions
    Contributor

    The triage sweep dispatched intake triage for this issue (attempt 1 of 2).

  5. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.
    and removed
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    on Oct 26, 2026
  6. melodic-automation-lanes commented on Oct 26, 2026

    @melodic-automation-lanes
    Contributor

    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).

  7. melodic-automation-lanes commented on Oct 26, 2026

    @melodic-automation-lanes
    Contributor

    The latest triage verdict holds nothing: the issue is at status: ready.

  8. melodic-automation-lanes commented on Oct 26, 2026

    @melodic-automation-lanes
    Contributor

    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

    1. 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
    2. 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
    3. 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
    4. 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.**.

  9. kyle-sexton commented on Oct 26, 2026

    @kyle-sexton
    MemberAuthor

    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.

    Questions from intake's refinement (#9090 (comment)):

    1. 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).
    2. 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).
    3. 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).
    4. 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.
    5. 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.sh resolve_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.
  10. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    and removed
    status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 26, 2026
  11. added
    status: readyTriaged, unblocked, and fully specified; eligible to pick up.
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    and removed on Oct 26, 2026
  12. melodic-automation-lanes commented on Oct 27, 2026

    @melodic-automation-lanes
    Contributor

    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.

  13. added
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    and removed
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.priority: mediumReal value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions