Skip to content
Closed
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
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
31 changes: 28 additions & 3 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,12 +653,20 @@ export async function workflowRunCommand(
}

// Auto-generate title for CLI workflow conversations (fire-and-forget)
const workflowConfig = await loadConfig(cwd);
const titleAssistantType = resolveTitleAssistantType(
workflow,
workflowConfig.assistant,
conversation.ai_assistant_type
);
const titleAssistantConfig = workflowConfig.assistants?.[titleAssistantType] ?? {};
void generateAndSetTitle(
conversation.id,
userMessage,
conversation.ai_assistant_type,
titleAssistantType,
workingCwd,
workflowName
workflowName,
titleAssistantConfig
);
Comment on lines 655 to 670
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't make title generation a hard dependency of workflowRunCommand.

loadConfig(cwd) now runs on the main execution path, so a missing or malformed .archon/config.yaml will abort the workflow before it even starts. That regresses the previous fire-and-forget behavior for titles.

Suggested fix
-  const workflowConfig = await loadConfig(cwd);
-  const titleAssistantType = resolveTitleAssistantType(
-    workflow,
-    workflowConfig.assistant,
-    conversation.ai_assistant_type
-  );
-  const titleAssistantConfig = workflowConfig.assistants?.[titleAssistantType] ?? {};
+  let workflowConfig;
+  try {
+    workflowConfig = await loadConfig(cwd);
+  } catch (error) {
+    getLog().warn({ err: error as Error, cwd }, 'workflow.title_config_load_failed');
+  }
+
+  const titleAssistantType = resolveTitleAssistantType(
+    workflow,
+    workflowConfig?.assistant,
+    conversation.ai_assistant_type
+  );
+  const titleAssistantConfig = workflowConfig?.assistants?.[titleAssistantType] ?? {};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Auto-generate title for CLI workflow conversations (fire-and-forget)
const workflowConfig = await loadConfig(cwd);
const titleAssistantType = resolveTitleAssistantType(
workflow,
workflowConfig.assistant,
conversation.ai_assistant_type
);
const titleAssistantConfig = workflowConfig.assistants?.[titleAssistantType] ?? {};
void generateAndSetTitle(
conversation.id,
userMessage,
conversation.ai_assistant_type,
titleAssistantType,
workingCwd,
workflowName
workflowName,
titleAssistantConfig
);
// Auto-generate title for CLI workflow conversations (fire-and-forget)
let workflowConfig;
try {
workflowConfig = await loadConfig(cwd);
} catch (error) {
getLog().warn({ err: error as Error, cwd }, 'workflow.title_config_load_failed');
}
const titleAssistantType = resolveTitleAssistantType(
workflow,
workflowConfig?.assistant,
conversation.ai_assistant_type
);
const titleAssistantConfig = workflowConfig?.assistants?.[titleAssistantType] ?? {};
void generateAndSetTitle(
conversation.id,
userMessage,
titleAssistantType,
workingCwd,
workflowName,
titleAssistantConfig
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/workflow.ts` around lines 655 - 670, The change
made title generation a blocking step by awaiting loadConfig(cwd); instead
restore fire-and-forget behavior by moving config loading and title generation
into an unawaited async task that catches and swallows errors: create an inline
async wrapper (or helper) that calls loadConfig, resolveTitleAssistantType,
determines titleAssistantConfig, then calls generateAndSetTitle(conversation.id,
...), and invoke it without await; ensure any exceptions from loadConfig or
generateAndSetTitle are caught/logged but do not propagate to the main
workflowRunCommand path.


// Register cleanup handlers for graceful termination
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 @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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