Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
40 changes: 20 additions & 20 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
56 changes: 48 additions & 8 deletions packages/cli/src/commands/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import { createLogger, getArchonHome } from '@archon/paths';
import { join } from 'node:path';
import { createWorkflowDeps } from '@archon/core/workflows/store-adapter';
import { discoverWorkflowsWithConfig } from '@archon/workflows/workflow-discovery';
import { inferProviderFromModel } from '@archon/workflows/model-validation';
import { resolveWorkflowName } from '@archon/workflows/router';
import { executeWorkflow } from '@archon/workflows/executor';
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 +130,22 @@ 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;
if (workflow.model) return inferProviderFromModel(workflow.model, fallbackAssistant);
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 +653,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,
workflowName,
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-12',
'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
Loading