| name | github-pr-workflow |
| description | Open and manage GitHub pull requests through Kai MCP tools — propose changes, monitor CI, iterate on failures, and merge. No git tokens are shared to the sandbox; every GitHub operation goes through the backend via the workspace's GitHub App installation. |
| version | 2.0.0 |
| author | kai-agent |
| license | MIT |
| metadata | {"kai":{"tags":["GitHub","Pull-Requests","CI/CD","MCP","Automation"],"related_skills":["github-issues","github-code-review"]}} |
GitHub Pull Request Workflow
How to ship changes as pull requests from inside the Kai sandbox.
Critical constraint — no git in the sandbox
You have no GitHub credentials in this sandbox. No GITHUB_TOKEN, no gh auth, no SSH keys, no git remote access. Do not try to git clone, git push, gh pr create, or curl https://api.github.com/... directly — none of it will work, and attempting it leaks failure noise into the trajectory.
Every GitHub operation goes through MCP tools. The backend authenticates as the workspace's GitHub App installation on your behalf. Your job is to compose the change (read the files, build the diff, write the description) and invoke the tool — the backend does the git work.
If the MCP PR tools aren't listed in your available tool surface yet, stop. Tell the user: "I can't open a PR from this workspace — the kai_create_pull_request tool isn't available. That means either the backend hasn't shipped it yet (see kai-backend#344) or the GitHub App installation is missing. Let me file the finding as an issue via kai_create_github_issue instead, or outline the fix in a lifecycle action for you to pick up manually." Do not fake a PR by constructing markdown that looks like one.
Prerequisites
- Workspace has the repo connected (check with
kai_list_repositories)
- GitHub App installation is active and has permissions:
Contents: Read & Write, Pull requests: Read & Write, Metadata: Read
- You know why you're opening the PR — typically because a
lifecycle_action is in approved or in_progress state and you've been told to execute it
- Activate the
integrations MCP category first (once per session): kai_activate_category(category="integrations"). All GitHub write + read tools (kai_create_pull_request, kai_update_pull_request_branch, kai_list_pull_request_checks, kai_merge_pull_request, kai_create_github_issue, kai_list_pull_requests, kai_list_commits, etc.) live in that on-demand category. Skip activation and calls fail with "unknown tool".
Workflow
1. Understand the change
Before touching any file, answer three questions:
- What's the goal? One sentence. "Fix the reachable CVE-2022-23529 in payment-service's webhook handler."
- What's the scope? Which files, which lines, what's the blast radius? Read surrounding code via
kai_read_repository_files so you don't break things unrelated.
- Is there an originating action? Look it up with
kai_lifecycle_actions_list and grab the actionId. You'll link the PR back to it so the Brief page shows the full chain.
If you can't answer all three, you're not ready — stop and figure them out before proposing changes.
2. Read current state
kai_browse_repository_files(workspaceId, repoId, path="src/auth")
kai_read_repository_files(workspaceId, repoId, paths=["src/auth/webhook.ts", "package.json", "tests/auth/webhook.test.ts"])
Read enough to understand the change, not the whole repo. For each file you'll modify, read it first and build the new content in your head (or in a scratch variable). For files you need to create, you still need to read the directory structure and neighboring files to match conventions.
3. Compose the diff
kai_create_pull_request takes structured changes — a list of {path, operation, content} entries. No patches, no hunks. You provide the full target content of each file.
Three operations:
create — new file. Provide full content.
update — existing file. Provide full new content. The backend diffs against the current base.
delete — remove a file. No content.
Size budget: keep the payload under ~1 MB. Very large PRs should be split.
4. Link to the originating action
If a lifecycle_action triggered this work, include it in linkedItems:
linkedItems=[
{
"platform": "kai",
"externalId": actionId,
"url": f"kai://actions/{actionId}",
"title": "Upgrade jsonwebtoken to 9.0.0"
}
]
Add GitHub issues the PR closes:
linkedItems=[
{"platform": "github", "externalId": "acme/payment-service#142", "url": "...", "title": "CVE-2022-23529"},
{"platform": "kai", "externalId": actionId, "url": f"kai://actions/{actionId}"}
]
Also include Closes #142 in the PR body so GitHub auto-closes the issue on merge.
5. Open the PR
kai_create_pull_request(
workspaceId=...,
repoId=...,
base="main",
branchName="fix/cve-2022-23529-jwt-upgrade",
title="fix: upgrade jsonwebtoken to 9.0.0 (closes CVE-2022-23529)",
body="""## Summary
Upgrades jsonwebtoken from 8.5.1 → 9.0.0 to fix CVE-2022-23529 (alg=none spoof reachable via Stripe webhook handler).
Verified exploit no longer triggers against the new version. Token verification path unchanged.
## Test plan
- [x] Unit tests: `bun test tests/auth/webhook.test.ts` — 12/12 pass
- [x] Exploit replay against main: throws `JsonWebTokenError` as expected
- [ ] Manual webhook signature check in staging after deploy
Closes #142
""",
changes=[
{"path": "package.json", "operation": "update", "content": PKG_JSON_NEW},
{"path": "src/auth/webhook.ts", "operation": "update", "content": WEBHOOK_TS_NEW},
{"path": "tests/auth/webhook.test.ts","operation": "update", "content": TEST_TS_NEW},
],
commitMessage="fix: upgrade jsonwebtoken to 9.0.0",
draft=False,
labels=["security", "dependencies"],
reviewers=["emir"], # from blueprint team roster
linkedItems=[
{"platform": "github", "externalId": "acme/payment-service#142", "url": "...", "title": "CVE-2022-23529"},
{"platform": "kai", "externalId": actionId, "url": f"kai://actions/{actionId}"}
],
)
→ { number: 147, url: "https://github.com/acme/payment-service/pull/147", branch: "fix/cve-...", commitSha: "abc123", state: "open" }
The tool creates the branch, commits the changes, opens the PR, applies labels/assignees/reviewers, and (if the workspace is wired for it) auto-updates the linked lifecycle_action to status=remediating. One call, one transaction.
6. Monitor CI
Wait for checks to run, then:
kai_list_pull_request_checks(workspaceId, repoId, number=147)
→ [
{name: "lint", status: "completed", conclusion: "success"},
{name: "test", status: "completed", conclusion: "failure", detailsUrl: "..."},
{name: "typecheck",status: "in_progress"}
]
If any checks are still in_progress, don't block the trajectory — either return control to the user with "checks in flight, will re-check in N minutes" and schedule a cron wake-up, or move on to other work and revisit.
7. Handle failures
When a check is failure:
- Fetch the failing job's logs via
kai_get_pull_request_check_logs(..., checkRunId=<id>) (if available) — or, if only the detailsUrl is exposed, tell the user to open it and share what's there.
- Read the log, identify the root cause. Don't guess.
- Compose the fix the same way you composed the original PR (steps 2-3).
- Ship the fix to the same branch via
kai_update_pull_request_branch(workspaceId, repoId, number=147, changes=[...], commitMessage="fix: ...") — this is a follow-up commit on the existing branch, not a new PR.
- Re-check CI.
Iterate limits:
- Max 3 auto-fix attempts. If still red after 3, stop and hand off to the user with a clear summary of what you tried.
- Never force-push. Always additive commits.
- Never modify test expectations to pass — fix the code.
8. Merge
When CI is green and reviews (if required) are approved:
kai_merge_pull_request(
workspaceId, repoId,
number=147,
method="squash", # "merge" | "squash" | "rebase"
commitTitle="fix: upgrade jsonwebtoken to 9.0.0 (#147)",
commitMessage="Closes CVE-2022-23529. Verified exploit no longer reachable.",
)
Don't merge unilaterally on critical/user-facing paths. For most changes, wait for human review or approval unless the lifecycle_action explicitly granted auto-merge authority. A green CI is not the same as "reviewed."
After merge, the backend updates the linked lifecycle_action to status=resolved (assuming webhook handler is live) and the briefing surfaces "Resolved today" in the next update.
Examples
Single-file typo fix (trivial)
kai_create_pull_request(
workspaceId=..., repoId=...,
base="main",
branchName="chore/fix-readme-typo",
title="docs: fix typo in README.md",
body="Typo: 'recieve' → 'receive'.",
changes=[{"path": "README.md", "operation": "update", "content": FIXED_README}],
labels=["docs"],
)
Multi-file refactor
kai_create_pull_request(
...,
branchName="refactor/extract-auth-middleware",
title="refactor: extract auth middleware into shared module",
body="...",
changes=[
{"path": "src/middleware/auth.ts", "operation": "create", "content": NEW_MIDDLEWARE},
{"path": "src/routes/users.ts", "operation": "update", "content": UPDATED_USERS},
{"path": "src/routes/admin.ts", "operation": "update", "content": UPDATED_ADMIN},
{"path": "src/legacy/auth-inline.ts", "operation": "delete"},
],
commitMessage="refactor: extract shared auth middleware",
reviewers=["batuhan"],
)
Deleting a file
changes=[{"path": "scripts/old-migration-2023.py", "operation": "delete"}]
No content field when operation is delete.
Reading PR state (no creation)
Daily-cycle and self-onboard skills use these to map what the team is working on:
kai_list_pull_requests(workspaceId, repoId, state="open", limit=30)
→ [{number, title, state, author, createdAt, updatedAt, labels, url}, ...]
kai_get_pull_request(workspaceId, repoId, number)
→ full PR detail including changed files, reviews, mergeable state
Same for issues via kai_list_github_issues / kai_get_github_issue.
Anti-patterns
- Trying to run
gh or git in the terminal. They will not work. No credentials in sandbox.
- Faking PRs with markdown. If the tool isn't available, say so plainly and offer a fallback (file an issue, create a lifecycle action with the plan).
- Force-pushing to fix CI.
kai_update_pull_request_branch is additive only. If you genuinely need to rewrite history, stop and ask the user.
- Merging without review on behalf of the team. Default to waiting for a human. Auto-merge only when the originating action explicitly authorized it.
- Over-scoping PRs. One change per PR. If the fix needs to touch 20 files across 4 unrelated concerns, split it — 4 PRs always beats 1 giant PR that nobody can review.
- Trying to edit
.github/workflows/*. The Kai GitHub App does not hold Workflows: Write permission. Any PR that includes a workflow file will be rejected by the GitHub API with a 422. If a fix requires a workflow change, file a standalone issue via kai_create_github_issue describing the exact edit needed and link it to the main remediation PR — don't silently drop the workflow part and don't try to include it.
- Not linking to the originating action. Every PR Kai opens should trace back to a
lifecycle_action via linkedItems. This is how the dashboard shows the full chain from finding → action → remediation → merge.
Tool reference (pending kai-backend#344)
The following MCP tools are required for this skill. If any are missing, the skill can't complete — flag the gap and file an issue rather than improvising.
| Tool | Purpose |
|---|
kai_create_pull_request | Open a PR from structured changes |
kai_update_pull_request_branch | Add commits to an existing PR branch |
kai_list_pull_requests | Enumerate open / closed / merged PRs |
kai_get_pull_request | Full PR detail |
kai_list_pull_request_checks | CI status per PR |
kai_merge_pull_request | Merge by squash / merge / rebase |
kai_create_pull_request_comment | Post a review comment |
kai_create_github_issue | File a general-purpose issue (not security-scoped) |
kai_list_github_issues / kai_get_github_issue | Read issue state |
kai_list_commits | Recent commits with author/date/files (used by self-onboard, daily-cycle) |
Until they land on the backend, any skill that tries to open a PR from within a sandbox session should follow the "stop and flag" pattern at the top of this file.