fix(research): the contrarian runs as a second wave with the leading answer to attack

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-23 15:23:18 +02:00
commit b16e39ccd9
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 104 additions and 15 deletions

View file

@ -1199,3 +1199,60 @@ test('[#102] sibling: commands/trekexecute.md names project-dir and repo-root ca
assert.doesNotMatch(text, /plugin-root candidates/);
assert.equal((text.match(/project-dir and repo-root candidates/g) || []).length, 2);
});
// --- #113: the contrarian gets a conclusion to attack -----------------------
// contrarian-researcher was launched in the same parallel message as the agents
// whose conclusion it is meant to attack, and standard effort pointed at a
// "conditional trigger" that nothing defined. It now runs as wave 2, after wave
// 1 returns, given wave 1's leading answer from the external findings only,
// with the local parts stripped (the de-localising rule of External agents), so
// no local file name or project term reaches its web queries.
const RESEARCH = 'commands/trekresearch.md';
const RESEARCH_ORCH = 'agents/research-orchestrator.md';
test('[#113] Phase 4 launches contrarian-researcher as wave 2 after wave 1, with the leading answer and claims from external findings only, and names the deep-research case', () => {
const text = read(RESEARCH);
const ext = partAt(RESEARCH, '### External agents (scope = both or external)', (l) => /^## /.test(l));
assert.doesNotMatch(ext, /Launch ALL selected agents \*\*in parallel\*\*/, 'Launch rules still start every selected agent, the contrarian too, in one parallel message');
assert.match(ext, /Launch in two waves, both in Phase 4 \(before Phase 4\.5 and Phase 5/);
assert.match(ext, /\*\*Wave 1:\*\* ALL selected agents except `contrarian-researcher` \*\*in parallel\*\* in a single message/);
assert.match(ext, /\*\*Wave 2:\*\* `contrarian-researcher`, when selected, after wave 1 returns/);
assert.match(ext, /the leading answer \(one or two sentences\), the claims for it and the alternatives wave 1 dismissed/);
assert.match(ext, /taken from wave 1's external findings only, never from the local agents' findings/, 'wave 2 does not say external findings only');
assert.match(ext, /stripped of local parts like the research question above \(absolute paths, user names, internal project or repository names, ticket IDs\)/, 'wave 2 does not apply the stripping rule');
assert.match(ext, /Pass the research question to them with the local parts \(absolute paths, user names, internal project or repository names, ticket IDs\) already removed/);
assert.match(ext, /With `--engine deep-research` \(`effective_engine = deep-research`\), wave 2 runs after the engine returns, from the leading answer of its report/);
const p4 = partAt(RESEARCH, '## Phase 4 — Parallel research (agent swarm)', (l) => /^## /.test(l));
assert.match(p4, /then run wave 2 \(`contrarian-researcher`, see Launch rules\) from the report's leading answer/, 'the deep-research step 5 does not hand over to wave 2');
const w2 = text.indexOf('**Wave 2:**');
assert.ok(w2 > text.indexOf('## Phase 4 —') && w2 < text.indexOf('## Phase 4.5 —'), 'wave 2 is not in Phase 4, before Phase 4.5');
});
test('[#113] no conditional trigger is left, and high effort keeps contrarian-researcher unconditional, as wave 2', () => {
const text = flat(read(RESEARCH));
assert.doesNotMatch(text, /triggers conditionally when a leading recommendation is emerging/);
assert.doesNotMatch(text, /use the existing conditional triggers/);
const high = flat(section(read(RESEARCH), '### High-effort behavior (v5.1.1)'));
assert.ok(high.includes('force `contrarian-researcher` to always-on: in high-effort mode `contrarian-researcher` runs unconditionally, as wave 2 (see Launch rules), so the final brief always carries an adversarial counter-evidence pass'),
'high effort does not keep the contrarian unconditional as wave 2');
});
test('[#113] sibling: README.md', () => {
const text = flat(read('README.md'));
assert.doesNotMatch(text, /contrarian-researcher when a leading recommendation forms/);
assert.ok(text.includes('contrarian-researcher, when selected, runs in wave 2 after the other external agents return, with their leading answer to attack (always at `effort=high`)'),
'README.md does not describe wave 2');
});
test('[#113] sibling: agents/research-orchestrator.md', () => {
const p2 = flat(section(read(RESEARCH_ORCH), '### Phase 2 — Parallel research'));
assert.doesNotMatch(p2, /Launch ALL selected agents \*\*in parallel\*\*/);
assert.match(p2, /\*\*Wave 1:\*\* ALL selected agents except `contrarian-researcher` \*\*in parallel\*\* using the Agent tool — one message, multiple tool calls/);
assert.match(p2, /\*\*Wave 2:\*\* `contrarian-researcher`, after wave 1 returns/);
assert.match(p2, /the leading answer \(one or two sentences\), the claims for it and the alternatives wave 1 dismissed/);
assert.match(p2, /taken from wave 1's external findings only, never from the local agents' findings/);
assert.match(p2, /with the local parts \(absolute paths, user names, internal project or repository names, ticket IDs\) removed/);
assert.ok(flat(read(RESEARCH_ORCH)).includes('| `contrarian-researcher` | Counter-evidence, overlooked alternatives, reasons to reconsider; runs in wave 2, against wave 1\'s leading answer |'),
'the agent table does not say the contrarian runs in wave 2');
});