Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 48 additions & 1 deletion packages/cli/src/commands/workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,54 @@ describe('workflowRunCommand', () => {
'hello world',
'claude',
'/test/path',
'assist'
'assist',
{}
);
});

it('uses the workflow provider for title generation', async () => {
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
const { executeWorkflow } = await import('@archon/workflows/executor');
const conversationDb = await import('@archon/core/db/conversations');
const codebaseDb = await import('@archon/core/db/codebases');
const core = await import('@archon/core');

(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
workflows: [
makeTestWorkflowWithSource({
name: 'figma-mcp-smoke',
description: 'Smoke test Figma MCP',
provider: 'codex',
}),
],
errors: [],
});
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
id: 'conv-123',
ai_assistant_type: 'claude',
});
(core.loadConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
assistant: 'claude',
assistants: { codex: { model: 'gpt-5.4' } },
defaults: {},
});
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce(null);
(conversationDb.updateConversation as ReturnType<typeof mock>).mockResolvedValueOnce(undefined);
(executeWorkflow as ReturnType<typeof mock>).mockResolvedValueOnce({
success: true,
workflowRunId: 'run-123',
});
(core.generateAndSetTitle as ReturnType<typeof mock>).mockClear();

await workflowRunCommand('/test/path', 'figma-mcp-smoke', 'check figma', { noWorktree: true });

expect(core.generateAndSetTitle).toHaveBeenCalledWith(
'conv-123',
'check figma',
'codex',
'/test/path',
'figma-mcp-smoke',
{ model: 'gpt-5.4' }
);
});

Expand Down
54 changes: 46 additions & 8 deletions packages/cli/src/commands/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
getWorkflowEventEmitter,
type WorkflowEmitterEvent,
} from '@archon/workflows/event-emitter';
import type { WorkflowLoadResult } from '@archon/workflows/schemas/workflow';
import type { WorkflowDefinition, WorkflowLoadResult } from '@archon/workflows/schemas/workflow';
import type { WorkflowRun } from '@archon/workflows/schemas/workflow-run';
import {
approveWorkflow,
Expand Down Expand Up @@ -129,6 +129,21 @@ function buildRegistrationFailureError(action: string, error: Error): Error {
);
}

/**
* Resolve the provider used for CLI conversation titles from the workflow itself.
* This keeps auxiliary title generation aligned with workflow execution instead
* of falling back to a stale conversation default.
*/
function resolveTitleAssistantType(
workflow: WorkflowDefinition,
defaultAssistant: string | undefined,
conversationAssistant: string | undefined
): string {
const fallbackAssistant = defaultAssistant ?? conversationAssistant ?? 'claude';
if (workflow.provider) return workflow.provider;
return fallbackAssistant;
}

/** Render a workflow event to stderr as a progress line. Called only when --quiet is not set. */
function renderWorkflowEvent(event: WorkflowEmitterEvent, verbose: boolean): void {
switch (event.type) {
Expand Down Expand Up @@ -636,13 +651,36 @@ export async function workflowRunCommand(
}

// Auto-generate title for CLI workflow conversations (fire-and-forget)
void generateAndSetTitle(
conversation.id,
userMessage,
conversation.ai_assistant_type,
workingCwd,
workflowName
);
void (async (): Promise<void> => {
let workflowConfig: Awaited<ReturnType<typeof loadConfig>> | undefined;
try {
workflowConfig = await loadConfig(cwd);
} catch (error) {
getLog().warn({ err: error as Error, cwd }, 'workflow.title_config_load_failed');
}

try {
const titleAssistantType = resolveTitleAssistantType(
workflow,
workflowConfig?.assistant,
conversation.ai_assistant_type
);
const titleAssistantConfig = workflowConfig?.assistants?.[titleAssistantType] ?? {};
await generateAndSetTitle(
conversation.id,
userMessage,
titleAssistantType,
workingCwd,
workflow.name,
titleAssistantConfig
);
} catch (error) {
getLog().warn(
{ err: error as Error, conversationId: conversation.id },
'workflow.title_generation_failed'
);
}
})();

// Register cleanup handlers for graceful termination
let terminating = false;
Expand Down
20 changes: 19 additions & 1 deletion packages/core/src/services/title-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const mockSendQuery = mock(async function* (): AsyncGenerator<MessageChunk> {
prompt: string,
cwd: string,
resumeSessionId?: string,
options?: { model?: string; tools?: string[] }
options?: { model?: string; tools?: string[]; assistantConfig?: Record<string, unknown> }
) => AsyncGenerator<MessageChunk>
>;

Expand Down Expand Up @@ -177,6 +177,24 @@ describe('title-generator', () => {
expect(optionsArg.nodeConfig?.allowed_tools).toEqual([]);
});

test('passes assistantConfig through to the provider', async () => {
const assistantConfig = { model: 'gpt-5.4', modelReasoningEffort: 'medium' };

await generateAndSetTitle(
'conv-17',
'Some message',
'codex',
'/tmp',
'figma-mcp-smoke',
assistantConfig
);

const optionsArg = mockSendQuery.mock.calls[0][3] as {
assistantConfig?: Record<string, unknown>;
};
expect(optionsArg.assistantConfig).toEqual(assistantConfig);
});

test('handles double failure gracefully (AI fails + fallback DB write fails)', async () => {
mockSendQuery.mockImplementation(async function* (): AsyncGenerator<MessageChunk> {
throw new Error('AI failure');
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/services/title-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ const MAX_TITLE_LENGTH = 100;
* @param assistantType - Provider identifier (e.g. 'claude', 'codex')
* @param cwd - Working directory for the AI client
* @param workflowName - Optional workflow name for additional context
* @param assistantConfig - Optional provider-specific defaults for the selected assistant
*/
export async function generateAndSetTitle(
conversationDbId: string,
userMessage: string,
assistantType: string,
cwd: string,
workflowName?: string
workflowName?: string,
assistantConfig?: Record<string, unknown>
): Promise<void> {
try {
getLog().debug({ conversationDbId, assistantType }, 'title.generate_started');
Expand All @@ -52,6 +54,7 @@ export async function generateAndSetTitle(

for await (const chunk of client.sendQuery(titlePrompt, cwd, undefined, {
model: titleModel,
assistantConfig,
nodeConfig: { allowed_tools: [] }, // No tool access — pure text generation
})) {
if (chunk.type === 'assistant') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ nodes:
provider: claude # Per-node provider override
model: haiku # Per-node model override
# hooks: # Optional: per-node SDK hook callbacks (Claude only) — see hooks guide
# mcp: .archon/mcp/servers.json # Optional: per-node MCP servers (Claude only)
# mcp: .archon/mcp/servers.json # Optional: per-node MCP servers (Codex and Claude)
# skills: [remotion-best-practices] # Optional: per-node skills (Claude only) — see skills guide
```

Expand Down Expand Up @@ -205,7 +205,7 @@ nodes:
| `allowed_tools` | string[] | — | Whitelist of built-in tools. `[]` = no tools. Claude only |
| `denied_tools` | string[] | — | Tools to remove. Applied after `allowed_tools`. Claude only |
| `hooks` | object | — | Per-node SDK hook callbacks. Claude only. See [Hooks](/guides/hooks/) |
| `mcp` | string | — | Path to MCP server config JSON file. Claude only. See [MCP Servers](/guides/mcp-servers/) |
| `mcp` | string | — | Path to MCP server config JSON file. Codex and Claude. See [MCP Servers](/guides/mcp-servers/) |
| `skills` | string[] | — | Skills to preload. Claude only. See [Skills](/guides/skills/) |
| `agents` | object | — | Inline sub-agent definitions keyed by kebab-case ID. Claude only. See [Inline sub-agents](#inline-sub-agents) |
| `effort` | `'low'`\|`'medium'`\|`'high'`\|`'max'` | — | Reasoning depth. Claude only. Also settable at workflow level |
Expand Down Expand Up @@ -1173,7 +1173,7 @@ Before deploying a workflow:
8. **`allowed_tools` / `denied_tools`** — restrict tools per node (Claude only, SDK-enforced)
9. **`retry:`** — auto-retries transient errors (default: 2 retries / 3 total attempts, 3 s backoff); customize per node
10. **`hooks`** — attach SDK hook callbacks to Claude nodes for tool control and context injection
11. **`mcp:`** — attach per-node MCP servers via JSON config (Claude only)
11. **`mcp:`** — attach per-node MCP servers via JSON config (Codex and Claude)
12. **`skills:`** — preload skills into Claude nodes for domain expertise
13. **`agents:`** — inline Claude sub-agent definitions invokable via the `Task` tool
14. **`effort` / `thinking`** — control reasoning depth and thinking mode per node or workflow (Claude only)
Expand Down
52 changes: 31 additions & 21 deletions packages/docs-web/src/content/docs/guides/mcp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ DAG workflow nodes support a `mcp` field that attaches MCP (Model Context Protoc
servers to individual nodes. Each node gets exactly the external tools it needs —
GitHub, Linear, Postgres, etc. — without over-provisioning.

**Claude only** — Codex nodes will warn and ignore the `mcp` field.
MCP works with Codex and Claude workflow nodes. Pi nodes still warn and ignore
the `mcp` field.

## Quick Start

Expand Down Expand Up @@ -50,6 +51,9 @@ to the AI, and it shuts down when the node completes.
MCP config files are JSON objects where each key is a server name and the value
is a server configuration. Three transport types are supported:

Archon also accepts the common wrapper format `{ "mcpServers": { ... } }`; this
is useful when copying config from tools that already export MCP JSON.

### stdio (default)

Runs a local process. This is the most common type.
Expand Down Expand Up @@ -119,8 +123,9 @@ Connects to an SSE endpoint.

## Environment Variable Expansion

Values in `env` and `headers` fields support `$VAR_NAME` references that are
expanded from `process.env` at execution time.
Values in `env` and `headers` fields support `$VAR_NAME` references. They are
expanded from Archon's process environment at execution time. Codex workflow
nodes also include codebase-scoped env vars in that expansion.

```json
{
Expand All @@ -137,6 +142,7 @@ expanded from `process.env` at execution time.

**Rules:**
- Pattern: `$UPPER_CASE_VAR` (matches `[A-Z_][A-Z0-9_]*`)
- Lowercase or mixed-case references are ignored and left unchanged
- Only `env` and `headers` values are expanded — `command`, `args`, `url` are left untouched
- Undefined vars are replaced with empty string and a warning is shown:
`Warning: Node 'X' MCP config references undefined env vars: VAR_NAME`
Expand Down Expand Up @@ -165,21 +171,23 @@ A single config file can define multiple servers:
}
```

## Automatic Tool Wildcards
## Provider Tool Wiring

When a node loads MCP servers, tool wildcards are automatically added to `allowedTools`.
For servers named `github` and `postgres`, the node gets:
Claude nodes automatically add tool wildcards to `allowed_tools`. For servers
named `github` and `postgres`, the node gets:

- `mcp__github__*`
- `mcp__postgres__*`

This means all tools from those servers are immediately available without manually
listing them. The wildcards merge with any existing `allowed_tools` on the node.
Codex nodes pass the same MCP config as per-node `mcp_servers` overrides to the
Codex SDK, so the servers are available for that node without requiring global
`~/.codex/config.toml` setup.

## MCP-Only Nodes

Combine `mcp` with `allowed_tools: []` to create nodes that can only use MCP tools
and have no access to built-in tools (Bash, Read, Write, etc.):
For providers that support tool restrictions, combine `mcp` with
`allowed_tools: []` to create nodes that can only use MCP tools and have no
access to built-in tools (Bash, Read, Write, etc.):

```yaml
nodes:
Expand All @@ -190,7 +198,9 @@ nodes:
```

This is useful for sandboxing — the AI can only interact through the MCP server
and cannot touch the filesystem or run shell commands.
and cannot touch the filesystem or run shell commands. Codex currently does not
support Archon's `allowed_tools` / `denied_tools` restrictions, so this pattern
is enforced for Claude nodes but not Codex nodes.

## Connection Failure Handling

Expand All @@ -205,12 +215,12 @@ MCP server connection failed: github (failed)
The node continues executing but without the tools from the failed server.
Check your config file path, server command, and environment variables if this happens.

User-level Claude plugin MCPs inherited from `~/.claude/` (e.g. `telegram`,
`notion`) routinely fail to connect inside the headless workflow subprocess
and are **not** surfaced here — they're not actionable for the workflow author.
They appear only in debug logs as `dag.mcp_plugin_connection_suppressed`. Run
the CLI with `--verbose` (or set `LOG_LEVEL=debug` on the server) if you need
to see them.
User-level plugin MCPs inherited from provider-specific user config routinely
fail to connect inside headless workflow subprocesses and are **not** surfaced
when the workflow did not configure MCP itself — they're not actionable for the
workflow author. They appear only in debug logs as
`dag.mcp_plugin_connection_suppressed` for Claude workflows. Run the CLI with
`--verbose` (or set `LOG_LEVEL=debug` on the server) if you need to see them.

## Workflow Examples

Expand Down Expand Up @@ -368,8 +378,8 @@ bun run cli workflow run archon-smart-pr-review "Review PR #123"

## Limitations

- **Claude only** — Codex nodes warn and ignore the `mcp` field. Configure MCP
servers globally in the Codex CLI config instead.
- **Codex tool restrictions** — Codex nodes support `mcp`, but Archon's
`allowed_tools` / `denied_tools` restrictions are still ignored by Codex.
- **Haiku model** — Tool search (lazy loading for many tools) is not supported on
Haiku. You'll see a warning. Consider using Sonnet or Opus for MCP nodes.
- **No load-time validation** — The MCP config file is read at execution time, not
Expand All @@ -386,8 +396,8 @@ bun run cli workflow run archon-smart-pr-review "Review PR #123"
| `MCP config must be a JSON object` | Top-level value is array or string | Wrap in `{ "server-name": { ... } }` |
| `undefined env vars: VAR_NAME` | Environment variable not set | Export the variable or add it to your `.env` |
| `MCP server connection failed` | Server process crashed or URL unreachable | Check command/URL, test the server standalone |
| Plugin MCP missing from workflow output | User-level plugin MCPs (from `~/.claude/`) are filtered out of workflow warnings | Run with `--verbose` and look for `dag.mcp_plugin_connection_suppressed` |
| `mcp config but uses Codex` | Node resolved to Codex provider | Set `provider: claude` on the node or switch default |
| Plugin MCP missing from workflow output | User-level plugin MCPs are filtered out of workflow warnings | Run with `--verbose` and look for provider MCP debug logs |
| `allowed_tools` ignored with Codex | Codex provider does not support Archon's tool restrictions yet | Do not rely on `allowed_tools: []` for Codex sandboxing |
| `Haiku model with MCP servers` | Haiku doesn't support tool search | Use `model: sonnet` or `model: opus` instead |

## Finding MCP Servers
Expand Down
1 change: 1 addition & 0 deletions packages/providers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"./codex/provider": "./src/codex/provider.ts",
"./codex/config": "./src/codex/config.ts",
"./codex/binary-resolver": "./src/codex/binary-resolver.ts",
"./mcp/config": "./src/mcp/config.ts",
"./community/pi": "./src/community/pi/index.ts",
"./errors": "./src/errors.ts",
"./registry": "./src/registry.ts"
Expand Down
8 changes: 2 additions & 6 deletions packages/providers/src/claude/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
export { ClaudeProvider } from './provider';
export { parseClaudeConfig, type ClaudeProviderDefaults } from './config';
export {
loadMcpConfig,
buildSDKHooksFromYAML,
withFirstMessageTimeout,
getProcessUid,
} from './provider';
export { loadMcpConfig } from '../mcp/config';
export { buildSDKHooksFromYAML, withFirstMessageTimeout, getProcessUid } from './provider';
Loading