Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ bun run web:typecheck
bun run web:build
```

Website release notes live on GitHub Releases; do not add a manually maintained release-notes data source to the static site.
Do not “fix” a web CI npm-freshness failure by editing `web/src/data/releases.ts` in an unrelated PR. That file is release/web-owned; leave version drift for the dedicated release/web path.

Diagnostics and PR hygiene:

Expand All @@ -101,4 +101,4 @@ When modifying provider behavior:
- Do not skip tests for behavior changes.
- Do not silently change provider tags; maintainers control them during review.
- Do not ignore CodeRabbit or maintainer feedback; address it before requesting more review.
- Do not add a manually maintained release-notes data source to the static site; link to GitHub Releases instead.
- Do not edit `web/src/data/releases.ts` in ordinary feature or bugfix PRs. That curated `/changelog` list is owned by the release/web process (release automation and dedicated web release PRs). If web CI fails because npm is ahead of the site version, leave `releases.ts` alone in your unrelated PR and let the release/web path update it.
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`, `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
8 changes: 5 additions & 3 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,8 +90,8 @@ 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
`verification` (the read-only auditor that runs before completion), `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. Absent
any entry, the verifier inherits the main-loop model.

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.
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.
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