Skip to content

Commit 42fddbd

Browse files
that-github-userunknownclaude
authored
Make convergence clustering threshold configurable via --threshold (#100)
analyzeConvergence now accepts a threshold parameter (default 0.3). New --threshold CLI flag passes through RunOptions. Test verifies different thresholds produce different clustering results. Generated by thinktank Opus (5 agents, 76% consensus, all pass). Closes #65 Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a6a0da6 commit 42fddbd

6 files changed

Lines changed: 35 additions & 4 deletions

File tree

src/cli.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ program
2929
.option("--timeout <seconds>", "Timeout per agent in seconds", "300")
3030
.option("--model <model>", "Claude model to use", "sonnet")
3131
.option("-r, --runner <name>", "AI coding tool to use (default: claude-code)")
32+
.option("--threshold <number>", "Convergence clustering similarity threshold (0.0-1.0)", "0.3")
3233
.option("--verbose", "Show detailed output from each agent")
3334
.action(async (promptArg: string | undefined, opts) => {
3435
const prompt = resolvePrompt(promptArg, opts.file);
@@ -51,6 +52,12 @@ program
5152
process.exit(1);
5253
}
5354

55+
const threshold = parseFloat(opts.threshold);
56+
if (Number.isNaN(threshold) || threshold < 0 || threshold > 1) {
57+
console.error("Error: --threshold must be a number between 0.0 and 1.0");
58+
process.exit(1);
59+
}
60+
5461
const knownModels = ["sonnet", "opus", "haiku"];
5562
if (!knownModels.includes(opts.model) && !opts.model.startsWith("claude-")) {
5663
console.warn(
@@ -65,6 +72,7 @@ program
6572
testTimeout,
6673
timeout,
6774
model: opts.model,
75+
threshold,
6876
runner: opts.runner,
6977
verbose: opts.verbose ?? false,
7078
});

src/commands/run.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function makeOpts(overrides: Partial<RunOptions> = {}): RunOptions {
1010
testTimeout: 120,
1111
timeout: 300,
1212
model: "sonnet",
13+
threshold: 0.3,
1314
verbose: false,
1415
...overrides,
1516
};

src/commands/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export async function run(opts: RunOptions): Promise<void> {
126126
}
127127

128128
// Phase 4: Convergence analysis
129-
const convergence = analyzeConvergence(agents);
129+
const convergence = analyzeConvergence(agents, opts.threshold);
130130

131131
// Phase 5: Recommendation
132132
const { recommended, scores } = recommend(agents, testResults, convergence);

src/scoring/convergence.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@ describe("analyzeConvergence", () => {
7676
assert.deepEqual(groups[0]!.agents.sort(), [1, 2]);
7777
});
7878

79+
it("produces different clusters with different thresholds", () => {
80+
// Agent 1 and 2 have similar (but not identical) diffs; agent 3 is different
81+
const agents = [
82+
makeAgent({ id: 1, diff: DIFF_A, filesChanged: ["a.ts"] }),
83+
makeAgent({ id: 2, diff: DIFF_A_VARIANT, filesChanged: ["a.ts"] }),
84+
makeAgent({ id: 3, diff: DIFF_B, filesChanged: ["b.ts"] }),
85+
];
86+
87+
// Low threshold: agents 1 and 2 cluster together (Jaccard similarity = 0.5 >= 0.3)
88+
const lowGroups = analyzeConvergence(agents, 0.3);
89+
const groupWith1And2 = lowGroups.find((g) => g.agents.includes(1) && g.agents.includes(2));
90+
assert.ok(groupWith1And2, "At threshold 0.3, agents 1 and 2 should cluster together");
91+
92+
// Very high threshold: nothing clusters (similarity < 1.0 for non-identical diffs)
93+
const highGroups = analyzeConvergence(agents, 1.0);
94+
// Each agent should be in its own group since no pair has perfect similarity
95+
assert.ok(
96+
highGroups.length > lowGroups.length,
97+
`Higher threshold should produce more groups (got ${highGroups.length} vs ${lowGroups.length})`,
98+
);
99+
});
100+
79101
it("labels strong consensus correctly", () => {
80102
const agents = [
81103
makeAgent({ id: 1, diff: DIFF_A }),

src/scoring/convergence.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { pairwiseSimilarity } from "./diff-parser.js";
99
* Agents are clustered by diff similarity using single-linkage clustering
1010
* with a 0.5 similarity threshold.
1111
*/
12-
export function analyzeConvergence(agents: AgentResult[]): ConvergenceGroup[] {
12+
export function analyzeConvergence(agents: AgentResult[], threshold = 0.3): ConvergenceGroup[] {
1313
const completed = agents.filter((a) => a.status === "success" && a.diff.length > 0);
1414

1515
if (completed.length === 0) return [];
@@ -18,11 +18,10 @@ export function analyzeConvergence(agents: AgentResult[]): ConvergenceGroup[] {
1818
const similarities = pairwiseSimilarity(completed.map((a) => ({ id: a.id, diff: a.diff })));
1919

2020
// Single-linkage clustering: merge agents with similarity >= threshold
21-
const SIMILARITY_THRESHOLD = 0.3;
2221
const clusters = clusterAgents(
2322
completed.map((a) => a.id),
2423
similarities,
25-
SIMILARITY_THRESHOLD,
24+
threshold,
2625
);
2726

2827
// Convert clusters to convergence groups

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface RunOptions {
55
testTimeout: number;
66
timeout: number;
77
model: string;
8+
threshold: number;
89
verbose: boolean;
910
runner?: string;
1011
}

0 commit comments

Comments
 (0)