diff --git a/.changeset/happy-voles-roar.md b/.changeset/happy-voles-roar.md new file mode 100644 index 000000000..024e0c6ae --- /dev/null +++ b/.changeset/happy-voles-roar.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1288 +--- +**Researcher agents can now invoke Perplexity** — `gsd-phase-researcher` and `gsd-project-researcher` referenced `mcp__perplexity__*` in their provider dispatch tables but never granted it in their `tools:` allowlist, so Perplexity web research silently fell through to the next provider. The grant is now generated from the researcher profiles, with a parity guard that fails if a future dispatch-table provider is added without its tool grant. (#1284) diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index e78443aa7..045f178bd 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-phase-researcher description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator. -tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__* +tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__* color: cyan # hooks: # PostToolUse: diff --git a/agents/gsd-project-researcher.md b/agents/gsd-project-researcher.md index 8f5286bc5..d4676a2be 100644 --- a/agents/gsd-project-researcher.md +++ b/agents/gsd-project-researcher.md @@ -1,7 +1,7 @@ --- name: gsd-project-researcher description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators. -tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__* +tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__* color: cyan # hooks: # PostToolUse: diff --git a/scripts/research-profiles.cjs b/scripts/research-profiles.cjs index afe801e17..4d1c2ba72 100644 --- a/scripts/research-profiles.cjs +++ b/scripts/research-profiles.cjs @@ -24,7 +24,7 @@ const PROFILES = [ 'Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.', color: 'cyan', tools: - 'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*', + 'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*', requiredIncludes: [ '@~/.claude/gsd-core/references/research-documentation-lookup.md', '@~/.claude/gsd-core/references/research-philosophy.md', @@ -46,7 +46,7 @@ const PROFILES = [ 'Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.', color: 'cyan', tools: - 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*', + 'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*', requiredIncludes: [ '@~/.claude/gsd-core/references/research-documentation-lookup.md', '@~/.claude/gsd-core/references/research-philosophy.md', diff --git a/tests/agent-size-baseline.json b/tests/agent-size-baseline.json index e31016fd5..f6ec64c07 100644 --- a/tests/agent-size-baseline.json +++ b/tests/agent-size-baseline.json @@ -21,10 +21,10 @@ "gsd-mempalace-curator.md": 4160, "gsd-nyquist-auditor.md": 7255, "gsd-pattern-mapper.md": 12487, - "gsd-phase-researcher.md": 40618, + "gsd-phase-researcher.md": 40638, "gsd-plan-checker.md": 42003, "gsd-planner.md": 48892, - "gsd-project-researcher.md": 21994, + "gsd-project-researcher.md": 22014, "gsd-research-synthesizer.md": 13653, "gsd-roadmapper.md": 21781, "gsd-security-auditor.md": 6226, diff --git a/tests/mcp-tool-inheritance.test.cjs b/tests/mcp-tool-inheritance.test.cjs index fade31c19..b5d32192a 100644 --- a/tests/mcp-tool-inheritance.test.cjs +++ b/tests/mcp-tool-inheritance.test.cjs @@ -116,3 +116,56 @@ describe('Researcher agent MCP tool set parity: new tools match exa/firecrawl pa }); } }); + +// --- Regression (#1284): every MCP-backed provider named in the Step-C +// dispatch table must be granted in the agent's frontmatter `tools:` line. +// Guards against a provider being added to the waterfall + dispatch table +// without the matching mcp____* grant (the perplexity drift). --- +describe('researcher Step-C dispatch ↔ tools frontmatter parity (#1284)', () => { + const RESEARCHERS = ['gsd-phase-researcher', 'gsd-project-researcher']; + + function mcpServersIn(text) { + const servers = new Set(); + const re = /mcp__([a-z0-9]+)__/gi; + let m; + while ((m = re.exec(text)) !== null) servers.add(m[1].toLowerCase()); + return servers; + } + function readAgent(name) { + return fs.readFileSync(path.join(__dirname, '..', 'agents', `${name}.md`), 'utf8'); + } + function toolsLine(content) { + const m = content.match(/^tools:\s*(.+)$/m); + assert.ok(m, 'agent frontmatter must have a tools: line'); + return m[1]; + } + function stepCTableRows(content) { + const start = content.indexOf('### Step C'); + assert.ok(start !== -1, 'agent must have a "### Step C" dispatch section'); + const rest = content.slice(start + 1); + const nextHeading = rest.indexOf('\n### '); + const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading); + // Only the markdown dispatch-table rows (| provider | mcp tool |) define + // provider->tool mappings. Generic fallback prose (e.g. `mcp____*`) + // is intentionally excluded so it cannot create false positives. + return section + .split('\n') + .filter((line) => line.trimStart().startsWith('|')) + .join('\n'); + } + + for (const name of RESEARCHERS) { + test(`${name}: grants mcp____* for every MCP provider in its Step-C table`, () => { + const content = readAgent(name); + const granted = mcpServersIn(toolsLine(content)); + const referenced = mcpServersIn(stepCTableRows(content)); + assert.ok(referenced.size > 0, + `${name} Step-C table should reference at least one mcp__ provider`); + const missing = [...referenced].filter((srv) => !granted.has(srv)); + assert.deepStrictEqual(missing, [], + `${name}: Step-C references MCP provider(s) not granted in tools: frontmatter: ` + + `${missing.join(', ')}. Add mcp____* to the profile in ` + + `scripts/research-profiles.cjs and regenerate.`); + }); + } +});