Repository navigation
Expand file tree
/
Copy pathcouncil.program.ts
More file actions
196 lines (167 loc) · 6.66 KB
/
Copy pathcouncil.program.ts
File metadata and controls
196 lines (167 loc) · 6.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/**
* @name council
* @description Ensemble code review: fans the same read-only review out to one reviewer per model (caller-supplied list of models), running them in parallel, then synthesizes all reviews into one report — deduplicating findings, taking the higher severity on conflicts, preserving unique findings, and hiding which model found what.
*/
// --- Schemas (top-level, outside the program) ---
const Severity = z.enum(["critical", "medium", "low"])
const LineRef = z.union([z.string(), z.number()]).optional()
const Confidence = z.union([z.number(), z.string()]).optional()
const Issue = z.object({
severity: Severity,
confidence: Confidence,
file: z.string().default(""),
line: LineRef,
description: z.string(),
})
const ReviewOutput = z.object({
summary: z.string(),
issues: z.array(Issue).default([]),
suggestions: z.array(z.string()).default([]),
verdict: z.string(),
})
const SynthIssue = z.object({
file: z.string().default(""),
line: LineRef,
description: z.string(),
confidence: Confidence,
})
const SynthesisOutput = z.object({
summary: z.string(),
critical: z.array(SynthIssue).default([]),
medium: z.array(SynthIssue).default([]),
low: z.array(SynthIssue).default([]),
suggestions: z.array(z.string()).default([]),
verdict: z.string(),
})
// --- Prompt builders (top-level, outside the program) ---
const reviewerPrompt = (target: string, baseRef: string | null, instructions: string | null) => `You are one reviewer on a code-review council. Perform a THOROUGH, READ-ONLY code review.
Review target: ${target}
${baseRef ? `Diff base: review the changes relative to \`${baseRef}\` — inspect the real diff of the current state against ${baseRef}.` : ""}
${instructions ? `Focus / instructions: ${instructions}` : ""}
Rules:
- You are READ-ONLY. Do NOT modify, stage, or commit any files — investigation only.
- Inspect the ACTUAL code/diff before judging: read the relevant files and the real diff; never review from assumptions.
- Be thorough but calibrated. Report real issues only; avoid nitpicks, pure-style comments, and duplicate findings.
- Cover at least: bugs, security, performance, error handling, test coverage, and API design.
Report via the structured output:
- summary: overall assessment of what you reviewed.
- issues: each with severity ("critical" | "medium" | "low"), confidence (0..1), file, line (a line number or range as a string; "" if not applicable), and a clear description.
- suggestions: concrete improvements that are not themselves issues.
- verdict: a short overall verdict.`
const synthesizerPrompt = (target: string, reviewsJson: string) => `You are the synthesizer for a code-review council. Several independent reviewers each produced a JSON review of the SAME target. Merge them into ONE coherent report.
Review target: ${target}
Synthesis rules:
- Deduplicate identical or substantially-overlapping findings into a single entry.
- When reviewers disagree on the severity of the same issue, use the HIGHER severity.
- Preserve unique findings that only one reviewer raised.
- Do NOT mention which reviewer or model found which issue — present it as one unified review.
- Group issues by severity into critical, medium, and low.
- Merge and deduplicate the suggestions too.
Reviews (anonymous, order is not meaningful):
${reviewsJson}
Report via the structured output:
- summary: overall assessment.
- critical / medium / low: arrays of issues (file, line, description, optional confidence).
- suggestions: merged, deduplicated list.
- verdict: one overall verdict for the whole council.`
// --- Program ---
const DEFAULT_MODELS = [
"anthropic/claude-sonnet-5",
"openai/gpt-5.5",
"google/gemini-3-flash-preview",
"z-ai/glm-5",
"moonshotai/kimi-k2.6",
]
type Input = {
models?: string[]
target?: string
baseRef?: string
instructions?: string
synthesisModel?: string
}
type Output = {
target: string
modelsRun: string[]
modelsFailed: { model: string; error: string }[]
summary: string
critical: z.infer<typeof SynthIssue>[]
medium: z.infer<typeof SynthIssue>[]
low: z.infer<typeof SynthIssue>[]
suggestions: string[]
verdict: string
}
export default program<Input, Output>(async (ctx) => {
const models = (ctx.input.models && ctx.input.models.length ? ctx.input.models : DEFAULT_MODELS)
.map((m) => m.trim())
.filter(Boolean)
if (!models.length) throw new Error("council program requires at least one model")
const target = ctx.input.target?.trim() || "the current working-tree changes in this repository"
const baseRef = ctx.input.baseRef?.trim() || null
const instructions = ctx.input.instructions?.trim() || null
await checkpoint({
name: "council.start",
message: `Council review across ${models.length} model(s)`,
data: { models, target },
})
// Fan out one read-only reviewer per model, in parallel. Resilient to
// individual reviewer failures (e.g. a model that can't run a read agent).
const outcomes = await Promise.all(
models.map(async (model, i) => {
try {
const handle = await spawn(`reviewer-${i + 1}`, {
type: "read",
model,
maxSteps: 40,
prompt: reviewerPrompt(target, baseRef, instructions),
output: ReviewOutput,
})
const review = await handle.result()
return { model, review }
} catch (err) {
return { model, error: err instanceof Error ? err.message : String(err) }
}
})
)
const reviews: z.infer<typeof ReviewOutput>[] = []
const modelsRun: string[] = []
const modelsFailed: { model: string; error: string }[] = []
for (const o of outcomes) {
if ("review" in o) {
reviews.push(o.review)
modelsRun.push(o.model)
} else {
modelsFailed.push({ model: o.model, error: o.error })
}
}
if (!reviews.length) {
throw new Error(
`council: all ${models.length} reviewers failed: ${modelsFailed
.map((f) => `${f.model} (${f.error})`)
.join("; ")}`
)
}
await checkpoint({
name: "council.synthesize",
message: `Synthesizing ${reviews.length} review(s)`,
data: { modelsRun, modelsFailed },
})
// Anonymize before synthesis so the synthesizer can't attribute findings to models.
const synth = await run("synthesizer", {
type: "general",
maxSteps: 30,
...(ctx.input.synthesisModel?.trim() ? { model: ctx.input.synthesisModel.trim() } : {}),
prompt: synthesizerPrompt(target, JSON.stringify(reviews, null, 2)),
output: SynthesisOutput,
})
return {
target,
modelsRun,
modelsFailed,
summary: synth.summary,
critical: synth.critical,
medium: synth.medium,
low: synth.low,
suggestions: synth.suggestions,
verdict: synth.verdict,
}
})