Repository navigation
story(backlog): run an unscoped backlog cycle from a Claude Code cloud session - #482
Merged
Merged
Conversation
…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
There was a problem hiding this comment.
🟡 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 viaGET /repos/{owner}/{repo}). - Replace GraphQL-backed
ghcommands across backlog scripts/skills withgh api repos/{owner}/{repo}/...REST routes; project-scoped selection becomes local-only with an explicit exit. - Add/expand offline test coverage and a
ghguard 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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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 underrepos/{owner}/{repo}/.... The one exception isselect-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 inselect-issue.sh, and that read runs only for a requested scope. Every other call isgh apiagainstrepos/{owner}/{repo}/....Repository resolution. New
scripts/repo.shreadsgit remote get-url origin(HTTPS, SSH,ssh://, or a local git proxy URL) and confirms it withGET /repos/{owner}/{repo}. It printsfull_nameanddefault_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.shselects over REST.GET /issues, with pull requests dropped. A milestone title is resolved to its number through/milestones, and pages are read up toselect.limit.GET /issues/{n}./dependencies/blocked_by, using each blocker'srepository_urlandstate. That state seeds the cache, so a native edge needs no second read.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. Withselect.project.value: null, selection makes no project call.await-checks.sh,finish-issue.sh,post-merge-criteria.shandpost-review.shwork over REST, with exit codes unchanged.GET /pulls/{n}.state_reason: completed) and commented on through/issues/{n}and/issues/{n}/comments.await-review.shrequests reviews over REST only. It usesPOST /pulls/{n}/requested_reviewerswith the full…[bot]login and never callsGET /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
localrung in a cloud session is UNAVAILABLE (exit 3) with no request made.app-token.shexits 3 whenCLAUDE_CODE_REMOTE=true, and also recognises the proxy's refusal ofGET /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 /pullsgh pr edit --add-label→POST /issues/{n}/labelsgh pr diff→GET /pulls/{n}withAccept: application/vnd.github.diffgh pr view … state,autoMergeRequest→GET /pulls/{n}, reading.state,.mergedand.auto_mergegh issue comment→POST /issues/{n}/commentssetup-backlog:gh repo view,gh label list,gh pr list,gh issue list,gh issue viewand theautoMergeAllowedquery moved to REST. Project discovery stays as it is and says it works only locally.gh runstays.Access probe.
run-backlogandsetup-backlogprobe withgh api repos/{owner}/{repo} --jq .full_nameinstead ofgh auth status. Theread:projectcheck runs only for a project-scoped run, and readsX-Oauth-Scopesfrom the same REST response.Step 5 checks the pull request body instead of
closingIssuesReferences. Newscripts/closing-refs.shrequires 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 asFAILEDand the run asBLOCKED, with the pull request merged and the issue closed.Step 4 runs each verify command with a Bash
timeoutof600000.Every stub
ghrefuses GraphQL. Each one sourcesscripts/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:repository_url[bot]checkapp-token.sh's handling of the proxy refusalReverting one REST call to
gh issue viewfails five cases.Verified from a Claude Code cloud session — see Verification below.
auto-merge_test.shis 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
Changes beyond the issue
.github/scripts/verify-affected-modules.shopened withgh repo view, so step 4 would have failed in a cloud session, including the first after-merge criterion's run. It now readsgh api 'repos/{owner}/{repo}' --jq .default_branch. This is a separate commit.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 nowwas(n.{0,3}t| not). This was also the one failing case inawait-review_test.shbefore this change.finish-issue.shcloses even when its comment fails. A comment that won't post is now aWARN. The close still happens, and it is the part that matters.select.limittruncates the list, it keeps the lowest-numbered issues, which are the ones the ascending walk wants.setup-backlog's permission allow-list grants onlygh api,gh runandgh workflowamongghcommands. Those are the only ones the cycle now runs.Scanner
scan-plugin.sh plugins/backlog --target claude-code-cloud, compared withmain:mainbreaks github-graphqlselect-issue.sh, project scope only)degrades github-graphqlgh-auth-status-failsbreaks non-repository-endpointVerification
Run from Claude Code cloud session https://claude.ai/code/session_014kTbDnKA5evnEZDcCBqoFu on this branch's scripts.
Suites and verify commands
CLAUDE_CODE_REMOTE=trueset and unset:app-token,auto-merge,await-checks,await-review,closing-refs,finish-issue,post-merge-criteria,repoandselect-issue.dagger checkexits 0.bash .github/scripts/verify-affected-modules.shexits 0, reporting no daggerverse module changed.Selection and preflight
select-issue.sh --no-project-filter→ exit 0, story(kafka): add examples/go/ cookbook module showcasing kafka usage #60.select-issue.sh --issue 480→ exit 0. Its typed blocker story(hosted-agents): add the rules the backlog review had to leave as reviewer's notes #479 is closed.select-issue.sh --project-value daggerverse→ exit 4, local-only message.post-review.sh --preflight→ exit 3, naming the cloud session.closing-refs.sh 482 480→ exit 0,#480.await-checks.sh 482. Oncfa9c01it gave exit 2, 2, then 0. After the fix commit35ec7acit 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.review_requestedforCopilot,type: Bot. ButGET /pulls/{n}/requested_reviewersanswers{"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.35ec7acreads the request back from the timeline as well. A rung counts as requested while its newestreview_requestedis newer than its newestreviewedorreview_request_removed. The test stub now records a bot's request on the timeline, as GitHub does, and reverting the read fails five cases.Copilot's review comments. All three are addressed in
35ec7ac:app-token.sh'sEXITtrap now calls a function. The quoted${CLEANUP:+'…'}form does expand under bash 5, but older bash releases parse it differently.X-Oauth-Scopesprobes no longer pass a--jqthey 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-reviewercheck 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