Created
October 6, 2026 18:38
-
-
Save pamelafox/dd6c3ad66933105affcaf838f6a02f92 to your computer and use it in GitHub Desktop.
Extension for review-changed workflow
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
| import { defineWorkflow, joinSession } from "@github/copilot-sdk/extension"; | |
| const workflow = defineWorkflow({ | |
| meta: { | |
| "name": "review-changed", | |
| "description": "Lists changed files in the current git repo, has a subagent review each file for bugs, and summarizes the findings. args (all optional): { base?: string — git ref to diff against (default: merge-base with origin/main or main), files?: string[] — explicit paths to review instead of auto-detecting, cwd?: string — repo directory (default: process cwd) }.", | |
| "phases": [ | |
| { | |
| "title": "List changes", | |
| "detail": "Detect changed files via git" | |
| }, | |
| { | |
| "title": "Review", | |
| "detail": "One reviewer subagent per changed file" | |
| }, | |
| { | |
| "title": "Summarize", | |
| "detail": "Aggregate findings into a report" | |
| } | |
| ], | |
| "argsSchema": { | |
| "type": "object", | |
| "properties": { | |
| "base": { | |
| "type": "string" | |
| }, | |
| "files": { | |
| "type": "array", | |
| "items": { | |
| "type": "string" | |
| } | |
| }, | |
| "cwd": { | |
| "type": "string" | |
| } | |
| } | |
| } | |
| }, | |
| run: ( | |
| async (ctx) => { | |
| const { execFile } = await import("node:child_process"); | |
| const { promisify } = await import("node:util"); | |
| const exec = promisify(execFile); | |
| const args = ctx.args && typeof ctx.args === "object" ? ctx.args : {}; | |
| const cwd = typeof args.cwd === "string" && args.cwd ? args.cwd : process.cwd(); | |
| const git = async (gitArgs) => { | |
| try { | |
| const { stdout } = await exec("git", gitArgs, { cwd, signal: ctx.signal, maxBuffer: 16 * 1024 * 1024 }); | |
| return stdout; | |
| } catch (e) { | |
| return null; | |
| } | |
| }; | |
| ctx.phase("List changes"); | |
| const listing = await ctx.step("list-changed-v1", async () => { | |
| if (Array.isArray(args.files) && args.files.length > 0) { | |
| return { base: null, files: args.files.filter((f) => typeof f === "string" && f) }; | |
| } | |
| let base = typeof args.base === "string" && args.base ? args.base : null; | |
| if (!base) { | |
| for (const candidate of ["origin/main", "main", "origin/master", "master"]) { | |
| const mb = await git(["merge-base", "HEAD", candidate]); | |
| if (mb && mb.trim()) { base = mb.trim(); break; } | |
| } | |
| } | |
| const set = new Set(); | |
| const add = (out) => { | |
| if (!out) return; | |
| for (const line of out.split("\n")) { const p = line.trim(); if (p) set.add(p); } | |
| }; | |
| if (base) add(await git(["diff", "--name-only", "--diff-filter=ACMR", base])); | |
| add(await git(["diff", "--name-only", "--diff-filter=ACMR", "HEAD"])); | |
| add(await git(["ls-files", "--others", "--exclude-standard"])); | |
| return { base, files: [...set].sort() }; | |
| }); | |
| const files = listing.files; | |
| ctx.log(`Found ${files.length} changed file(s)${listing.base ? ` vs ${listing.base.slice(0, 12)}` : ""}.`); | |
| if (files.length === 0) { | |
| return { base: listing.base, files: [], reviews: [], summary: "No changed files to review." }; | |
| } | |
| const findingSchema = { | |
| type: "object", | |
| required: ["file", "findings"], | |
| properties: { | |
| file: { type: "string" }, | |
| findings: { | |
| type: "array", | |
| items: { | |
| type: "object", | |
| required: ["severity", "title", "explanation"], | |
| properties: { | |
| severity: { type: "string", enum: ["critical", "high", "medium", "low"] }, | |
| line: { type: ["integer", "null"] }, | |
| title: { type: "string" }, | |
| explanation: { type: "string" } | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| ctx.phase("Review"); | |
| const diffBase = listing.base ? listing.base : "HEAD"; | |
| const reviews = await ctx.parallel(files.map((file) => async () => { | |
| const prompt = [ | |
| `You are reviewing a changed file in the git repository at ${cwd}.`, | |
| `File: ${file}`, | |
| `See the change with: git -C "${cwd}" diff ${diffBase} -- "${file}" (if empty, the file may be untracked; read it directly).`, | |
| "Read the surrounding code as needed. Report only high-confidence, meaningful issues: bugs, logic errors, security problems, broken behavior, or missing error handling that would matter in practice. Ignore style nits and formatting.", | |
| "Do not modify any files.", | |
| `Return JSON with "file" set to "${file}" and "findings" as an array (empty if nothing significant). Each finding has severity (critical|high|medium|low), line (integer or null), title, and explanation including a suggested fix.` | |
| ].join("\n"); | |
| const result = await ctx.agent(prompt, { label: `review:${file}`, schema: findingSchema }); | |
| if (result === null) { | |
| ctx.log(`Review failed for ${file}`); | |
| return { file, findings: [], failed: true }; | |
| } | |
| return { file, findings: Array.isArray(result.findings) ? result.findings : [], failed: false }; | |
| })); | |
| const completed = reviews.map((r, i) => r !== null ? r : { file: files[i], findings: [], failed: true }); | |
| ctx.phase("Summarize"); | |
| const rank = { critical: 0, high: 1, medium: 2, low: 3 }; | |
| const all = completed.flatMap((r) => r.findings.map((f) => ({ file: r.file, ...f }))) | |
| .sort((a, b) => (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9)); | |
| const counts = { critical: 0, high: 0, medium: 0, low: 0 }; | |
| for (const f of all) if (f.severity in counts) counts[f.severity]++; | |
| const failedFiles = completed.filter((r) => r.failed).map((r) => r.file); | |
| let summary = null; | |
| if (all.length > 0) { | |
| summary = await ctx.agent( | |
| "Summarize these code review findings for a developer in a concise markdown report: a one-paragraph overview, then the most important issues grouped by severity with file:line references. Do not invent issues beyond those listed. Do not use any tools.\n\n" + | |
| JSON.stringify(all, null, 2), | |
| { label: "summarize" } | |
| ); | |
| } | |
| if (!summary) { | |
| summary = all.length === 0 | |
| ? `Reviewed ${files.length} file(s); no significant issues found.` | |
| : all.map((f) => `- [${f.severity}] ${f.file}${f.line ? `:${f.line}` : ""} — ${f.title}`).join("\n"); | |
| } | |
| if (failedFiles.length) ctx.log(`Reviews failed for: ${failedFiles.join(", ")}`); | |
| return { base: listing.base, files, counts, findings: all, failedFiles, summary }; | |
| } | |
| ), | |
| }); | |
| await joinSession({ workflows: [workflow] }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment