Skip to content

story(backlog): run an unscoped backlog cycle from a Claude Code cloud session - #482

Merged
z5labs-auto-merge[bot] merged 3 commits into
mainfrom
claude/youthful-keller-6hgz72
Nov 1, 2026
Merged

z5labs-auto-merge[bot] merged 3 commits into
mainfrom
claude/youthful-keller-6hgz72

Conversation

@Zaba505

@Zaba505 Zaba505 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Closes #480

A Claude Code cloud session's GitHub proxy answers GraphQL with HTTP 403 and refuses API paths outside repos/{owner}/{repo}/..., so the backlog cycle stopped at its first GitHub call there. Every script and skill fence now uses REST under repos/{owner}/{repo}/.... The one exception is select-issue.sh's project-scope read, which happens only when a scope is requested and is now exit 4 in a cloud session.

Acceptance criteria

  • No GraphQL in plugins/backlog/scripts/ except the project-scope read in select-issue.sh, and that read runs only for a requested scope. Every other call is gh api against repos/{owner}/{repo}/....

  • Repository resolution. New scripts/repo.sh reads git remote get-url origin (HTTPS, SSH, ssh://, or a local git proxy URL) and confirms it with GET /repos/{owner}/{repo}. It prints full_name and default_branch, so a renamed repository resolves to its new name. A remote that doesn't parse is exit 4, naming the URL with any credential masked.

  • select-issue.sh selects over REST.

    • Candidates come from GET /issues, with pull requests dropped. A milestone title is resolved to its number through /milestones, and pages are read up to select.limit.
    • Bodies and states come from GET /issues/{n}.
    • Native dependencies come from /dependencies/blocked_by, using each blocker's repository_url and state. That state seeds the cache, so a native edge needs no second read.
    • The walk is still ascending, UNREADABLE still blocks, and an unknown milestone is still exit 4.
  • A requested project scope that gets the GraphQL 403 is exit 4. The message says project-scoped selection is local-only in a cloud session and names --no-project-filter. When the scope came from flags, it says to drop them. With select.project.value: null, selection makes no project call.

  • await-checks.sh, finish-issue.sh, post-merge-criteria.sh and post-review.sh work over REST, with exit codes unchanged.

    • Pull request state, base, head, merge commit and auto merge come from GET /pulls/{n}.
    • Issues are read, closed (state_reason: completed) and commented on through /issues/{n} and /issues/{n}/comments.
  • await-review.sh requests reviews over REST only. It uses POST /pulls/{n}/requested_reviewers with the full …[bot] login and never calls GET /users/{login}. A login without [bot] is exit 3 before anything is read or requested. A bot that isn't installed is still exit 3. Whether a bot is requested is read back from the timeline as well; see Verification for why.

  • The local rung in a cloud session is UNAVAILABLE (exit 3) with no request made. app-token.sh exits 3 when CLAUDE_CODE_REMOTE=true, and also recognises the proxy's refusal of GET /app. It reports a key mismatch only on GitHub's own 401 or 404. setup-backlog's reviewer-credential row reports a cloud session as unverified.

  • Skill fences use REST.

    • gh pr create → POST /pulls
    • gh pr edit --add-label → POST /issues/{n}/labels
    • gh pr diff → GET /pulls/{n} with Accept: application/vnd.github.diff
    • gh pr view … state,autoMergeRequest → GET /pulls/{n}, reading .state, .merged and .auto_merge
    • gh issue comment → POST /issues/{n}/comments
    • setup-backlog: gh repo view, gh label list, gh pr list, gh issue list, gh issue view and the autoMergeAllowed query moved to REST. Project discovery stays as it is and says it works only locally. gh run stays.
  • Access probe. run-backlog and setup-backlog probe with gh api repos/{owner}/{repo} --jq .full_name instead of gh auth status. The read:project check runs only for a project-scoped run, and reads X-Oauth-Scopes from the same REST response.

  • Step 5 checks the pull request body instead of closingIssuesReferences. New scripts/closing-refs.sh requires exactly one closing keyword outside a fence, naming #, and has its own fixture corpus. The skill says this is weaker than GitHub's own parse.

  • Step 10 says an After merge: criterion on a discussion can't be carried out from a cloud session. It is reported as FAILED and the run as BLOCKED, with the pull request merged and the issue closed.

  • Step 4 runs each verify command with a Bash timeout of 600000.

  • Every stub gh refuses GraphQL. Each one sources scripts/testdata/gh-guard.sh, which refuses GraphQL-backed commands the way the proxy does and logs them. Each suite ends with a case asserting nothing was refused outside the project-scope cases. New cases cover:

    • REST native selection, including cross-repository blockers named by repository_url
    • a milestone title resolved to its number
    • pagination past one page, including a first page of nothing but pull requests
    • the project-scope exit 4
    • HTTPS and SSH remotes
    • the [bot] check
    • app-token.sh's handling of the proxy refusal

    Reverting one REST call to gh issue view fails five cases.

  • Verified from a Claude Code cloud session — see Verification below.

auto-merge_test.sh is unchanged. It tests the merge workflow, which runs on GitHub Actions, where GraphQL is served.

After merge — carried out once this pull request has merged

- [ ] After merge: in a fresh Claude Code cloud session on `main`, run `/backlog:next-issue` and comment its report on this issue.
- [ ] After merge: in the same session, run `/hosted-agents:review-plugin plugins/backlog --target claude-code-cloud` and comment the report on this issue. It has no `gh-auth-status-fails` finding. Every `breaks` finding left in it is on a path the cycle reaches only for a project scope or for the `local` rung.

Changes beyond the issue

  • The repository's own verify script. .github/scripts/verify-affected-modules.sh opened with gh repo view, so step 4 would have failed in a cloud session, including the first after-merge criterion's run. It now reads gh api 'repos/{owner}/{repo}' --jq .default_branch. This is a separate commit.
  • Copilot's refusal pattern. was(n.?t| not) never matched a typographic apostrophe under the C or POSIX locale a cloud session runs in, because ’ is three bytes there. With a successful Copilot check run, a refusal would have classified as a completed review. The pattern is now was(n.{0,3}t| not). This was also the one failing case in await-review_test.sh before this change.
  • An unreadable body is ineligible. Under the body styles, a body that couldn't be read used to be treated as empty, which made the issue eligible.
  • finish-issue.sh closes even when its comment fails. A comment that won't post is now a WARN. The close still happens, and it is the part that matters.
  • The issue list is read oldest first. When select.limit truncates the list, it keeps the lowest-numbered issues, which are the ones the ascending walk wants.
  • setup-backlog's permission allow-list grants only gh api, gh run and gh workflow among gh commands. Those are the only ones the cycle now runs.
  • Version. The plugin is bumped to 0.16.0.

Scanner

scan-plugin.sh plugins/backlog --target claude-code-cloud, compared with main:

main this branch
breaks github-graphql 22 1 (select-issue.sh, project scope only)
degrades github-graphql 15 0
gh-auth-status-fails 1 0
breaks non-repository-endpoint 1 0

Verification

Run from Claude Code cloud session https://claude.ai/code/session_014kTbDnKA5evnEZDcCBqoFu on this branch's scripts.

Suites and verify commands

  • All nine suites pass, with CLAUDE_CODE_REMOTE=true set and unset: app-token, auto-merge, await-checks, await-review, closing-refs, finish-issue, post-merge-criteria, repo and select-issue.
  • dagger check exits 0. bash .github/scripts/verify-affected-modules.sh exits 0, reporting no daggerverse module changed.

Selection and preflight

await-checks.sh 482. On cfa9c01 it gave exit 2, 2, then 0. After the fix commit 35ec7ac it gave 2, 2, then 0 again. Locally, CI would give the same sequence: exit 2 while it runs, then 0.

await-review.sh 482 copilot.

  • The first run was exit 3. That was a regression, and this verification caught it. The request had taken: the timeline shows review_requested for Copilot, type: Bot. But GET /pulls/{n}/requested_reviewers answers {"users":[],"teams":[]} for a requested bot, and story(opentofu): run a root module that sits inside a larger tree #474 reads the same. The old script only appeared to work because a successful GraphQL mutation counted as confirmation. With the request over REST, every Copilot request read as a failure.
  • 35ec7ac reads the request back from the timeline as well. A rung counts as requested while its newest review_requested is newer than its newest reviewed or review_request_removed. The test stub now records a bot's request on the timeline, as GitHub does, and reverting the read fails five cases.
  • Re-run on the fix: exit 0. Copilot's review completed. That is the same exit a local run gives once the review lands.

Copilot's review comments. All three are addressed in 35ec7ac:

  • app-token.sh's EXIT trap now calls a function. The quoted ${CLEANUP:+'…'} form does expand under bash 5, but older bash releases parse it differently.
  • The two X-Oauth-Scopes probes no longer pass a --jq they didn't need.

Observed while verifying. #474's Copilot review reads "Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota". Its commit has no copilot-pull-request-reviewer check run, so the gate gives exit 5, escalation. That wording describes unavailability, but it matches neither the refusal pattern nor anything else. That is a separate issue from this one.

🤖 Generated with Claude Code

https://claude.ai/code/session_014kTbDnKA5evnEZDcCBqoFu

…d session

A cloud session's GitHub proxy answers GraphQL with HTTP 403 and refuses
paths outside repos/{owner}/{repo}/..., so the cycle stopped at its first
GitHub call there. Every script and skill fence now uses REST under
repos/{owner}/{repo}/..., except select-issue.sh's project-scope read,
which happens only when a scope is requested.

- scripts/repo.sh resolves the repository from the origin remote (HTTPS
  or SSH), confirms it with GET /repos/{owner}/{repo}, and uses its
  full_name and default_branch. An unparseable remote is exit 4 naming it.
- select-issue.sh lists issues over REST (pull requests dropped, the
  milestone title resolved to its number, paged up to select.limit),
  reads bodies and states from /issues/{n}, and takes native edges and
  their states from /dependencies/blocked_by. A project scope that gets
  the session's GraphQL 403 is exit 4, local-only. An unreadable body is
  now ineligible rather than read as empty.
- await-checks, finish-issue, post-merge-criteria and post-review read
  pull requests and issues over REST; finish-issue closes with
  state_reason completed, and a comment that will not post no longer
  stops the close.
- await-review requests reviews over REST only, never calls
  GET /users/{login}, and treats a login without [bot] as not a Bot
  (exit 3). Copilot's refusal pattern now matches a typographic
  apostrophe under the C locale a cloud session runs in.
- app-token.sh is exit 3 in a cloud session (CLAUDE_CODE_REMOTE=true)
  and recognises the proxy's refusal of GET /app, without reporting a
  key mismatch GitHub never judged.
- closing-refs.sh replaces the closingIssuesReferences check with a
  parse of the pull request body, which the skill says is weaker.
- The skills' fences use REST; run-backlog and setup-backlog probe
  access with gh api repos/{owner}/{repo} instead of gh auth status;
  step 4 runs verify with a 600000 ms timeout; step 10 says a discussion
  criterion cannot be carried out from a cloud session.
- Every stub gh sources testdata/gh-guard.sh, which refuses GraphQL-
  backed commands as the proxy does and logs them, and each suite fails
  if anything was refused outside the project-scope cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kTbDnKA5evnEZDcCBqoFu
The script is the second verify command in .claude/backlog.json, and it
opened with gh repo view, which is GraphQL-backed. In a Claude Code cloud
session that is a 403, so the backlog cycle's verify step failed before
it diffed anything. gh api 'repos/{owner}/{repo}' reads the same value
over REST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kTbDnKA5evnEZDcCBqoFu

Copilot AI 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.

🟡 Changes recommended

Changes are blocked by an unresolved moderate shell-script cleanup bug in the EXIT trap that can fail to remove the temp key file, along with minor documentation command brittleness around combining gh --include with --jq.

3 open issues
What changed in this PR

This PR updates the plugins/backlog workflow so it can run an unscoped backlog cycle from a Claude Code cloud session by removing GraphQL-backed gh usage in favor of repository-scoped REST calls (with a single project-scope GraphQL exception that becomes a controlled exit).

Changes:

  • Add robust repository resolution via a new scripts/repo.sh (origin remote → confirm via GET /repos/{owner}/{repo}).
  • Replace GraphQL-backed gh commands across backlog scripts/skills with gh api repos/{owner}/{repo}/... REST routes; project-scoped selection becomes local-only with an explicit exit.
  • Add/expand offline test coverage and a gh guard to refuse GraphQL-backed commands in suites.
File Description
plugins/​backlog/​skills/​setup-backlog/​SKILL.md Update setup instructions to use REST + repo resolution helper; document cloud-session constraints.
plugins/​backlog/​skills/​run-backlog/​SKILL.md Replace auth probe guidance with REST access probe; clarify project-scope behavior.
plugins/​backlog/​skills/​next-issue/​SKILL.md Document REST-only cycle behavior and update command snippets accordingly.
plugins/​backlog/​scripts/​testdata/​gh-guard.sh Add a guard to fail tests if GraphQL-backed gh commands are used.
plugins/​backlog/​scripts/​select-issue.sh Port selection to REST; add milestone-number resolution and native dependency reads via REST.
plugins/​backlog/​scripts/​select-issue_test.sh Expand fixtures/tests for REST selection, pagination, milestone resolution, and cloud-session behavior.
plugins/​backlog/​scripts/​repo.sh New helper: parse origin remote, confirm via REST, print full_name + default_branch.
plugins/​backlog/​scripts/​repo_test.sh New fixture corpus for repo parsing/resolution (HTTPS/SSH/proxy remotes + renames).
plugins/​backlog/​scripts/​post-review.sh Resolve repo via repo.sh instead of GraphQL-backed gh repo view.
plugins/​backlog/​scripts/​post-merge-criteria.sh Read issue bodies via REST and resolve repo via repo.sh.
plugins/​backlog/​scripts/​post-merge-criteria_test.sh Add end-to-end REST-body-read coverage using a file-backed gh stub + GraphQL guard.
plugins/​backlog/​scripts/​finish-issue.sh Move PR/issue reads & close/comment operations to REST; resolve repo via repo.sh.
plugins/​backlog/​scripts/​finish-issue_test.sh Add end-to-end tests for REST close/comment behavior and GraphQL avoidance.
plugins/​backlog/​scripts/​closing-refs.sh New script: parse PR body (outside fences) for closing keywords over REST.
plugins/​backlog/​scripts/​closing-refs_test.sh New corpus for closing-keyword parsing + end-to-end REST check.
plugins/​backlog/​scripts/​await-review.sh Remove GraphQL/user lookups; request reviews via REST only with [bot] login validation.
plugins/​backlog/​scripts/​await-review_test.sh Update fixtures to assert REST-only behavior, [bot] enforcement, and cloud-session “local rung” unavailability.
plugins/​backlog/​scripts/​await-checks.sh Switch PR base/head reads to REST and resolve repo via repo.sh.
plugins/​backlog/​scripts/​await-checks_test.sh Add end-to-end wait plumbing tests using REST stubs + GraphQL guard.
plugins/​backlog/​scripts/​app-token.sh Improve cloud-session handling (unavailable local rung) and distinguish proxy refusal vs GitHub verdicts.
plugins/​backlog/​scripts/​app-token_test.sh Update tests to cover cloud-session behaviors and REST repo resolution.
plugins/​backlog/​assets/​backlog.schema.json Update schema docs to reflect REST-based repo/default-branch resolution and other behavior changes.
plugins/​backlog/​.claude-plugin/​plugin.json Bump plugin version to 0.16.0.
.github/​scripts/​verify-affected-modules.sh Replace gh repo view with REST to work in Claude Code cloud sessions.

Review effort: Lite

Comment thread plugins/backlog/scripts/app-token.sh Resolved
Verifying #480 on its own pull request found that
GET /pulls/{n}/requested_reviewers never lists a bot. On #482 it
answered {"users":[],"teams":[]} while the timeline carried
review_requested for Copilot, and #474 reads the same. The old request
path never noticed, because the GraphQL mutation's own response was what
confirmed the request. With the request over REST, await-review.sh
called every placed Copilot request a failure: exit 3, unavailable.

requested() now also reads the timeline. A rung is requested while its
most recent review_requested is newer than its most recent reviewed or
review_request_removed. The test stub records a bot's request on the
timeline, as GitHub does, and reverting the read fails five cases.

From review on #482: app-token.sh's EXIT trap now calls a function
instead of a string built with quotes inside ${CLEANUP:+...}, which
older bash releases parse differently. The X-Oauth-Scopes probe in
run-backlog and setup-backlog drops the --jq it did not need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kTbDnKA5evnEZDcCBqoFu
@Zaba505 Zaba505 added the auto-merge Hand this pull request to the auto merge workflow label Nov 1, 2026 — with Claude
@z5labs-auto-merge
z5labs-auto-merge Bot merged commit 78a6421 into main Nov 1, 2026
10 checks passed
@z5labs-auto-merge
z5labs-auto-merge Bot deleted the claude/youthful-keller-6hgz72 branch November 1, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Hand this pull request to the auto merge workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

story(backlog): run an unscoped backlog cycle from a Claude Code cloud session

3 participants