From 063b0e8d70cd1d9f285738921468c9ef2511892a Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Sat, 31 Jan 2026 23:24:24 +0900 Subject: [PATCH] resolved #69 --- AGENTS.md | 56 ++++---- src/__tests__/engine-agent-overrides.test.ts | 129 +++++++++++++++++++ src/__tests__/pipelineExecution.test.ts | 34 ++++- src/cli.ts | 40 ++++-- src/commands/listTasks.ts | 13 +- src/commands/pipelineExecution.ts | 11 +- src/commands/taskExecution.ts | 19 ++- src/commands/watchTasks.ts | 5 +- src/commands/workflowExecution.ts | 5 + src/workflow/engine.ts | 4 +- src/workflow/types.ts | 3 + 11 files changed, 262 insertions(+), 57 deletions(-) create mode 100644 src/__tests__/engine-agent-overrides.test.ts diff --git a/AGENTS.md b/AGENTS.md index df15a8f..12031e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,44 +1,40 @@ # Repository Guidelines +このリポジトリに貢献する際の基本的な構成と期待値をまとめています。短い説明と例で各セクションを完結に示します。 ## プロジェクト構成とモジュール整理 -- 主要なソースは `src/`、CLI は `src/cli.ts`、公開 API は `src/index.ts` にある。 -- テストは `src/__tests__/` に配置する。 -- ビルド成果物は `dist/`、実行用スクリプトは `bin/`。 -- 既定のリソースは `resources/`、ドキュメントは `docs/`。 -- 実行時設定はユーザーディレクトリ `~/.takt/`、プロジェクト固有設定は `.takt/` に置く。 +- 主要ソースは `src/` にあり、エントリポイントは `src/index.ts`、CLI は `src/cli.ts` です。 +- テストは `src/__tests__/` に置き、ファイル名は対象機能が一目でわかるようにします(例: `client.test.ts`)。 +- ビルド成果物は `dist/`、実行スクリプトは `bin/`、静的リソースは `resources/`、ドキュメントは `docs/` で管理します。 +- 設定やキャッシュを使う際は `~/.takt/` 以下(実行時)や `.takt/`(プロジェクト固有)を参照します。 ## ビルド・テスト・開発コマンド ``` -npm run build # TypeScript をコンパイルして dist/ を生成 -npm run watch # 変更監視ビルド -npm run test # Vitest の全テスト実行 -npm run test:watch # Vitest のウォッチモード -npm run lint # ESLint で静的解析 +npm run build # TypeScript コンパイルを実行し dist/ を生成 +npm run watch # ソース変更を監視しつつ再ビルド +npm run lint # ESLint で src/ を解析 +npm run test # Vitest で全テストを実行 +npm run test:watch # テスト実行をウォッチ ``` -単体実行例: `npx vitest run src/__tests__/client.test.ts` +- 単体テストを個別実行する例: `npx vitest run src/__tests__/client.test.ts`。 -## コーディング規約と命名 -- TypeScript の strict 設定を前提にする。 -- ESM 形式のため、import の拡張子は `.js` を使う。 -- 既存ファイルは ESLint ルールに従い、読みやすさ優先で簡潔に書く。 -- 変更対象の命名や構成は既存パターンに合わせる。 +## コーディングスタイルと命名 +- TypeScript + strict モードを前提にし、可読性や null 安全を優先します。 +- ESM 形式なので `import` の拡張子は `.js` に固定してください。 +- ESLint(`eslint src/`)と prettier ルールを守り、命名は camelCase(関数・変数)および PascalCase(クラス)を採用。 +- クロスファイルの共有型は `src/types/` 風に整理し、既存の命名パターンを踏襲します。 ## テスト指針 -- テストフレームワークは Vitest。 -- 追加・修正は関連テストの追加を推奨する。 -- テストファイルは `src/__tests__/` に置き、内容が分かる名前を付ける。 +- テストフレームワークは Vitest(`vitest.config.ts` 参照)。全ての新機能・修正には関連テストを追加。 +- テストファイル名は `<対象>.test.ts`、あるいは `<対象>.spec.ts` で統一。 +- コンポーネント依存はモックやスタブを使い、状態を分離したシナリオを心がけます。 ## コミットとプルリク -- 直近の履歴は短い要約の一行コミットが中心(日本語・英語混在)。 -- 変更内容が分かる簡潔な件名を推奨。 -- PR は小さく集中した変更を基本とし、必要ならテストとドキュメントを更新。 -- 事前に Issue を立てて相談する方針。 +- 履歴は「短い要約 + 1 行」スタイル。英語・日本語混在可、目的が伝わるよう `feat:`, `fix:` 等のプレフィックスも可。 +- PR には変更概要・テスト結果・関連 Issue(あれば)を含め、小さな対象に絞ってレビュー負荷を抑えます。 +- ドキュメントや設定変更を伴う場合は `CHANGELOG.md` への追記を検討し、スクリーンショットやログがあれば添付します。 ## セキュリティと設定の注意 -- 脆弱性は公開 Issue ではなく、メンテナへ非公開で報告する。 -- `.takt/logs/` には機密情報が残る可能性があるため取り扱いに注意。 -- `~/.takt/config.yaml` の trusted ディレクトリは必要最小限に絞る。 - -## エージェント向け補足 -- ワークフローは `~/.takt/workflows/` の YAML を読み込む。 -- 既存の遷移条件やスキーマは安易に拡張しない。 +- 脆弱性は公開 Issue ではなくメンテナへ直接報告します。 +- `.takt/logs/` など機密情報を含む可能性のあるファイルは共有しないでください。 +- `~/.takt/config.yaml` の `trusted` ディレクトリは最小限にし、不要なパスは登録しないでください。 +- 新しいワークフローを追加する場合は `~/.takt/workflows/` の既存スキーマを踏襲し、不要な拡張を避けます。 diff --git a/src/__tests__/engine-agent-overrides.test.ts b/src/__tests__/engine-agent-overrides.test.ts new file mode 100644 index 0000000..b0f36cd --- /dev/null +++ b/src/__tests__/engine-agent-overrides.test.ts @@ -0,0 +1,129 @@ +/** + * Tests for WorkflowEngine provider/model overrides. + * + * Verifies that CLI-specified overrides take precedence over workflow step defaults, + * and that step-specific values are used when no overrides are present. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('../agents/runner.js', () => ({ + runAgent: vi.fn(), +})); + +vi.mock('../workflow/rule-evaluator.js', () => ({ + detectMatchedRule: vi.fn(), +})); + +vi.mock('../workflow/phase-runner.js', () => ({ + needsStatusJudgmentPhase: vi.fn(), + runReportPhase: vi.fn(), + runStatusJudgmentPhase: vi.fn(), +})); + +vi.mock('../utils/session.js', () => ({ + generateReportDir: vi.fn().mockReturnValue('test-report-dir'), +})); + +import { WorkflowEngine } from '../workflow/engine.js'; +import { runAgent } from '../agents/runner.js'; +import type { WorkflowConfig } from '../models/types.js'; +import { + makeResponse, + makeRule, + makeStep, + mockRunAgentSequence, + mockDetectMatchedRuleSequence, + applyDefaultMocks, +} from './engine-test-helpers.js'; + +describe('WorkflowEngine agent overrides', () => { + beforeEach(() => { + vi.resetAllMocks(); + applyDefaultMocks(); + }); + + it('respects workflow step provider/model even when CLI overrides are provided', async () => { + const step = makeStep('plan', { + provider: 'claude', + model: 'claude-step', + rules: [makeRule('done', 'COMPLETE')], + }); + const config: WorkflowConfig = { + name: 'override-test', + steps: [step], + initialStep: 'plan', + maxIterations: 1, + }; + + mockRunAgentSequence([ + makeResponse({ agent: step.agent, content: 'done' }), + ]); + mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); + + const engine = new WorkflowEngine(config, '/tmp/project', 'override task', { + provider: 'codex', + model: 'cli-model', + }); + + await engine.run(); + + const options = vi.mocked(runAgent).mock.calls[0][2]; + expect(options.provider).toBe('claude'); + expect(options.model).toBe('claude-step'); + }); + + it('allows CLI overrides when workflow step leaves provider/model undefined', async () => { + const step = makeStep('plan', { + rules: [makeRule('done', 'COMPLETE')], + }); + const config: WorkflowConfig = { + name: 'override-fallback', + steps: [step], + initialStep: 'plan', + maxIterations: 1, + }; + + mockRunAgentSequence([ + makeResponse({ agent: step.agent, content: 'done' }), + ]); + mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); + + const engine = new WorkflowEngine(config, '/tmp/project', 'override task', { + provider: 'codex', + model: 'cli-model', + }); + + await engine.run(); + + const options = vi.mocked(runAgent).mock.calls[0][2]; + expect(options.provider).toBe('codex'); + expect(options.model).toBe('cli-model'); + }); + + it('falls back to workflow step provider/model when no overrides supplied', async () => { + const step = makeStep('plan', { + provider: 'claude', + model: 'step-model', + rules: [makeRule('done', 'COMPLETE')], + }); + const config: WorkflowConfig = { + name: 'step-defaults', + steps: [step], + initialStep: 'plan', + maxIterations: 1, + }; + + mockRunAgentSequence([ + makeResponse({ agent: step.agent, content: 'done' }), + ]); + mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]); + + const engine = new WorkflowEngine(config, '/tmp/project', 'step task'); + await engine.run(); + + const options = vi.mocked(runAgent).mock.calls[0][2]; + expect(options.provider).toBe('claude'); + expect(options.model).toBe('step-model'); + }); +}); diff --git a/src/__tests__/pipelineExecution.test.ts b/src/__tests__/pipelineExecution.test.ts index 94d7e4a..18e01df 100644 --- a/src/__tests__/pipelineExecution.test.ts +++ b/src/__tests__/pipelineExecution.test.ts @@ -150,6 +150,30 @@ describe('executePipeline', () => { 'Fix the bug', '/tmp/test', 'default', + undefined, + undefined, + ); + }); + + it('passes provider/model overrides to task execution', async () => { + mockExecuteTask.mockResolvedValueOnce(true); + + const exitCode = await executePipeline({ + task: 'Fix the bug', + workflow: 'default', + autoPr: false, + cwd: '/tmp/test', + provider: 'codex', + model: 'codex-model', + }); + + expect(exitCode).toBe(0); + expect(mockExecuteTask).toHaveBeenCalledWith( + 'Fix the bug', + '/tmp/test', + 'default', + undefined, + { provider: 'codex', model: 'codex-model' }, ); }); @@ -205,6 +229,8 @@ describe('executePipeline', () => { 'From --task flag', '/tmp/test', 'magi', + undefined, + undefined, ); }); @@ -359,7 +385,13 @@ describe('executePipeline', () => { }); expect(exitCode).toBe(0); - expect(mockExecuteTask).toHaveBeenCalledWith('Fix the bug', '/tmp/test', 'default'); + expect(mockExecuteTask).toHaveBeenCalledWith( + 'Fix the bug', + '/tmp/test', + 'default', + undefined, + undefined, + ); // No git operations should have been called const gitCalls = mockExecFileSync.mock.calls.filter( diff --git a/src/cli.ts b/src/cli.ts index 3434be6..ef5bdfb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -50,6 +50,8 @@ import { DEFAULT_WORKFLOW_NAME } from './constants.js'; import { checkForUpdates } from './utils/updateNotifier.js'; import { resolveIssueTask, isIssueReference } from './github/issue.js'; import { createPullRequest, buildPrBody } from './github/pr.js'; +import type { TaskExecutionOptions } from './commands/taskExecution.js'; +import type { ProviderType } from './providers/index.js'; const require = createRequire(import.meta.url); const { version: cliVersion } = require('../package.json') as { version: string }; @@ -109,6 +111,7 @@ async function selectAndExecuteTask( cwd: string, task: string, options?: { autoPr?: boolean; repo?: string }, + agentOverrides?: TaskExecutionOptions, ): Promise { const selectedWorkflow = await selectWorkflow(cwd); @@ -120,7 +123,7 @@ async function selectAndExecuteTask( const { execCwd, isWorktree, branch } = await confirmAndCreateWorktree(cwd, task); log.info('Starting task execution', { workflow: selectedWorkflow, worktree: isWorktree }); - const taskSuccess = await executeTask(task, execCwd, selectedWorkflow, cwd); + const taskSuccess = await executeTask(task, execCwd, selectedWorkflow, cwd, agentOverrides); if (taskSuccess && isWorktree) { const commitResult = autoCommitAndPush(execCwd, task, cwd); @@ -186,6 +189,18 @@ export async function confirmAndCreateWorktree( const program = new Command(); +function resolveAgentOverrides(): TaskExecutionOptions | undefined { + const opts = program.opts(); + const provider = opts.provider as ProviderType | undefined; + const model = opts.model as string | undefined; + + if (!provider && !model) { + return undefined; + } + + return { provider, model }; +} + program .name('takt') .description('TAKT: Task Agent Koordination Tool') @@ -198,6 +213,8 @@ program .option('-b, --branch ', 'Branch name (auto-generated if omitted)') .option('--auto-pr', 'Create PR after successful execution') .option('--repo ', 'Repository (defaults to current)') + .option('--provider ', 'Override agent provider (claude|codex|mock)') + .option('--model ', 'Override agent model') .option('-t, --task ', 'Task content (triggers pipeline/non-interactive mode)') .option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)'); @@ -239,14 +256,14 @@ program .description('Run all pending tasks from .takt/tasks/') .action(async () => { const workflow = getCurrentWorkflow(resolvedCwd); - await runAllTasks(resolvedCwd, workflow); + await runAllTasks(resolvedCwd, workflow, resolveAgentOverrides()); }); program .command('watch') .description('Watch for tasks and auto-execute') .action(async () => { - await watchTasks(resolvedCwd); + await watchTasks(resolvedCwd, resolveAgentOverrides()); }); program @@ -261,7 +278,7 @@ program .command('list') .description('List task branches (merge/delete)') .action(async () => { - await listTasks(resolvedCwd); + await listTasks(resolvedCwd, resolveAgentOverrides()); }); program @@ -318,6 +335,7 @@ program .argument('[task]', 'Task to execute (or GitHub issue reference like "#6")') .action(async (task?: string) => { const opts = program.opts(); + const agentOverrides = resolveAgentOverrides(); // --- Pipeline mode (non-interactive): triggered by --task --- if (pipelineMode) { @@ -328,9 +346,11 @@ program branch: opts.branch as string | undefined, autoPr: opts.autoPr === true, repo: opts.repo as string | undefined, - skipGit: opts.skipGit === true, - cwd: resolvedCwd, - }); + skipGit: opts.skipGit === true, + cwd: resolvedCwd, + provider: agentOverrides?.provider, + model: agentOverrides?.model, + }); if (exitCode !== 0) { process.exit(exitCode); @@ -350,7 +370,7 @@ program if (issueFromOption) { try { const resolvedTask = resolveIssueTask(`#${issueFromOption}`); - await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions); + await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions, agentOverrides); } catch (e) { error(e instanceof Error ? e.message : String(e)); process.exit(1); @@ -371,7 +391,7 @@ program } } - await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions); + await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions, agentOverrides); return; } @@ -382,7 +402,7 @@ program return; } - await selectAndExecuteTask(resolvedCwd, result.task, prOptions); + await selectAndExecuteTask(resolvedCwd, result.task, prOptions, agentOverrides); }); program.parse(); diff --git a/src/commands/listTasks.ts b/src/commands/listTasks.ts index 4bade02..243a384 100644 --- a/src/commands/listTasks.ts +++ b/src/commands/listTasks.ts @@ -24,7 +24,7 @@ import { autoCommitAndPush } from '../task/autoCommit.js'; import { selectOption, confirm, promptInput } from '../prompt/index.js'; import { info, success, error as logError, warn } from '../utils/ui.js'; import { createLogger } from '../utils/debug.js'; -import { executeTask } from './taskExecution.js'; +import { executeTask, type TaskExecutionOptions } from './taskExecution.js'; import { listWorkflows } from '../config/workflowLoader.js'; import { getCurrentWorkflow } from '../config/paths.js'; import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; @@ -292,6 +292,7 @@ function getBranchContext(projectDir: string, branch: string): string { export async function instructBranch( projectDir: string, item: BranchListItem, + options?: TaskExecutionOptions, ): Promise { const { branch } = item.info; @@ -323,7 +324,7 @@ export async function instructBranch( : instruction; // 5. Execute task on temp clone - const taskSuccess = await executeTask(fullInstruction, clone.path, selectedWorkflow, projectDir); + const taskSuccess = await executeTask(fullInstruction, clone.path, selectedWorkflow, projectDir, options); // 6. Auto-commit+push if successful if (taskSuccess) { @@ -351,7 +352,7 @@ export async function instructBranch( /** * Main entry point: list branch-based tasks interactively. */ -export async function listTasks(cwd: string): Promise { +export async function listTasks(cwd: string, options?: TaskExecutionOptions): Promise { log.info('Starting list-tasks'); const defaultBranch = detectDefaultBranch(cwd); @@ -367,7 +368,7 @@ export async function listTasks(cwd: string): Promise { const items = buildListItems(cwd, branches, defaultBranch); // Build selection options - const options = items.map((item, idx) => { + const menuOptions = items.map((item, idx) => { const filesSummary = `${item.filesChanged} file${item.filesChanged !== 1 ? 's' : ''} changed`; const description = item.originalInstruction ? `${filesSummary} | ${item.originalInstruction}` @@ -381,7 +382,7 @@ export async function listTasks(cwd: string): Promise { const selected = await selectOption( 'List Tasks (Branches)', - options, + menuOptions, ); if (selected === null) { @@ -406,7 +407,7 @@ export async function listTasks(cwd: string): Promise { switch (action) { case 'instruct': - await instructBranch(cwd, item); + await instructBranch(cwd, item, options); break; case 'try': tryMergeBranch(cwd, item); diff --git a/src/commands/pipelineExecution.ts b/src/commands/pipelineExecution.ts index 964ae72..259a8ab 100644 --- a/src/commands/pipelineExecution.ts +++ b/src/commands/pipelineExecution.ts @@ -12,7 +12,7 @@ import { execFileSync } from 'node:child_process'; import { fetchIssue, formatIssueAsTask, checkGhCli, type GitHubIssue } from '../github/issue.js'; import { createPullRequest, pushBranch, buildPrBody } from '../github/pr.js'; -import { executeTask } from './taskExecution.js'; +import { executeTask, type TaskExecutionOptions } from './taskExecution.js'; import { loadGlobalConfig } from '../config/globalConfig.js'; import { info, error, success, status } from '../utils/ui.js'; import { createLogger } from '../utils/debug.js'; @@ -23,6 +23,7 @@ import { EXIT_GIT_OPERATION_FAILED, EXIT_PR_CREATION_FAILED, } from '../exitCodes.js'; +import type { ProviderType } from '../providers/index.js'; const log = createLogger('pipeline'); @@ -43,6 +44,8 @@ export interface PipelineExecutionOptions { skipGit?: boolean; /** Working directory */ cwd: string; + provider?: ProviderType; + model?: string; } /** @@ -184,7 +187,11 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis info(`Running workflow: ${workflow}`); log.info('Pipeline workflow execution starting', { workflow, branch, skipGit, issueNumber: options.issueNumber }); - const taskSuccess = await executeTask(task, cwd, workflow); + const agentOverrides: TaskExecutionOptions | undefined = (options.provider || options.model) + ? { provider: options.provider, model: options.model } + : undefined; + + const taskSuccess = await executeTask(task, cwd, workflow, undefined, agentOverrides); if (!taskSuccess) { error(`Workflow '${workflow}' failed`); diff --git a/src/commands/taskExecution.ts b/src/commands/taskExecution.ts index 2d0a316..039e9e8 100644 --- a/src/commands/taskExecution.ts +++ b/src/commands/taskExecution.ts @@ -18,9 +18,15 @@ import { createLogger } from '../utils/debug.js'; import { getErrorMessage } from '../utils/error.js'; import { executeWorkflow } from './workflowExecution.js'; import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; +import type { ProviderType } from '../providers/index.js'; const log = createLogger('task'); +export interface TaskExecutionOptions { + provider?: ProviderType; + model?: string; +} + /** * Execute a single task with workflow * @param task - Task content @@ -32,7 +38,8 @@ export async function executeTask( task: string, cwd: string, workflowName: string = DEFAULT_WORKFLOW_NAME, - projectCwd?: string + projectCwd?: string, + options?: TaskExecutionOptions ): Promise { const workflowConfig = loadWorkflow(workflowName); @@ -52,6 +59,8 @@ export async function executeTask( const result = await executeWorkflow(workflowConfig, task, cwd, { projectCwd, language: globalConfig.language, + provider: options?.provider, + model: options?.model, }); return result.success; } @@ -69,6 +78,7 @@ export async function executeAndCompleteTask( taskRunner: TaskRunner, cwd: string, workflowName: string, + options?: TaskExecutionOptions, ): Promise { const startedAt = new Date().toISOString(); const executionLog: string[] = []; @@ -77,7 +87,7 @@ export async function executeAndCompleteTask( const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName); // cwd is always the project root; pass it as projectCwd so reports/sessions go there - const taskSuccess = await executeTask(task.content, execCwd, execWorkflow, cwd); + const taskSuccess = await executeTask(task.content, execCwd, execWorkflow, cwd, options); const completedAt = new Date().toISOString(); if (taskSuccess && isWorktree) { @@ -132,7 +142,8 @@ export async function executeAndCompleteTask( */ export async function runAllTasks( cwd: string, - workflowName: string = DEFAULT_WORKFLOW_NAME + workflowName: string = DEFAULT_WORKFLOW_NAME, + options?: TaskExecutionOptions, ): Promise { const taskRunner = new TaskRunner(cwd); @@ -155,7 +166,7 @@ export async function runAllTasks( info(`=== Task: ${task.name} ===`); console.log(); - const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName); + const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options); if (taskSuccess) { successCount++; diff --git a/src/commands/watchTasks.ts b/src/commands/watchTasks.ts index 4e92e9f..aba0883 100644 --- a/src/commands/watchTasks.ts +++ b/src/commands/watchTasks.ts @@ -16,12 +16,13 @@ import { } from '../utils/ui.js'; import { executeAndCompleteTask } from './taskExecution.js'; import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; +import type { TaskExecutionOptions } from './taskExecution.js'; /** * Watch for tasks and execute them as they appear. * Runs until Ctrl+C. */ -export async function watchTasks(cwd: string): Promise { +export async function watchTasks(cwd: string, options?: TaskExecutionOptions): Promise { const workflowName = getCurrentWorkflow(cwd) || DEFAULT_WORKFLOW_NAME; const taskRunner = new TaskRunner(cwd); const watcher = new TaskWatcher(cwd); @@ -51,7 +52,7 @@ export async function watchTasks(cwd: string): Promise { info(`=== Task ${taskCount}: ${task.name} ===`); console.log(); - const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName); + const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options); if (taskSuccess) { successCount++; diff --git a/src/commands/workflowExecution.ts b/src/commands/workflowExecution.ts index 033d0fa..3c718b9 100644 --- a/src/commands/workflowExecution.ts +++ b/src/commands/workflowExecution.ts @@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs'; import { WorkflowEngine } from '../workflow/engine.js'; import type { WorkflowConfig, Language } from '../models/types.js'; import type { IterationLimitRequest } from '../workflow/types.js'; +import type { ProviderType } from '../providers/index.js'; import { loadAgentSessions, updateAgentSession, @@ -73,6 +74,8 @@ export interface WorkflowExecutionOptions { projectCwd?: string; /** Language for instruction metadata */ language?: Language; + provider?: ProviderType; + model?: string; } /** @@ -182,6 +185,8 @@ export async function executeWorkflow( onIterationLimit: iterationLimitHandler, projectCwd, language: options.language, + provider: options.provider, + model: options.model, }); let abortReason: string | undefined; diff --git a/src/workflow/engine.ts b/src/workflow/engine.ts index 4767680..fcafbfc 100644 --- a/src/workflow/engine.ts +++ b/src/workflow/engine.ts @@ -226,8 +226,8 @@ export class WorkflowEngine extends EventEmitter { return { cwd: this.cwd, agentPath: step.agentPath, - provider: step.provider, - model: step.model, + provider: step.provider ?? this.options.provider, + model: step.model ?? this.options.model, permissionMode: step.permissionMode, onStream: this.options.onStream, onPermissionRequest: this.options.onPermissionRequest, diff --git a/src/workflow/types.ts b/src/workflow/types.ts index 5a78fd6..5c9e920 100644 --- a/src/workflow/types.ts +++ b/src/workflow/types.ts @@ -8,6 +8,7 @@ import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js'; import type { StreamCallback } from '../agents/runner.js'; import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js'; +import type { ProviderType } from '../providers/index.js'; /** Events emitted by workflow engine */ export interface WorkflowEvents { @@ -75,6 +76,8 @@ export interface WorkflowEngineOptions { projectCwd?: string; /** Language for instruction metadata. Defaults to 'en'. */ language?: Language; + provider?: ProviderType; + model?: string; } /** Loop detection result */