-
Notifications
You must be signed in to change notification settings - Fork 0
Council of Claudes: orchestrator agent for cross-persona comment dedup #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
db4f739
268b1cd
5c74a44
be7a226
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -221,6 +221,90 @@ async function reviewWithAgent({ core, title, url, token, messageText, msgId }) | |||||||||||||||||||||
| return null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // --- Orchestrator: cross-persona deduplication of inline comments --- | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — Consider extracting the “Orchestrator” block (parseClusters, applyClusters, orchestrateDedup and the ORCH_* constants) into a dedicated module (e.g., .github/workflows/scripts/orchestrator.js) and import it here. This reduces file size/complexity and makes the logic easier to unit test in isolation. If you prefer not to split files, minimally export the helpers for tests: // near the bottom, before module.exports default:
module.exports._orchestratorTest = { parseClusters, applyClusters, orchestrateDedup };This keeps production behavior unchanged while enabling direct imports in a small Node test. |
||||||||||||||||||||||
| const ORCH_URL_ENV = 'ORCHESTRATOR_AGENT_URL'; | ||||||||||||||||||||||
| const ORCH_TOKEN_ENV = 'ORCHESTRATOR_AGENT_TOKEN'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Robustly extract the orchestrator's { clusters: [...] } JSON from its response | ||||||||||||||||||||||
| // (tolerates a ```json fence — single- or multi-line — or stray prose). Returns | ||||||||||||||||||||||
| // the parsed object or null. | ||||||||||||||||||||||
| function parseClusters(text) { | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — parseClusters has several tolerant fallback paths (fenced json, substring braces, raw text). Add unit tests covering:
|
||||||||||||||||||||||
| if (!text) return null; | ||||||||||||||||||||||
| const tries = [text.trim()]; | ||||||||||||||||||||||
| const fence = text.match(/```[a-zA-Z]*\s*([\s\S]*?)```/); // handles single-line fences too | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||
| if (fence) tries.push(fence[1].trim()); | ||||||||||||||||||||||
| const first = text.indexOf('{'), last = text.lastIndexOf('}'); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Reason: simpler and less error-prone; the agent instructions can enforce JSON-only or a fenced block. |
||||||||||||||||||||||
| if (first !== -1 && last > first) tries.push(text.slice(first, last + 1)); | ||||||||||||||||||||||
| for (const t of tries) { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const obj = JSON.parse(t); | ||||||||||||||||||||||
| if (obj && Array.isArray(obj.clusters)) return obj; | ||||||||||||||||||||||
| } catch { /* try next candidate */ } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Pure decision logic: given the inline comments and the orchestrator's parsed | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| // clusters, return the SET of ids to drop. No env / network — directly unit | ||||||||||||||||||||||
| // testable. Defensive against arbitrary LLM output: honor a cluster only if its | ||||||||||||||||||||||
| // survivor is a known id; drop only known duplicate ids; keep-wins (an id that | ||||||||||||||||||||||
| // survives anywhere is never dropped); anything not mentioned is implicitly kept. | ||||||||||||||||||||||
| function applyClusters({ core, allInline, parsed }) { | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — applyClusters is pure and central to safety (omission=keep, keep-wins, unknown-id ignore). Add unit tests for:
|
||||||||||||||||||||||
| const known = new Set(allInline.map(c => c.id)); | ||||||||||||||||||||||
| const survivors = new Set(); | ||||||||||||||||||||||
| const candidates = new Set(); | ||||||||||||||||||||||
| for (const cl of parsed.clusters) { | ||||||||||||||||||||||
| // Every field here is LLM-controlled / possibly garbage — guard each access. | ||||||||||||||||||||||
| if (!cl || typeof cl.survivor !== 'string' || !known.has(cl.survivor)) continue; | ||||||||||||||||||||||
| survivors.add(cl.survivor); | ||||||||||||||||||||||
| const dups = Array.isArray(cl.duplicates) ? cl.duplicates.filter(d => known.has(d)) : []; | ||||||||||||||||||||||
| dups.forEach(d => candidates.add(d)); | ||||||||||||||||||||||
| if (dups.length) core.info(` orchestrator: keep ${cl.survivor}, drop [${dups.join(', ')}] — ${safe(cl.reason)}`); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Totally optional; up to you. |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // The prompt says each id appears at most once; if the model listed an id as | ||||||||||||||||||||||
| // both a survivor and a duplicate, keep-wins silently saves it — warn so a | ||||||||||||||||||||||
| // misbehaving orchestrator is visible rather than silently absorbed. | ||||||||||||||||||||||
| const conflicts = [...candidates].filter(id => survivors.has(id)); | ||||||||||||||||||||||
| if (conflicts.length) core.warning(`Orchestrator: ${conflicts.length} id(s) listed as both survivor and duplicate (kept via keep-wins): ${conflicts.join(', ')}`); | ||||||||||||||||||||||
| const drop = new Set([...candidates].filter(id => !survivors.has(id))); | ||||||||||||||||||||||
| core.info(`Orchestrator: dropping ${drop.size} duplicate comment(s) of ${allInline.length} inline`); | ||||||||||||||||||||||
| return drop; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Ask the orchestrator which inline comments are redundant duplicates and return | ||||||||||||||||||||||
| // the SET of comment ids to drop. Lossless by construction: returns an empty set | ||||||||||||||||||||||
| // (drop nothing → post everything) if the orchestrator is unconfigured, errors, | ||||||||||||||||||||||
| // times out, or returns anything we can't safely apply. The try/catch makes the | ||||||||||||||||||||||
| // "post everything on failure" guarantee robust even if a callee starts throwing. | ||||||||||||||||||||||
| async function orchestrateDedup({ core, allInline }) { | ||||||||||||||||||||||
| const empty = new Set(); | ||||||||||||||||||||||
| const url = process.env[ORCH_URL_ENV]; | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛡️ Security — The orchestrator URL is read from the environment and used for an outbound request without validating the scheme or host. A misconfigured URL could downgrade to plaintext HTTP or redirect to an unintended host, leaking the dedup payload and the bearer token. Mitigation: before calling
Example: const u = new URL(url);
if (u.protocol !== 'https:' || !['agents.tigera.ai'].includes(u.hostname)) {
core.warning('Orchestrator URL failed validation — skipping dedup and posting all comments.');
return empty;
} |
||||||||||||||||||||||
| const token = process.env[ORCH_TOKEN_ENV]; | ||||||||||||||||||||||
| if (!url || !token) { | ||||||||||||||||||||||
| core.info(`Orchestrator not configured (${ORCH_URL_ENV}/${ORCH_TOKEN_ENV} unset) — posting all comments`); | ||||||||||||||||||||||
| return empty; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if (allInline.length < 2) return empty; // nothing to dedupe (need ≥2 comments) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const msgId = `council-orchestrator-${process.env.GITHUB_RUN_ID}`; | ||||||||||||||||||||||
| const payload = JSON.stringify(allInline.map(c => | ||||||||||||||||||||||
| ({ id: c.id, persona: c.persona, file: c.file, line: c.line, body: c.body }))); | ||||||||||||||||||||||
| const messageText = | ||||||||||||||||||||||
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| `Identify redundant duplicate groups and return the clusters JSON per your instructions.\n\n${payload}`; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const out = await reviewWithAgent({ core, title: 'Orchestrator', url, token, messageText, msgId }); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — Please verify reviewWithAgent already has a bounded poll/timeout. If not, consider adding an explicit timeout via AbortController/fetch timeout or a max poll duration passed into reviewWithAgent, so a stuck orchestrator can’t stall posting indefinitely. Add a brief comment here pointing to the timeout semantics to aid future maintainers. |
||||||||||||||||||||||
| if (!out) { core.warning('Orchestrator: no response — posting all comments unfiltered'); return empty; } | ||||||||||||||||||||||
| const parsed = parseClusters(out); | ||||||||||||||||||||||
| if (!parsed) { core.warning('Orchestrator: unparseable response — posting all comments unfiltered'); return empty; } | ||||||||||||||||||||||
| return applyClusters({ core, allInline, parsed }); | ||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| core.warning(`Orchestrator: unexpected error (${safe(e.message)}) — posting all comments unfiltered`); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛡️ Security — On error, the code logs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
No big deal though if safe() already handles undefined cleanly. |
||||||||||||||||||||||
| return empty; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| module.exports = async ({ github, context, core }) => { | ||||||||||||||||||||||
| const { owner, repo } = context.repo; | ||||||||||||||||||||||
| const pr = context.payload.pull_request; | ||||||||||||||||||||||
|
|
@@ -296,26 +380,47 @@ module.exports = async ({ github, context, core }) => { | |||||||||||||||||||||
| const existingComments = await github.paginate(github.rest.pulls.listReviewComments, { owner, repo, pull_number }); | ||||||||||||||||||||||
| const repliedTo = new Set(existingComments.map(c => c.in_reply_to_id).filter(id => id != null)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let summaries = 0; | ||||||||||||||||||||||
| let inlineTotal = 0; | ||||||||||||||||||||||
| // 4a. Parse + classify each persona's findings. Accumulate the anchorable | ||||||||||||||||||||||
| // inline findings across ALL personas (each with a stable id) so the | ||||||||||||||||||||||
| // orchestrator can dedupe the whole set; keep summary/folded per persona. | ||||||||||||||||||||||
| const perPersona = []; | ||||||||||||||||||||||
| const allInline = []; | ||||||||||||||||||||||
| for (const res of results) { | ||||||||||||||||||||||
| if (!res) continue; | ||||||||||||||||||||||
| const p = res.persona; | ||||||||||||||||||||||
| // Persona badge: a custom avatar image if provided, else the emoji. | ||||||||||||||||||||||
| const badge = p.image ? `<img src="${p.image}" width="20" align="top" alt="${p.title}">` : p.emoji; | ||||||||||||||||||||||
| const inlineMarker = `<!-- council-of-claudes:inline:${p.key} -->`; | ||||||||||||||||||||||
| const { summary, findings } = parseFindings(res.review); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Split findings into anchorable vs. unanchorable (folded into summary). | ||||||||||||||||||||||
| const folded = []; | ||||||||||||||||||||||
| const anchorable = []; | ||||||||||||||||||||||
| for (const f of findings) { | ||||||||||||||||||||||
| if (anchors.get(f.file)?.has(f.line)) anchorable.push(f); | ||||||||||||||||||||||
| else folded.push(f); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| findings.forEach((f, i) => { | ||||||||||||||||||||||
| if (anchors.get(f.file)?.has(f.line)) { | ||||||||||||||||||||||
| // Run-scoped ephemeral id: generated and consumed within this single | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — Great inline rationale for ephemeral ids. For maintainability, add a brief note that these ids are intentionally not correlated with GitHub review comment IDs and exist only within a single run to drive orchestrator decisions. This helps future refactors avoid accidentally persisting or reusing them across runs. |
||||||||||||||||||||||
| // invocation (sent to the orchestrator, matched against its drop set). | ||||||||||||||||||||||
| // `i` is the index into `findings` (gaps where entries were folded are | ||||||||||||||||||||||
| // harmless — ids only need per-persona uniqueness); never persisted, so | ||||||||||||||||||||||
| // it must not be correlated across runs or against existingComments. | ||||||||||||||||||||||
| const id = `${p.key}-${i}`; | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| anchorable.push({ id, ...f }); | ||||||||||||||||||||||
| allInline.push({ id, persona: p.title, file: f.file, line: f.line, body: f.body }); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| folded.push(f); // no valid line anchor → goes to the summary | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| perPersona.push({ p, badge, inlineMarker, summary, folded, anchorable }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // 4a. Inline: delete this persona's prior comments that have NO replies | ||||||||||||||||||||||
| // (preserve any thread with discussion), then post the fresh set. | ||||||||||||||||||||||
| // 4b. Orchestrator: dedupe the combined inline set → ids to drop. Lossless on | ||||||||||||||||||||||
| // any failure / unconfigured (returns empty → post everything). | ||||||||||||||||||||||
| const drop = await orchestrateDedup({ core, allInline }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // 4c. Post per persona: delete prior no-reply inline, post surviving inline | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — Now that dedup happens globally, consider logging a short per-persona summary in addition to the global dropped count (e.g., “Correctness: posted X, dropped Y”), to make it easier to spot if one persona is consistently being deduped away. This is optional, but it can help tune prompt-level levers later. |
||||||||||||||||||||||
| // (skipping orchestrator-dropped duplicates), then the sticky summary. | ||||||||||||||||||||||
| let summaries = 0; | ||||||||||||||||||||||
| let inlineTotal = 0; | ||||||||||||||||||||||
| let dropped = 0; | ||||||||||||||||||||||
| for (const { p, badge, inlineMarker, summary, folded, anchorable } of perPersona) { | ||||||||||||||||||||||
| const priorInline = existingComments.filter(c => (c.body || '').includes(inlineMarker)); | ||||||||||||||||||||||
| for (const c of priorInline) { | ||||||||||||||||||||||
| if (repliedTo.has(c.id)) continue; // keep — has a discussion thread | ||||||||||||||||||||||
|
|
@@ -327,6 +432,7 @@ module.exports = async ({ github, context, core }) => { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| let inline = 0; | ||||||||||||||||||||||
| for (const f of anchorable) { | ||||||||||||||||||||||
| if (drop.has(f.id)) { dropped++; continue; } // redundant — removed by orchestrator | ||||||||||||||||||||||
| const body = `${badge} **${p.title}** — ${f.body}\n\n${inlineMarker}`; | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| await github.rest.pulls.createReviewComment({ | ||||||||||||||||||||||
|
|
@@ -341,8 +447,8 @@ module.exports = async ({ github, context, core }) => { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| inlineTotal += inline; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // 4b. Summary: sticky review, updated in place. Folded findings (no valid | ||||||||||||||||||||||
| // line anchor) are listed here so nothing is lost. | ||||||||||||||||||||||
| // Summary: sticky review, updated in place. Folded findings (no valid line | ||||||||||||||||||||||
| // anchor) are listed here so nothing is lost. | ||||||||||||||||||||||
| const marker = `<!-- council-of-claudes:${p.key} -->`; | ||||||||||||||||||||||
| let body = | ||||||||||||||||||||||
| `### ${badge} Council of Claudes — ${p.title}\n\n` + | ||||||||||||||||||||||
|
|
@@ -368,5 +474,5 @@ module.exports = async ({ github, context, core }) => { | |||||||||||||||||||||
| core.warning(`${p.title}: failed to post/update summary (${safe(e.message)})`); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| core.info(`Done: ${summaries}/${active.length} summaries, ${inlineTotal} inline comment(s) on PR #${pull_number}`); | ||||||||||||||||||||||
| core.info(`Done: ${summaries}/${active.length} summaries, ${inlineTotal} inline posted, ${dropped} duplicate(s) dropped on PR #${pull_number}`); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — Nice addition to the final summary. If you add unit tests, also assert the log string format in at least one test to detect accidental changes (people often grep CI logs for this). |
||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Review Orchestrator — Council of Claudes | ||
|
|
||
| Paste this as the `systemMessage` when creating the **orchestrator** agent in the | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — Add a simple version tag and cross-link to the JS that consumes this schema to prevent drift:
|
||
| kagent portal (https://agents.tigera.ai). Model: `claude-opus-4-8`. | ||
|
|
||
| This agent is NOT a code reviewer — it does not read the diff or generate review comments. It is a deduplication layer that runs *after* the review personas and decides which of their comments are redundant restatements of each other. | ||
|
|
||
| --- | ||
|
|
||
| You are the **deduplication orchestrator** for a multi-agent code-review system. Several independent reviewer personas have each left inline comments on one pull request. Personas frequently make the **same point** as one another (and sometimes repeat themselves), in different words. Your only job is to find those redundant groups so the system can keep one comment per point and drop the rest. | ||
|
|
||
| You do **not** judge whether a comment is correct, valuable, or nit-picky. You do **not** rewrite, summarize, or generate any comment text. You only group genuine duplicates and pick which one to keep. | ||
|
|
||
| ## Input | ||
| A JSON array of inline review comments, each: | ||
| ```json | ||
| { "id": "<id — opaque per-request token; echo it back exactly>", "persona": "<persona name>", "file": "<path>", "line": <number>, "body": "<markdown>" } | ||
| ``` | ||
|
|
||
| ## What counts as a duplicate | ||
| Two (or more) comments are duplicates **only when they make essentially the same actionable point** — the same underlying issue AND the same suggested fix/direction — even if: | ||
| - worded differently, or | ||
| - authored by different personas (or the same persona twice), or | ||
| - anchored a line or two apart. | ||
|
|
||
| These are **NOT** duplicates (keep them separate): | ||
| - Same file / function / topic but **different issues** (e.g. two distinct bugs in one function). | ||
| - A **general** observation vs. a **specific instance** of it. | ||
| - The same kind of issue (e.g. "naming") raised about **different** identifiers/locations. | ||
| - Comments that merely *relate* to each other but ask for different things. | ||
|
|
||
| When in doubt, **do not group them** — leave them separate. It is much worse to wrongly merge two distinct findings (losing one) than to leave a borderline pair unmerged. Precision over recall. | ||
|
|
||
| ## Choosing the survivor | ||
| For each duplicate group, choose exactly one **survivor**: the single **clearest, most actionable, best-articulated** comment — the one that would most help the PR author understand and fix the issue. | ||
| Choose on clarity/usefulness alone, **regardless of which persona authored it**. The other comments in the group are the duplicates to drop. | ||
|
|
||
| ## Output | ||
| Respond with **only** a JSON object (a fenced ```json block is acceptable), in exactly this shape: | ||
| ```json | ||
| { | ||
| "clusters": [ | ||
| { "survivor": "<id>", "duplicates": ["<id>", "<id>"], "reason": "<one-line gist of the shared point>" } | ||
| ] | ||
| } | ||
| ``` | ||
| Rules for the output: | ||
| - Include a cluster **only** if it has at least one duplicate (group size ≥ 2). A comment with no duplicate must **not** appear anywhere in the output — anything you don't mention is kept. | ||
| - Use the **exact** `id` values from the input. Never invent ids or return ids that weren't provided. | ||
| - Each id may appear **at most once** across the whole output (as a survivor or a duplicate, not both, and not in two clusters). | ||
| - Never include comment bodies, rewrites, or any prose outside the JSON object. | ||
| - If there are no duplicates at all, return `{ "clusters": [] }`. | ||
|
|
||
| ## Example | ||
| Input (abbreviated): | ||
| ```json | ||
| [ | ||
| {"id":"a1","persona":"Correctness","file":"x.go","line":42,"body":"This `==` is case-sensitive; use strings.EqualFold or the feature silently disables."}, | ||
| {"id":"b2","persona":"Nell","file":"x.go","line":42,"body":"Comparison is case-sensitive — should this use EqualFold?"}, | ||
| {"id":"c3","persona":"Security","file":"y.go","line":10,"body":"Interval has no lower bound; a tiny value could hammer the dataplane."} | ||
| ] | ||
| ``` | ||
| Output: | ||
| ```json | ||
| { "clusters": [ { "survivor": "a1", "duplicates": ["b2"], "reason": "case-sensitive compare should use EqualFold" } ] } | ||
| ``` | ||
| (`c3` is unique, so it is absent from the output and will be kept.) | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -65,8 +65,12 @@ echo " author: @$author human top-level review comments: $humans" | |||||||
| echo " base : ${base:0:12}" | ||||||||
| echo " head : ${head:0:12} (earliest human-reviewed commit)" | ||||||||
|
|
||||||||
| baseBr="coc-sample-$N-base" | ||||||||
| headBr="coc-sample-$N-head" | ||||||||
| # Optional iteration label (ITER=v2) → parallel duplicate PRs for the same | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 Maintainability & Tests — The ITER suffix is a helpful addition. Consider adding a one-liner to hack/council-of-claudes/README.md under the Tools section documenting usage (e.g., “Use ITER=v2 to create parallel duplicate PRs for the same sample: ITER=v2 ./gen-benchmark-pr.sh ”). This makes the knob discoverable without having to read the design doc. |
||||||||
| # original, so a new run doesn't clobber an earlier labeled baseline. Empty by | ||||||||
| # default (branches stay coc-sample-<N>-base/head). | ||||||||
| SUFFIX="${ITER:+-$ITER}" | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
If it’s already there, ignore me. |
||||||||
| baseBr="coc-sample-$N$SUFFIX-base" | ||||||||
| headBr="coc-sample-$N$SUFFIX-head" | ||||||||
|
|
||||||||
| # Resolve the fork point (merge-base) and the as-reviewed diff via the GitHub | ||||||||
| # compare API rather than local git. This works even when the PR was force-pushed | ||||||||
|
|
@@ -147,7 +151,7 @@ fi | |||||||
|
|
||||||||
| echo "Opening PR ..." | ||||||||
| gh pr create --repo "$FORK" --base "$baseBr" --head "$headBr" \ | ||||||||
| --title "[sample #$N] $title" \ | ||||||||
| --title "[sample #$N]${ITER:+ [$ITER]} $title" \ | ||||||||
| --body "Benchmark sample reproducing the **as-first-reviewed** state of [$UPREPO#$N](https://github.com/$UPREPO/pull/$N) (by @$author). | ||||||||
|
|
||||||||
| This PR's diff equals what the human reviewers first saw: fork point \`${fork:0:12}\` → earliest human-reviewed commit \`${head:0:12}\`. The original PR drew **$humans** human top-level review comments — the ground truth to compare the Council's feedback against. | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛡️ Security — The workflow introduces new secrets (ORCHESTRATOR_AGENT_URL/TOKEN). If this job can run in contexts where repository secrets are available to untrusted fork PRs, an attacker could cause outbound calls to an attacker-controlled URL or probe for environment details through log messages. Mitigation: ensure the workflow only runs in trusted contexts (e.g., not on untrusted forks) or add an explicit guard like
if: ${{ !github.event.pull_request.head.repo.fork }}. Alternatively, trigger via pull_request_target with extreme caution and strict input validation on all consumed data.