Skip to content

Instantly share code, notes, and snippets.

@pamelafox
Created October 6, 2026 18:38
Show Gist options
  • Select an option

  • Save pamelafox/dd6c3ad66933105affcaf838f6a02f92 to your computer and use it in GitHub Desktop.

Select an option

Save pamelafox/dd6c3ad66933105affcaf838f6a02f92 to your computer and use it in GitHub Desktop.
Extension for review-changed workflow
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