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

@ -823,7 +823,7 @@ flowchart TB
BR --> RES --> PL --> EX --> RV
```
`* = conditional`: convention-scanner / test-strategist on medium+ codebases (50+ files); research-scout for unknown external tech; security-/contrarian-researcher when a leading recommendation forms (or always at `effort=high`); brief-conformance-reviewer skipped under `--quick`.
`* = conditional`: convention-scanner / test-strategist on medium+ codebases (50+ files); research-scout for unknown external tech; security-researcher when a leading recommendation forms (or always at `effort=high`); contrarian-researcher, when selected, runs in wave 2 after the other external agents return, with their leading answer to attack (always at `effort=high`); brief-conformance-reviewer skipped under `--quick`.
> **Which Claude Code primitive each phase uses — and the alternatives considered (Workflow substrate, delegated orchestrator, dormant synthesis-agent)** → see [docs/architecture.md §Primitives per step](docs/architecture.md#primitives-per-step-decision-matrix).

View file

@ -88,12 +88,22 @@ Based on the mode, determine which agent groups to launch:
| `docs-researcher` | Official documentation, RFCs, vendor docs |
| `community-researcher` | Real-world experience, issues, blog posts, discussions |
| `security-researcher` | CVEs, audit history, supply chain risks |
| `contrarian-researcher` | Counter-evidence, overlooked alternatives, reasons to reconsider |
| `contrarian-researcher` | Counter-evidence, overlooked alternatives, reasons to reconsider; runs in wave 2, against wave 1's leading answer |
### Phase 2 — Parallel research
Launch ALL selected agents **in parallel** using the Agent tool — one message,
multiple tool calls. This maximizes concurrency.
Launch the selected agents in two waves.
**Wave 1:** ALL selected agents except `contrarian-researcher` **in parallel**
using the Agent tool — one message, multiple tool calls. This maximizes
concurrency.
**Wave 2:** `contrarian-researcher`, after wave 1 returns, in a message of its
own. Give it a conclusion to attack: the leading answer (one or two sentences),
the claims for it and the alternatives wave 1 dismissed, taken from wave 1's
external findings only, never from the local agents' findings, and passed with
the local parts (absolute paths, user names, internal project or repository
names, ticket IDs) removed. It searches with the words it is given.
**Prompting local agents for research (not planning):**

View file

@ -384,7 +384,8 @@ changes nothing (SC1). Keep the native-swarm anchors intact ("in parallel",
5. **On a real report:** keep `effective_engine = deep-research`, log
`Engine: deep-research (active)`, and carry the report into Phase 6 triangulation
as the external-findings input (adapted in Phase 7 — see the Deep-research engine
adapter below).
adapter below); then run wave 2 (`contrarian-researcher`, see Launch rules)
from the report's leading answer.
### External agents (scope = both or external)
@ -413,7 +414,22 @@ ticket IDs) already removed: the agents search with the words they are given.
### Launch rules
- Launch ALL selected agents **in parallel** in a single message
- Launch in two waves, both in Phase 4 (before Phase 4.5 and Phase 5, so outside
the Phase 5 loop scope marker that `hooks/scripts/pre-agent-cap.mjs` reads).
- **Wave 1:** ALL selected agents except `contrarian-researcher` **in parallel**
in a single message.
- **Wave 2:** `contrarian-researcher`, when selected, after wave 1 returns, in a
message of its own. Give it a conclusion to attack: the leading answer (one or
two sentences), the claims for it and the alternatives wave 1 dismissed,
taken from wave 1's external findings only, never from the local agents'
findings (Hard rules → Independence), and stripped of local parts like the
research question above (absolute paths, user names, internal project or
repository names, ticket IDs). It searches with the words it is given, so no
local file name or project term may be in them. When wave 1's external
findings hold no leading answer, say so and give it the research question
alone, stripped the same way. With `--engine deep-research`
(`effective_engine = deep-research`), wave 2 runs after the engine returns,
from the leading answer of its report.
- Inject the resolved model at every spawn: `phase_signal_result.model` (brief
signal > profile), with the agent's frontmatter `model:` as the fallback — see
Hard rules → Cost. The orchestrator itself follows the session model.
@ -923,12 +939,11 @@ commands surface, don't re-enforce.
### High-effort behavior (v5.1.1)
When `phase_signal_result.effort == 'high'` for the `research` phase,
run the FULL swarm regardless of normal triggering rules: 5 local
run the FULL swarm regardless of normal selection rules: 5 local
agents + 4 external agents, AND force `contrarian-researcher` to
always-on. Normally `contrarian-researcher` triggers conditionally when
a leading recommendation is emerging from initial agents; in
high-effort mode it runs unconditionally so the final brief always
carries an adversarial counter-evidence pass.
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.
The former third-party second-opinion bridge pass is **unavailable,
skipped** (dropped in v5.10.1 by operator directive). High effort no
@ -943,7 +958,9 @@ turns per under-illuminated dimension, budgeted by
`--local`, `external_research_enabled: false`) overrides the effort
signal whenever the external phase does not run at all.
Standard effort (or absent): use the existing conditional triggers.
Standard effort (or absent): the selected agents run as in Phase 4, and
`contrarian-researcher`, when selected, runs as wave 2 after the other
agents return (see Launch rules).
Low effort: inline research only, no agent swarm (existing
`--quick`-equivalent code-path).

View file

@ -206,11 +206,16 @@ test('trekresearch — Phase 5 pins survive only while the prose does (mutation
);
});
test('trekresearch — High-effort behavior keeps the standard/low effort sentences verbatim', () => {
test('trekresearch — High-effort behavior gives standard effort the wave-2 sentence and keeps the low effort sentence verbatim', () => {
const text = read();
const flatText = text.replace(/\s+/g, ' ');
assert.ok(
text.includes('Standard effort (or absent): use the existing conditional triggers.'),
'the standard-effort sentence must survive the Phase 5 rewrite verbatim',
!flatText.includes('use the existing conditional triggers'),
'standard effort must not point at a conditional trigger that nothing defines',
);
assert.ok(
flatText.includes('Standard effort (or absent): the selected agents run as in Phase 4, and `contrarian-researcher`, when selected, runs as wave 2 after the other agents return (see Launch rules).'),
'the standard-effort sentence must say the contrarian runs as wave 2 when selected',
);
assert.ok(
text.includes('Low effort: inline research only, no agent swarm'),

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');
});