Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6554da5
feat: add code reviewer agent
laurentftech May 26, 2026
dd63459
feat(agent): add code-reviewer built-in agent implementation and tests
laurentftech May 28, 2026
e40b166
fix(code-reviewer): address review comments
laurentftech May 28, 2026
3e773e7
ci: trigger rerun — pre-existing test failures on main
laurentftech May 29, 2026
a3bbb67
fix(code-reviewer): enforce read-only contract by disallowing Bash
laurentftech May 30, 2026
991c858
fix(code-reviewer): deny all shell tools (Bash and PowerShell)
laurentftech May 30, 2026
25c6010
fix(code-reviewer): explicit read-only allow-list; drop unrelated art…
laurentftech May 31, 2026
af317cc
fix: restore temp_reference/ gitignore entry from main
laurentftech May 31, 2026
2b1855a
fix(code-reviewer): require inline diff input and preserve read-only …
dineshdinz12 Aug 8, 2026
c5f0ffb
fix(code-reviewer): clear cached agent definitions and markdown loade…
dineshdinz12 Aug 8, 2026
149cace
fix(code-reviewer): address all P2/P3 review findings from jatmn
dineshdinz12 Aug 10, 2026
c59ffe0
fix(code-reviewer): restore prior setting sources in test cleanup, ad…
dineshdinz12 Aug 10, 2026
d26a009
fix(code-reviewer): address remaining PR feedback (P1/P3)
dineshdinz12 Aug 10, 2026
ac051b8
docs: document feature gate for Explore and Plan agents
dineshdinz12 Aug 10, 2026
46d14cf
docs: document inline-diff requirement for code-reviewer agent
dineshdinz12 Aug 10, 2026
605b2e8
fix(code-reviewer): address P1/P2 review findings — teammate boundary…
dineshdinz12 Aug 11, 2026
285cb1c
fix(code-reviewer): guard lock release against failed acquisition
dineshdinz12 Aug 11, 2026
5fb9325
fix(code-reviewer): remove trailing whitespace
dineshdinz12 Aug 12, 2026
b2c9dc2
fix(code-reviewer): reliably block built-in teammate spawns
dineshdinz12 Aug 12, 2026
b114a1c
fix(code-reviewer): preserve original agent identity when resuming a …
dineshdinz12 Aug 13, 2026
c868b42
fix(code-reviewer): address remaining CodeRabbit feedback on test cle…
dineshdinz12 Aug 13, 2026
3b8a338
fix(code-reviewer): address P1/P2 issues for teammate spawns and resu…
dineshdinz12 Aug 13, 2026
edc708d
docs: make OpenLore prerequisite explicitly optional in AGENTS.md
dineshdinz12 Aug 13, 2026
02b4e58
docs: fix pinned OpenLore version in AGENTS.md
dineshdinz12 Aug 13, 2026
9ad319e
Fix review issues
dineshdinz12 Aug 14, 2026
a1b6038
Revert AGENTS.md changes
dineshdinz12 Aug 14, 2026
d80a40c
Restore AGENTS.md to match upstream/main
dineshdinz12 Aug 14, 2026
2a53635
fix(agent): address maintainer feedback on teammate spawns and resume…
dineshdinz12 Aug 14, 2026
d46c36c
test(agent): add regression coverage for legacy source-less agent resume
dineshdinz12 Aug 14, 2026
3570358
fix(agent): propagation pass — batch fork regression, TeamCreate poli…
dineshdinz12 Aug 18, 2026
abbae1f
fix(batch): allow specific custom agent types while requiring subagen…
dineshdinz12 Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,15 +349,18 @@ OpenClaude supports multiple providers, but behavior is not identical across all

For best results, use models with strong tool/function calling support.



## Agents

Route different agents to different models (cost optimization, splitting work
by model strength), cap sub-agent tool steps with `maxSteps`, and tune GitHub
Copilot sub-agent behavior. All settings-driven:
Copilot sub-agent behavior. Configured via settings, agent frontmatter, and
environment variables:

- per-agent provider/model overrides via `agentModels` + `agentRouting` in `~/.openclaude.json`
- per-agent provider/model overrides via `agentModels` + `agentRouting` in `~/.openclaude/settings.json`
- model-only routes that reuse your current provider's credentials
- built-in agents (`Explore`, `Plan`, `verification`) routable by type name
- built-in agents (`Explore` and `Plan` [feature-gated], `verification` [feature-gated: requires `VERIFICATION_AGENT` + `tengu_hive_evidence`], `code-reviewer` [requires diff inline]) routable by type name

See [Agent Routing and Step Limits](docs/agent-routing.md) for the full guide.

Expand Down
10 changes: 6 additions & 4 deletions docs/agent-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ OpenClaude can route different agents to different models through
settings-based routing. This is useful for cost optimization or splitting work
by model strength.

Add to `~/.openclaude.json`:
Add to `~/.openclaude/settings.json`:

> **Note:** `api_key` values in `settings.json` are stored in plaintext. Keep this file private and do not commit it to version control.

```json
{
Expand Down Expand Up @@ -88,9 +90,9 @@ no credential duplication:
```

**Built-in agents are routable by their type name.** Useful keys:
`verification` (the read-only auditor that runs before completion), `Explore`,
and `Plan`. For example, `"agentRouting": { "verification": "mini" }` runs the
verifier on `gpt-5-mini` while your main session stays on its model. Absent
`verification` (the read-only auditor that runs before completion; **feature-gated**: requires `VERIFICATION_AGENT` and `tengu_hive_evidence` flag), `Explore`
and `Plan` (if feature-gated on), and `code-reviewer` (requires diff inline). For example, `"agentRouting": { "verification": "mini" }` runs the
verifier on `gpt-5-mini` while your main session stays on its model, but only when the verification gate is active. Absent
any entry, the verifier inherits the main-loop model.

## GitHub Copilot sub-agent optimization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1483,7 +1483,6 @@ test('OpenGateway MiMo replays real reasoning_content without adding empty fallb
input: {
description: 'Inspect code',
prompt: 'Look at the relevant code',
subagent_type: 'general-purpose',
},
},
],
Expand Down Expand Up @@ -1566,7 +1565,6 @@ test('Xiaomi MiMo replays real reasoning_content without adding empty fallback',
input: {
description: 'Inspect code',
prompt: 'Look at the relevant code',
subagent_type: 'general-purpose',
},
},
],
Expand Down Expand Up @@ -1643,7 +1641,6 @@ test('OpenGateway MiMo does not synthesize empty reasoning_content when missing'
input: {
description: 'Inspect code',
prompt: 'Look at the relevant code',
subagent_type: 'general-purpose',
},
},
],
Expand Down
2 changes: 1 addition & 1 deletion src/skills/bundled/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ For each agent, the prompt must be fully self-contained. Include:
${WORKER_INSTRUCTIONS}
\`\`\`

Use \`subagent_type: "general-purpose"\` unless a more specific agent type fits.
Use a specific custom agent type if one fits the work unit, or \`subagent_type: "general-purpose"\` for default workers. Do NOT omit \`subagent_type\` — batch requires fresh, isolated subprocesses, and omitting the type routes to a fork (which inherits the coordinator's context) when the fork gate is on, which breaks the self-contained worktree invariant.

## Phase 3: Track Progress

Expand Down
4 changes: 0 additions & 4 deletions src/tools/AgentTool/AgentTool.routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ test('normal subagent prompt metadata uses routed effective model', async () =>
{
description: 'Inspect implementation',
prompt: 'Find the bug',
subagent_type: 'general-purpose',
},
createToolUseContext('parent-model', [createAgentDefinition()]),
mock(async () => ({ behavior: 'allow' })) as never,
Expand Down Expand Up @@ -124,7 +123,6 @@ test('agent invocation entering plan mode during MCP wait stays synchronous', as
{
description: 'Inspect implementation',
prompt: 'Find the bug',
subagent_type: 'general-purpose',
},
createToolUseContext('parent-model', [agent], 'default', 'plan'),
mock(async () => ({ behavior: 'allow' })) as never,
Expand Down Expand Up @@ -193,7 +191,6 @@ test('sync agents forward long-running tool progress to the parent tool call', a
{
description: 'Inspect implementation',
prompt: 'Find the bug',
subagent_type: 'general-purpose',
},
createToolUseContext('parent-model', [createAgentDefinition()]),
mock(async () => ({ behavior: 'allow' })) as never,
Expand Down Expand Up @@ -236,7 +233,6 @@ test('a throwing parent progress consumer does not change the subagent outcome',
{
description: 'Inspect implementation',
prompt: 'Find the bug',
subagent_type: 'general-purpose',
},
createToolUseContext('parent-model', [createAgentDefinition()]),
mock(async () => ({ behavior: 'allow' })) as never,
Expand Down
111 changes: 99 additions & 12 deletions src/tools/AgentTool/AgentTool.teammateModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ async function importAgentToolWithSpawnMock(): Promise<{
function makeToolUseContext(options: {
mainLoopModel?: string
activeAgents?: AgentDefinition[]
allAgents?: AgentDefinition[]
} = {}): ToolUseContext {
const appState = {
toolPermissionContext: { mode: 'default' },
Expand All @@ -146,7 +147,7 @@ function makeToolUseContext(options: {
isNonInteractiveSession: false,
agentDefinitions: {
activeAgents: options.activeAgents ?? [],
allAgents: options.activeAgents ?? [],
allAgents: options.allAgents ?? options.activeAgents ?? [],
},
},
abortController: new AbortController(),
Expand Down Expand Up @@ -191,6 +192,7 @@ function callTeammateAgentTool(
contextOptions: {
mainLoopModel?: string
activeAgents?: AgentDefinition[]
allAgents?: AgentDefinition[]
} = {},
): ReturnType<typeof AgentTool.call> {
return AgentTool.call(
Expand Down Expand Up @@ -272,16 +274,16 @@ test('passes routed agentModels keys to teammate spawns by subagent type', async
},
},
agentRouting: {
'general-purpose': 'deepseek-grunt',
'custom-helper': 'deepseek-grunt',
},
} as unknown as SettingsJson
allowedModelsForTest = new Set(['deepseek-grunt'])
const { AgentTool, spawnTeammate } = await importAgentToolWithSpawnMock()

await callTeammateAgentTool(
AgentTool,
{ subagent_type: 'general-purpose' },
{ activeAgents: [createAgentDefinition('general-purpose')] },
{ subagent_type: 'custom-helper' },
{ activeAgents: [createAgentDefinition('custom-helper')] },
)

expect(getSpawnConfig(spawnTeammate).model).toBe('deepseek-grunt')
Expand All @@ -300,16 +302,16 @@ test('applies a model-only route to a teammate spawn without a cross-provider ov
mini: { model: 'gpt-5-mini' },
},
agentRouting: {
verification: 'mini',
'custom-researcher': 'mini',
},
} as unknown as SettingsJson
allowedModelsForTest = new Set(['gpt-5-mini'])
const { AgentTool, spawnTeammate } = await importAgentToolWithSpawnMock()

await callTeammateAgentTool(
AgentTool,
{ subagent_type: 'verification' },
{ activeAgents: [createAgentDefinition('verification')] },
{ subagent_type: 'custom-researcher' },
{ activeAgents: [createAgentDefinition('custom-researcher')] },
)

expect(getSpawnConfig(spawnTeammate).model).toBe('gpt-5-mini')
Expand Down Expand Up @@ -383,9 +385,9 @@ test('does not let non-configured explicit teammate models fall through to defau
AgentTool,
{
model: 'custom-provider-model',
subagent_type: 'general-purpose',
subagent_type: 'custom-helper',
},
{ activeAgents: [createAgentDefinition('general-purpose')] },
{ activeAgents: [createAgentDefinition('custom-helper')] },
)

expect(getSpawnConfig(spawnTeammate).model).toBe('custom-provider-model')
Expand All @@ -401,19 +403,104 @@ test('rejects disallowed routed provider models before spawning a teammate', asy
},
},
agentRouting: {
'general-purpose': 'deepseek-grunt',
'custom-helper': 'deepseek-grunt',
},
} as unknown as SettingsJson
const { AgentTool, spawnTeammate } = await importAgentToolWithSpawnMock()

await expect(
callTeammateAgentTool(
AgentTool,
{ subagent_type: 'general-purpose' },
{ activeAgents: [createAgentDefinition('general-purpose')] },
{ subagent_type: 'custom-helper' },
{ activeAgents: [createAgentDefinition('custom-helper')] },
),
).rejects.toThrow(
"Model 'deepseek-grunt' is not available. Your organization restricts model selection.",
)
expect(spawnTeammate).not.toHaveBeenCalled()
})

test('rejects built-in agents from being spawned as teammates', async () => {
const { AgentTool, spawnTeammate } = await importAgentToolWithSpawnMock()

const builtinAgent = {
agentType: 'code-reviewer',
source: 'built-in',
getSystemPrompt: () => 'review code',
} as unknown as AgentDefinition

await expect(
callTeammateAgentTool(
AgentTool,
{ subagent_type: 'code-reviewer' },
{ activeAgents: [builtinAgent] },
),
).rejects.toThrow(
"Built-in agent type 'code-reviewer' cannot be spawned as a teammate. Please omit name and team_name to use it as a standard subagent.",
)
expect(spawnTeammate).not.toHaveBeenCalled()
})

test('rejects built-in agents from being spawned as teammates even when built-ins are disabled', async () => {
const { AgentTool, spawnTeammate } = await importAgentToolWithSpawnMock()

const prevEnv = process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS
process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS = '1'
const { setIsInteractive, getIsNonInteractiveSession } = await import('../../bootstrap/state.js')
const { getAgentDefinitionsWithOverrides, clearAgentDefinitionsCache } = await import('./loadAgentsDir.js')

const prevInteractive = !getIsNonInteractiveSession()
setIsInteractive(false)
clearAgentDefinitionsCache()

try {
const definitions = await getAgentDefinitionsWithOverrides()

await expect(
callTeammateAgentTool(
AgentTool,
{ subagent_type: 'code-reviewer' },
{ activeAgents: definitions.activeAgents, allAgents: definitions.allAgents },
),
).rejects.toThrow(
"Built-in agent type 'code-reviewer' cannot be spawned as a teammate. Please omit name and team_name to use it as a standard subagent.",
)

await expect(
callTeammateAgentTool(
AgentTool,
{ subagent_type: 'claude-code-guide' },
{ activeAgents: definitions.activeAgents, allAgents: definitions.allAgents },
),
).rejects.toThrow(
"Built-in agent type 'claude-code-guide' cannot be spawned as a teammate. Please omit name and team_name to use it as a standard subagent.",
)
expect(spawnTeammate).not.toHaveBeenCalled()
} finally {
setIsInteractive(prevInteractive)
if (prevEnv !== undefined) {
process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS = prevEnv
} else {
delete process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS
}
clearAgentDefinitionsCache()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

})

test('allows a custom agent to be spawned as a teammate even if it shadows a built-in name', async () => {
const { AgentTool, spawnTeammate } = await importAgentToolWithSpawnMock()

const customShadowAgent = {
agentType: 'code-reviewer',
source: 'projectSettings', // It shadows the name but has a different source
} as unknown as AgentDefinition

await callTeammateAgentTool(
AgentTool,
{ subagent_type: 'code-reviewer' },
{ activeAgents: [customShadowAgent] },
)

expect(spawnTeammate).toHaveBeenCalled()
})
13 changes: 13 additions & 0 deletions src/tools/AgentTool/AgentTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { clearDumpState } from '../../services/api/dumpPrompts.js';
import { resolveAgentRunModelRouting, resolveOutOfProcessTeammateProvider, resolveOutOfProcessTeammateModelOnly } from '../../services/api/agentRouting.js';
import { completeAgentTask as completeAsyncAgent, createActivityDescriptionResolver, createProgressTracker, enqueueAgentNotification, failAgentTask as failAsyncAgent, getProgressUpdate, getTokenCountFromTracker, isLocalAgentTask, killAsyncAgent, registerAgentForeground, registerAsyncAgent, unregisterAgentForeground, updateAgentProgress as updateAsyncAgentProgress, updateProgressFromMessage } from '../../tasks/LocalAgentTask/LocalAgentTask.js';
import { assembleToolPool } from '../../tools.js';
import { isBuiltInAgentType } from './builtInAgents.js';
import { asAgentId } from '../../types/ids.js';
import { runWithAgentContext } from '../../utils/agentContext.js';
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
Expand Down Expand Up @@ -374,6 +375,17 @@ export const AgentTool = buildTool({
if (teamName && name) {
// Set agent definition color for grouped UI display before spawning
const agentDef = subagent_type ? toolUseContext.options.agentDefinitions.activeAgents.find(a => a.agentType === subagent_type) : undefined;

if (subagent_type) {
if (agentDef) {
if (agentDef.source === 'built-in') {
throw new Error(`Built-in agent type '${subagent_type}' cannot be spawned as a teammate. Please omit name and team_name to use it as a standard subagent.`);
}
} else if (isBuiltInAgentType(subagent_type)) {
throw new Error(`Built-in agent type '${subagent_type}' cannot be spawned as a teammate. Please omit name and team_name to use it as a standard subagent.`);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (agentDef?.color) {
setAgentColor(subagent_type!, agentDef.color);
}
Expand Down Expand Up @@ -849,6 +861,7 @@ export const AgentTool = buildTool({
// present so resume can still land in the target repository.
void writeAgentMetadata(asAgentId(earlyAgentId), {
agentType: selectedAgent.agentType,
source: selectedAgent.source,
...(cwd && { cwd }),
...(description && { description }),
}).catch(_err => logForDebugging(`Failed to clear worktree metadata: ${_err}`));
Expand Down
Loading
Loading