Copilot orchestration
Skill gamedevpl/www.gamedev.pl/.claude/skills/copilot-orchestration
Delegate work to GitHub Copilot's remote coding agent — either via the Agent tasks REST API (no issue needed; prefer this when scripting) or by assigning an issue to the Copilot bot — then verify and merge what comes back. Use when offloading well-specified, self-contained coding tasks to run remotely instead of doing them locally, when driving or polling Copilot sessions programmatically, or when choosing what is and isn't suitable to delegate.From its SKILL.md
npx -y skills add gamedevpl/www.gamedev.pl --skill copilot-orchestrationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
20.9 KB, as published. Nobody here has run it
Orchestrating remote GitHub Copilot coding sessions
Copilot's coding agent runs on GitHub's infrastructure, not yours. There are two ways to dispatch work, and the newer one is better for anything programmatic:
- Agent tasks REST API (
POST /agents/repos/{owner}/{repo}/tasks) — no issue, no label, no bot assignment. Prefer this whenever you are scripting. See the Agent tasks API section. - Assigning an issue to the Copilot bot — the original path, still fine when a human issue is wanted as the artifact. Documented from The dispatch procedure onward.
Either way it is the cheapest way to get real work done without consuming local model quota — but only for the right tasks, and only if you verify what comes back.
When to delegate (and when not to)
Delegate when the task is:
- Self-contained code with a local test oracle. A green gate (
lint/type-check/test/build) is what makes a returned PR verifiable without trusting it. - Well-specified. You can write down the files, the contract, and what "done" means.
- Additive, not architecture-defining.
Do not delegate when:
- The hard part is empirical — needing real credentials, a running daemon, Docker, external services, or iterating against live behaviour. Copilot's sandbox can't do this, so it returns confident, plausible, never-executed code. Worst outcome for infra work.
- Secrets are involved. Never route credential handling through an autonomous PR agent.
- It's a security boundary or a core architectural decision. Do those yourself.
Rule of thumb: if verification requires a human judgement call rather than a passing test suite, keep it.
The Agent tasks REST API (prefer this when scripting)
Public preview, REST API version 2026-03-10. Verified against gamedevpl on 2026-07-29
— every behaviour below was observed, not read off the docs, and where the two disagree the
observation is noted.
curl -sS -X POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
-H "Authorization: Bearer $PAT" \
"https://api.github.com/agents/repos/OWNER/REPO/tasks" \
--data '{"prompt":"...","base_ref":"main","create_pull_request":false}'
Five endpoints: POST /agents/repos/{o}/{r}/tasks (start), GET the same path (list, filter
by state/since/creator_id/is_archived), GET .../tasks/{task_id} (one task with
sessions[]), plus GET /agents/tasks and GET /agents/tasks/{task_id} across all repos.
POST body: prompt (the only required field), base_ref, head_ref,
create_pull_request, model, custom_agent.
Auth — the constraint that bites automation. User-to-server tokens only: a PAT
(fine-grained needs Agent tasks: read to list / read and write to start; classic needs
repo), an OAuth app token, or a GitHub App user token. GitHub App installation
tokens are not supported. A server-side orchestrator must therefore hold a human's token —
there is no way to run this as a pure machine identity.
The gh CLI works — verified 2026-07-31. gh's stored credential is an OAuth app
user-to-server token, and a six-task dispatch (the seed-spike A/B) ran entirely through
gh api -H "X-GitHub-Api-Version: 2026-03-10" agents/repos/{o}/{r}/tasks with no PAT.
For interactive/local scripting prefer it: auth is handled for you and no token touches a
shell variable. Pass the POST body with --input file.json (not -f flags) so
create_pull_request stays a real boolean.
Licensing. Available on all paid Copilot plans; the agent-tasks API reached Pro/Pro+/Max
in June 2026. The REST reference still carries a stale line restricting POST to
Business/Enterprise — it is wrong; a personal paid plan works. No Enterprise seat needed.
Gotchas, all observed
- ⚠️
create_pull_requestdoes NOT default tofalse. The docs say it does. Omitting it opened a PR; setting it explicitly tofalsedid not. Always send it explicitly. - ⚠️
head_refonly works when that branch has an OPEN PR. The docs' phrasing is literal — it "looks up open PR context forhead_reftargetingbase_ref". With no open PR the parameter is silently ignored and the agent branches fresh frombase_ref; you get no error, just work on the wrong branch. Always read the branch back from the response's artifacts instead of assuming the one you asked for. To do follow-up rounds on one branch, make sure a PR is open on it first (create it yourself ifcreate_pull_requestwasfalse). With a PR present, resumption is reliable: verified appending to the same branch and the same PR id, while still sendingcreate_pull_request: false— that flag neither tears down nor refuses an existing PR, so no flag juggling is needed between rounds. - ⚠️ Auto model selection is not stable across tasks. With
modelomitted, two back-to-back tasks resolved toclaude-sonnet-4.6andgpt-5.4respectively. If you are comparing runs, measuring cost/latency, or want reproducibility, pinmodelexplicitly and record the namespaced string that comes back. - A follow-up is a new task, not a new session on the old one. Sessions are the within-task view; N rounds of work on one branch means N task IDs to track.
modelechoes back namespaced. Requestclaude-sonnet-4.6, read backsweagent-capi:claude-sonnet-4.6. Omitting it means auto-selection, which reports as"auto"until a session starts and then resolves to a concrete model. Don't compare request and response strings for equality.- GitHub rewrites
nameinto a human title. A prompt of "Fix any typo you find in games/x/SPEC.md…" came back as "Correcting typos in bubble pop rush specification". Useful for UI labels; do not rely onnameechoing your prompt (sessions[].promptdoes). artifacts[]is how you learn what happened. Abranchentry carries{base_ref, head_ref}; apullentry carries{id, global_id}. Both appear only once the task has progressed — a freshly-created task returnsartifacts: []andsession_count: 0. PollGET .../tasks/{id}rather than trusting the create response.- There is no cancel/stop endpoint.
cancelledexists as a state, but the API is create/list/get only; stopping is a UI action that ends the underlying Actions run. Plan for cooperative cancellation (tell the agent to stop through whatever channel it reads) plus discarding the output. custom_agentnames a.github/agents/<name>.agent.mdfile in the repo — the clean way to pin conventions and read-only boundaries for scripted dispatch.
States: queued, in_progress, completed, failed, idle, waiting_for_user,
timed_out, cancelled. This is a far better completion signal than the [WIP]-title
heuristic below — use it whenever you have the task ID.
Timing observed: a trivial one-file task went queued → completed in ≈4.5 minutes.
A freshly created task often returns artifacts: [] and session_count: 0 while still
queued — the session has been accepted but not started. gamedev.pl Studio maps that
stretch to job phase dispatched and only advances to building on in_progress
(see BYOCA skill "Platform session boot").
Dumping a session's generation log
Verified 2026-08-01. There are two sources at different fidelities. Reach for the task page first — the Actions log is a fallback for when you want machine-readable timing rather than what the agent actually saw.
GET /agents/.../sessions/{id}/logs does not exist on api.github.com (404); the endpoint
the runner posts to is https://api.individual.githubcopilot.com/agents/sessions/{id}/logs,
which wants a Copilot token, not a PAT. gh api /copilot_internal/v2/token did not yield one.
The full transcript — the task page
https://github.com/<owner>/<repo>/tasks/<id> has prose, full commands, complete stdout and
real exit codes. ⚠️ It is collapsed, and text-extraction tools miss it entirely. Each step
is a separate aria-expanded="false" button and there are no <details> elements, so
get_page_text returns only the prompt and the file diff — it reads as "this task has no
transcript at all", which is what led one session to wrongly conclude none existed. Expand
first:
document.querySelectorAll('button[aria-expanded="false"]').forEach((b) => b.click());
On a 136-turn session that took document.body.innerText from ~10 KB to ~200 KB. It
carries what the Actions log lacks: the step titles the agent wrote for itself ("Find where
unknown music error comes from"), every untruncated command, complete stdout, and
<shellId: … completed with exit code N> — the real pass/fail. Dump it to disk with a Blob
download rather than reading 200 KB back through a tool result.
Two traps: the browser extension blocks returned slices containing long token-like strings,
so redact (replace(/[A-Za-z0-9_-]{40,}/g,'[REDACTED]')) before returning any text overlapping
a prompt that carries a build token. And this page is the only copy — nothing on
api.github.com serves it.
The metadata transcript — the Actions run
gh api "repos/OWNER/REPO/actions/runs?created=YYYY-MM-DD" \
--jq '.workflow_runs[] | select(.name=="Running Copilot cloud agent") | "\(.id) \(.head_branch)"'
gh api "repos/OWNER/REPO/actions/runs/RUN_ID/logs" > logs.zip && unzip -q logs.zip -d run-logs
Match the run by head_branch against the task's artifacts[].data.head_ref. 0_copilot.txt
holds the whole session: setup, the firewall rule dump, and — from the line
[cca-engine] Created new session <id> onward — a turn-by-turn event stream:
| event | carries |
|---|---|
turn=N assistant.usage | model, cumulative input=, output= tokens for that turn |
turn=N assistant.message | char count only, plus the number of tool calls |
turn=N tool.execution_start | tool name, and for bash the command truncated at ≈120 chars |
turn=N tool.execution_complete | success=true / success=false — ⚠️ means bash launched, not that the command exited 0 |
turn=N user.message / session.idle | prompt/turn boundaries |
PR: / Sent pr_summary | final title and description lengths |
So the Actions log is a metadata transcript, not a transcript: the shape of the run, never the model's prose or the tool output. Use it when you want timing, token spend per turn, or a machine-readable tool sequence. Strip timestamps and ANSI, then filter:
sed -e 's/^[0-9T:.Z-]*Z //' -e 's/\x1b\[[0-9;]*m//g' run-logs/0_copilot.txt \
| grep -E '^\[cca-engine\] turn=' | grep -v checkQuota
Two things worth reading every time: success=false lines, and the
⚠️ Warning: I tried to connect to the following addresses, but was blocked by firewall rules
block near the end — headless-Chrome playtests routinely trip it.
Two environment traps
- Claude Code sessions cannot reach
/agents/*. The session's API proxy serves onlyrepos/{owner}/{repo}/...REST paths plus a pinned set of GraphQL operations, so these calls 403 at the proxy, not at GitHub (the error namesdocs.anthropic.com, which is the tell). Reads of session status are still available via the GitHub MCP toolget_copilot_job_status; anything else has to run outside the session. - zsh executes pasted
#comment lines.INTERACTIVE_COMMENTSis off by default in interactive zsh, so a pasted# (c) check the thingruns#as a command and reads(c)as a glob qualifier →zsh: number expected/unknown file attribute. Harmless but confusing. Give copy-paste blocks without comment lines, and don't use bash arrays (H=(-H ...)) in them either.
The dispatch procedure
(The issue-assignment path. Still valid; prefer the Agent tasks API above for scripting.)
1. Write the issue like a spec
Include: the exact scope (files/modules), the contract, what's explicitly out of scope,
the acceptance gate, and any invariants that must not change. Point at your agent
instructions file (e.g. .github/copilot-instructions.md) so conventions are picked up.
Over-specifying "out of scope" is worth it — it prevents scope creep you'd otherwise have to review out.
gh issue create --title "..." --body "..."
2. Assign to the Copilot bot — via GraphQL, not the REST API
⚠️ The REST assignees endpoint does not list the Copilot bot. Assignment goes through GraphQL. Find the bot and the issue node id, then assign:
# Find the bot (login is typically copilot-swe-agent, but resolve it rather than assuming)
gh api graphql -f query='
query { repository(owner: "OWNER", name: "REPO") {
suggestedActors(capabilities: [CAN_BE_ASSIGNED], first: 10) {
nodes { login __typename ... on Bot { id } ... on User { id } }
} } }'
# Get the issue node id
gh api graphql -f query='
query { repository(owner: "OWNER", name: "REPO") { issue(number: N) { id } } }'
# Assign (include yourself too if you want to stay assigned)
gh api graphql -f query='
mutation { replaceActorsForAssignable(
input: {assignableId: "ISSUE_NODE_ID", actorIds: ["BOT_ID"]}
) { assignable { ... on Issue { number assignees(first: 5) { nodes { login } } } } } }'
To re-trigger a fresh session on the same issue, unassign then reassign.
3. ⚠️ Confirm the base branch — the biggest gotcha
Now avoidable. Both dispatch paths accept an explicit base branch:
base_refon the Agent tasks API, and anagentAssignmentinput (targetRepositoryId,baseRef,customInstructions,customAgent,model) on thecreateIssue/updateIssue/addAssigneesToAssignable/replaceActorsForAssignableGraphQL mutations — the latter requires the headerGraphQL-Features: issues_copilot_assignment_api_support,coding_agent_model_selection. Pass the base branch explicitly and this whole class of failure disappears. The rest of this section applies when you did not.
Copilot forks from the repository's DEFAULT branch, not from whatever branch the issue text
mentions. If your work lives on a non-default branch, it will build on the wrong base — and
won't even see your .github/copilot-instructions.md if that file only exists on your branch.
Symptom: the PR recreates things that already exist, or its diff is enormous and unrelated.
Two fixes:
- Change the repo's default branch to your working branch (simplest; makes every future dispatch land correctly), or
- Invoke via an
@copilotcomment on a PR already based on the right branch.
Always verify before reviewing:
gh pr view N --json baseRefName,headRefName
If the base is wrong, close the PR — don't try to salvage it. Fix the default branch and re-dispatch.
4. Wait for it to finish
Poll patiently with real sleep between checks. Signals:
- Title initially carries a
[WIP]prefix; losing the prefix means Copilot considers it done. It may still be marked draft — draft status is not a completion signal either way. - ⚠️
gh pr list --search "<issue number>"often doesn't match, because the PR title and body may not contain the issue number. Usegh pr list --state open --json number,title,baseRefName,headRefName. - ⚠️ Branch names vary and are not a reliable identifier. Observed both
copilot/<default-branch-name>and descriptivecopilot/<task-slug>forms across runs, so match on the PR's content, not on an expected branch name. - It won't ping you. Nothing notifies you when the PR is ready, so either poll deliberately or hand the babysitting to a subagent — otherwise finished PRs sit unmerged and forgotten.
5. Verify independently — never trust the PR's own claims
GitHub's CI check on Copilot-authored PRs frequently sits in action_required (a repo/org
policy gating bot-triggered workflow runs) and therefore never actually runs. That is not
a failure — but it means there is no CI signal, so you must produce one yourself.
Note gh api .../actions/runs/<id>/approve returns 403 for these — it only applies to fork
PRs. Approving them needs the Actions UI or a settings change.
Verify in a throwaway clone outside your working directory:
git clone --branch <head-branch> --single-branch <repo-url> /tmp/pr-N-verify
cd /tmp/pr-N-verify && npm install
npm run type-check && npm run lint && npm run test && npm run build
Then review the diff against the spec:
gh pr diff N --name-only
gh pr diff N
Check: scope matches the issue, project invariants untouched, no secrets, no scope creep.
For security- or correctness-critical logic, exercise the code yourself — don't infer behaviour from the fact that its own tests pass. Import the built module in the verify clone and run the properties you actually care about against real project data. For a credential scanner that meant checking real generated bundles for false positives (the failure mode that would make it useless) rather than only that its own fixtures matched. Agent-written tests tend to assert what the implementation does, which makes them weak evidence that it does the right thing.
6. Merge
gh pr ready N # if it's still a draft
gh pr merge N --squash --delete-branch --subject "..." --body "..."
State in the merge body that you verified independently, and note the action_required quirk
so a passing-looking-but-unrun CI check isn't mistaken for real signal later.
Never verify inside your own working directory
Do not git checkout the PR branch, git merge it, or otherwise mutate the checked-out repo
you (or a parallel agent) are working in. Doing so silently reverts uncommitted work and can
land your commits on the wrong branch.
Always use a throwaway clone or worktree under /tmp. This matters even more when
multiple agents run in parallel — a shared working directory is the single biggest source of
cross-agent damage.
Delegating the babysitting itself
Watching a PR to completion is mostly waiting, so it's a good candidate for a subagent. When you do, state explicitly in its prompt:
- Verify in a throwaway clone under
/tmp; the main working directory is off-limits for any state-mutating git command. - The
action_requiredCI state is expected, not a failure. - Which invariants must not change.
- What to do if verification fails: report, don't merge.
⚠️ Be aware: a legitimate mid-task correction you send is indistinguishable from a prompt injection attempt from the subagent's perspective. A well-behaved subagent may refuse it and say so. That's correct behaviour — the fix is to avoid needing corrections (scope the prompt properly up front), not to demand compliance.
Mandatory: keep this skill current
This skill is a living record, and updating it is part of using it. Every time you run this workflow and learn something it didn't tell you, you must update this file in the same session, before you finish — not "later".
Update it when:
- A step here was wrong, stale, or incomplete — correct it, and say what the real behaviour was.
- You hit a gotcha that cost you time — add it, with the symptom you'd have recognised it
by. The default-branch trap and the
action_requiredCI state are both here because they cost a wasted PR and a confused debugging session respectively. - An API or CLI changed (GitHub's assignment mechanism especially) — record what actually works now, verified, not what you assume.
- A step turned out to be unnecessary — delete it. Stale ceremony is a cost too.
- You discovered a task type that should or shouldn't be delegated — refine that guidance with the concrete reason.
Record observed behaviour, not guesses. If you couldn't confirm something, mark it as unverified rather than stating it plainly.
Treat "I had to work something out that this skill should have told me" as a defect in the skill. Fixing it is not optional.