This commit is contained in:
nrslib 2026-01-31 23:24:24 +09:00
parent 36b438e45a
commit 063b0e8d70
11 changed files with 262 additions and 57 deletions

View File

@ -1,44 +1,40 @@
# Repository Guidelines # Repository Guidelines
このリポジトリに貢献する際の基本的な構成と期待値をまとめています。短い説明と例で各セクションを完結に示します。
## プロジェクト構成とモジュール整理 ## プロジェクト構成とモジュール整理
- 主要なソースは `src/`、CLI は `src/cli.ts`、公開 API は `src/index.ts` にある。 - 主要ソースは `src/` にあり、エントリポイントは `src/index.ts`、CLI は `src/cli.ts` です。
- テストは `src/__tests__/` に配置する。 - テストは `src/__tests__/` に置き、ファイル名は対象機能が一目でわかるようにします(例: `client.test.ts`)。
- ビルド成果物は `dist/`、実行用スクリプトは `bin/` - ビルド成果物は `dist/`、実行スクリプトは `bin/`、静的リソースは `resources/`、ドキュメントは `docs/` で管理します。
- 既定のリソースは `resources/`、ドキュメントは `docs/` - 設定やキャッシュを使う際は `~/.takt/` 以下(実行時)や `.takt/`(プロジェクト固有)を参照します。
- 実行時設定はユーザーディレクトリ `~/.takt/`、プロジェクト固有設定は `.takt/` に置く。
## ビルド・テスト・開発コマンド ## ビルド・テスト・開発コマンド
``` ```
npm run build # TypeScript コンパイルし dist/ を生成 npm run build # TypeScript コンパイルを実行し dist/ を生成
npm run watch # 変更監視ビルド npm run watch # ソース変更監視しつつ再ビルド
npm run test # Vitest の全テスト実行 npm run lint # ESLint で src/ を解析
npm run test:watch # Vitest のウォッチモード npm run test # Vitest で全テストを実行
npm run lint # ESLint で静的解析 npm run test:watch # テスト実行をウォッチ
``` ```
単体実行例: `npx vitest run src/__tests__/client.test.ts` - 単体テストを個別実行する例: `npx vitest run src/__tests__/client.test.ts`
## コーディング規約と命名 ## コーディングスタイルと命名
- TypeScript の strict 設定を前提にする - TypeScript + strict モードを前提にし、可読性や null 安全を優先します
- ESM 形式のため、import の拡張子は `.js` を使う - ESM 形式なので `import` の拡張子は `.js` に固定してください
- 既存ファイルは ESLint ルールに従い、読みやすさ優先で簡潔に書く - ESLint`eslint src/`)と prettier ルールを守り、命名は camelCase関数・変数および PascalCaseクラスを採用
- 変更対象の命名や構成は既存パターンに合わせる - クロスファイルの共有型は `src/types/` 風に整理し、既存の命名パターンを踏襲します
## テスト指針 ## テスト指針
- テストフレームワークは Vitest。 - テストフレームワークは Vitest`vitest.config.ts` 参照)。全ての新機能・修正には関連テストを追加
- 追加・修正は関連テストの追加を推奨する - テストファイル名は `<対象>.test.ts`、あるいは `<対象>.spec.ts` で統一
- テストファイルは `src/__tests__/` に置き、内容が分かる名前を付ける - コンポーネント依存はモックやスタブを使い、状態を分離したシナリオを心がけます
## コミットとプルリク ## コミットとプルリク
- 直近の履歴は短い要約の一行コミットが中心(日本語・英語混在)。 - 履歴は「短い要約 + 1 行」スタイル。英語・日本語混在可、目的が伝わるよう `feat:`, `fix:` 等のプレフィックスも可。
- 変更内容が分かる簡潔な件名を推奨。 - PR には変更概要・テスト結果・関連 Issueあればを含め、小さな対象に絞ってレビュー負荷を抑えます。
- PR は小さく集中した変更を基本とし、必要ならテストとドキュメントを更新。 - ドキュメントや設定変更を伴う場合は `CHANGELOG.md` への追記を検討し、スクリーンショットやログがあれば添付します。
- 事前に Issue を立てて相談する方針。
## セキュリティと設定の注意 ## セキュリティと設定の注意
- 脆弱性は公開 Issue ではなく、メンテナへ非公開で報告する。 - 脆弱性は公開 Issue ではなくメンテナへ直接報告します。
- `.takt/logs/` には機密情報が残る可能性があるため取り扱いに注意。 - `.takt/logs/` など機密情報を含む可能性のあるファイルは共有しないでください。
- `~/.takt/config.yaml` の trusted ディレクトリは必要最小限に絞る。 - `~/.takt/config.yaml``trusted` ディレクトリは最小限にし、不要なパスは登録しないでください。
- 新しいワークフローを追加する場合は `~/.takt/workflows/` の既存スキーマを踏襲し、不要な拡張を避けます。
## エージェント向け補足
- ワークフローは `~/.takt/workflows/` の YAML を読み込む。
- 既存の遷移条件やスキーマは安易に拡張しない。

View File

@ -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');
});
});

View File

@ -150,6 +150,30 @@ describe('executePipeline', () => {
'Fix the bug', 'Fix the bug',
'/tmp/test', '/tmp/test',
'default', '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', 'From --task flag',
'/tmp/test', '/tmp/test',
'magi', 'magi',
undefined,
undefined,
); );
}); });
@ -359,7 +385,13 @@ describe('executePipeline', () => {
}); });
expect(exitCode).toBe(0); 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 // No git operations should have been called
const gitCalls = mockExecFileSync.mock.calls.filter( const gitCalls = mockExecFileSync.mock.calls.filter(

View File

@ -50,6 +50,8 @@ import { DEFAULT_WORKFLOW_NAME } from './constants.js';
import { checkForUpdates } from './utils/updateNotifier.js'; import { checkForUpdates } from './utils/updateNotifier.js';
import { resolveIssueTask, isIssueReference } from './github/issue.js'; import { resolveIssueTask, isIssueReference } from './github/issue.js';
import { createPullRequest, buildPrBody } from './github/pr.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 require = createRequire(import.meta.url);
const { version: cliVersion } = require('../package.json') as { version: string }; const { version: cliVersion } = require('../package.json') as { version: string };
@ -109,6 +111,7 @@ async function selectAndExecuteTask(
cwd: string, cwd: string,
task: string, task: string,
options?: { autoPr?: boolean; repo?: string }, options?: { autoPr?: boolean; repo?: string },
agentOverrides?: TaskExecutionOptions,
): Promise<void> { ): Promise<void> {
const selectedWorkflow = await selectWorkflow(cwd); const selectedWorkflow = await selectWorkflow(cwd);
@ -120,7 +123,7 @@ async function selectAndExecuteTask(
const { execCwd, isWorktree, branch } = await confirmAndCreateWorktree(cwd, task); const { execCwd, isWorktree, branch } = await confirmAndCreateWorktree(cwd, task);
log.info('Starting task execution', { workflow: selectedWorkflow, worktree: isWorktree }); 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) { if (taskSuccess && isWorktree) {
const commitResult = autoCommitAndPush(execCwd, task, cwd); const commitResult = autoCommitAndPush(execCwd, task, cwd);
@ -186,6 +189,18 @@ export async function confirmAndCreateWorktree(
const program = new Command(); 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 program
.name('takt') .name('takt')
.description('TAKT: Task Agent Koordination Tool') .description('TAKT: Task Agent Koordination Tool')
@ -198,6 +213,8 @@ program
.option('-b, --branch <name>', 'Branch name (auto-generated if omitted)') .option('-b, --branch <name>', 'Branch name (auto-generated if omitted)')
.option('--auto-pr', 'Create PR after successful execution') .option('--auto-pr', 'Create PR after successful execution')
.option('--repo <owner/repo>', 'Repository (defaults to current)') .option('--repo <owner/repo>', 'Repository (defaults to current)')
.option('--provider <name>', 'Override agent provider (claude|codex|mock)')
.option('--model <name>', 'Override agent model')
.option('-t, --task <string>', 'Task content (triggers pipeline/non-interactive mode)') .option('-t, --task <string>', 'Task content (triggers pipeline/non-interactive mode)')
.option('--skip-git', 'Skip branch creation, commit, and push (pipeline 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/') .description('Run all pending tasks from .takt/tasks/')
.action(async () => { .action(async () => {
const workflow = getCurrentWorkflow(resolvedCwd); const workflow = getCurrentWorkflow(resolvedCwd);
await runAllTasks(resolvedCwd, workflow); await runAllTasks(resolvedCwd, workflow, resolveAgentOverrides());
}); });
program program
.command('watch') .command('watch')
.description('Watch for tasks and auto-execute') .description('Watch for tasks and auto-execute')
.action(async () => { .action(async () => {
await watchTasks(resolvedCwd); await watchTasks(resolvedCwd, resolveAgentOverrides());
}); });
program program
@ -261,7 +278,7 @@ program
.command('list') .command('list')
.description('List task branches (merge/delete)') .description('List task branches (merge/delete)')
.action(async () => { .action(async () => {
await listTasks(resolvedCwd); await listTasks(resolvedCwd, resolveAgentOverrides());
}); });
program program
@ -318,6 +335,7 @@ program
.argument('[task]', 'Task to execute (or GitHub issue reference like "#6")') .argument('[task]', 'Task to execute (or GitHub issue reference like "#6")')
.action(async (task?: string) => { .action(async (task?: string) => {
const opts = program.opts(); const opts = program.opts();
const agentOverrides = resolveAgentOverrides();
// --- Pipeline mode (non-interactive): triggered by --task --- // --- Pipeline mode (non-interactive): triggered by --task ---
if (pipelineMode) { if (pipelineMode) {
@ -328,9 +346,11 @@ program
branch: opts.branch as string | undefined, branch: opts.branch as string | undefined,
autoPr: opts.autoPr === true, autoPr: opts.autoPr === true,
repo: opts.repo as string | undefined, repo: opts.repo as string | undefined,
skipGit: opts.skipGit === true, skipGit: opts.skipGit === true,
cwd: resolvedCwd, cwd: resolvedCwd,
}); provider: agentOverrides?.provider,
model: agentOverrides?.model,
});
if (exitCode !== 0) { if (exitCode !== 0) {
process.exit(exitCode); process.exit(exitCode);
@ -350,7 +370,7 @@ program
if (issueFromOption) { if (issueFromOption) {
try { try {
const resolvedTask = resolveIssueTask(`#${issueFromOption}`); const resolvedTask = resolveIssueTask(`#${issueFromOption}`);
await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions); await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions, agentOverrides);
} catch (e) { } catch (e) {
error(e instanceof Error ? e.message : String(e)); error(e instanceof Error ? e.message : String(e));
process.exit(1); process.exit(1);
@ -371,7 +391,7 @@ program
} }
} }
await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions); await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions, agentOverrides);
return; return;
} }
@ -382,7 +402,7 @@ program
return; return;
} }
await selectAndExecuteTask(resolvedCwd, result.task, prOptions); await selectAndExecuteTask(resolvedCwd, result.task, prOptions, agentOverrides);
}); });
program.parse(); program.parse();

View File

@ -24,7 +24,7 @@ import { autoCommitAndPush } from '../task/autoCommit.js';
import { selectOption, confirm, promptInput } from '../prompt/index.js'; import { selectOption, confirm, promptInput } from '../prompt/index.js';
import { info, success, error as logError, warn } from '../utils/ui.js'; import { info, success, error as logError, warn } from '../utils/ui.js';
import { createLogger } from '../utils/debug.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 { listWorkflows } from '../config/workflowLoader.js';
import { getCurrentWorkflow } from '../config/paths.js'; import { getCurrentWorkflow } from '../config/paths.js';
import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
@ -292,6 +292,7 @@ function getBranchContext(projectDir: string, branch: string): string {
export async function instructBranch( export async function instructBranch(
projectDir: string, projectDir: string,
item: BranchListItem, item: BranchListItem,
options?: TaskExecutionOptions,
): Promise<boolean> { ): Promise<boolean> {
const { branch } = item.info; const { branch } = item.info;
@ -323,7 +324,7 @@ export async function instructBranch(
: instruction; : instruction;
// 5. Execute task on temp clone // 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 // 6. Auto-commit+push if successful
if (taskSuccess) { if (taskSuccess) {
@ -351,7 +352,7 @@ export async function instructBranch(
/** /**
* Main entry point: list branch-based tasks interactively. * Main entry point: list branch-based tasks interactively.
*/ */
export async function listTasks(cwd: string): Promise<void> { export async function listTasks(cwd: string, options?: TaskExecutionOptions): Promise<void> {
log.info('Starting list-tasks'); log.info('Starting list-tasks');
const defaultBranch = detectDefaultBranch(cwd); const defaultBranch = detectDefaultBranch(cwd);
@ -367,7 +368,7 @@ export async function listTasks(cwd: string): Promise<void> {
const items = buildListItems(cwd, branches, defaultBranch); const items = buildListItems(cwd, branches, defaultBranch);
// Build selection options // 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 filesSummary = `${item.filesChanged} file${item.filesChanged !== 1 ? 's' : ''} changed`;
const description = item.originalInstruction const description = item.originalInstruction
? `${filesSummary} | ${item.originalInstruction}` ? `${filesSummary} | ${item.originalInstruction}`
@ -381,7 +382,7 @@ export async function listTasks(cwd: string): Promise<void> {
const selected = await selectOption<string>( const selected = await selectOption<string>(
'List Tasks (Branches)', 'List Tasks (Branches)',
options, menuOptions,
); );
if (selected === null) { if (selected === null) {
@ -406,7 +407,7 @@ export async function listTasks(cwd: string): Promise<void> {
switch (action) { switch (action) {
case 'instruct': case 'instruct':
await instructBranch(cwd, item); await instructBranch(cwd, item, options);
break; break;
case 'try': case 'try':
tryMergeBranch(cwd, item); tryMergeBranch(cwd, item);

View File

@ -12,7 +12,7 @@
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { fetchIssue, formatIssueAsTask, checkGhCli, type GitHubIssue } from '../github/issue.js'; import { fetchIssue, formatIssueAsTask, checkGhCli, type GitHubIssue } from '../github/issue.js';
import { createPullRequest, pushBranch, buildPrBody } from '../github/pr.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 { loadGlobalConfig } from '../config/globalConfig.js';
import { info, error, success, status } from '../utils/ui.js'; import { info, error, success, status } from '../utils/ui.js';
import { createLogger } from '../utils/debug.js'; import { createLogger } from '../utils/debug.js';
@ -23,6 +23,7 @@ import {
EXIT_GIT_OPERATION_FAILED, EXIT_GIT_OPERATION_FAILED,
EXIT_PR_CREATION_FAILED, EXIT_PR_CREATION_FAILED,
} from '../exitCodes.js'; } from '../exitCodes.js';
import type { ProviderType } from '../providers/index.js';
const log = createLogger('pipeline'); const log = createLogger('pipeline');
@ -43,6 +44,8 @@ export interface PipelineExecutionOptions {
skipGit?: boolean; skipGit?: boolean;
/** Working directory */ /** Working directory */
cwd: string; cwd: string;
provider?: ProviderType;
model?: string;
} }
/** /**
@ -184,7 +187,11 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
info(`Running workflow: ${workflow}`); info(`Running workflow: ${workflow}`);
log.info('Pipeline workflow execution starting', { workflow, branch, skipGit, issueNumber: options.issueNumber }); 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) { if (!taskSuccess) {
error(`Workflow '${workflow}' failed`); error(`Workflow '${workflow}' failed`);

View File

@ -18,9 +18,15 @@ import { createLogger } from '../utils/debug.js';
import { getErrorMessage } from '../utils/error.js'; import { getErrorMessage } from '../utils/error.js';
import { executeWorkflow } from './workflowExecution.js'; import { executeWorkflow } from './workflowExecution.js';
import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
import type { ProviderType } from '../providers/index.js';
const log = createLogger('task'); const log = createLogger('task');
export interface TaskExecutionOptions {
provider?: ProviderType;
model?: string;
}
/** /**
* Execute a single task with workflow * Execute a single task with workflow
* @param task - Task content * @param task - Task content
@ -32,7 +38,8 @@ export async function executeTask(
task: string, task: string,
cwd: string, cwd: string,
workflowName: string = DEFAULT_WORKFLOW_NAME, workflowName: string = DEFAULT_WORKFLOW_NAME,
projectCwd?: string projectCwd?: string,
options?: TaskExecutionOptions
): Promise<boolean> { ): Promise<boolean> {
const workflowConfig = loadWorkflow(workflowName); const workflowConfig = loadWorkflow(workflowName);
@ -52,6 +59,8 @@ export async function executeTask(
const result = await executeWorkflow(workflowConfig, task, cwd, { const result = await executeWorkflow(workflowConfig, task, cwd, {
projectCwd, projectCwd,
language: globalConfig.language, language: globalConfig.language,
provider: options?.provider,
model: options?.model,
}); });
return result.success; return result.success;
} }
@ -69,6 +78,7 @@ export async function executeAndCompleteTask(
taskRunner: TaskRunner, taskRunner: TaskRunner,
cwd: string, cwd: string,
workflowName: string, workflowName: string,
options?: TaskExecutionOptions,
): Promise<boolean> { ): Promise<boolean> {
const startedAt = new Date().toISOString(); const startedAt = new Date().toISOString();
const executionLog: string[] = []; const executionLog: string[] = [];
@ -77,7 +87,7 @@ export async function executeAndCompleteTask(
const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName); const { execCwd, execWorkflow, isWorktree } = await resolveTaskExecution(task, cwd, workflowName);
// cwd is always the project root; pass it as projectCwd so reports/sessions go there // 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(); const completedAt = new Date().toISOString();
if (taskSuccess && isWorktree) { if (taskSuccess && isWorktree) {
@ -132,7 +142,8 @@ export async function executeAndCompleteTask(
*/ */
export async function runAllTasks( export async function runAllTasks(
cwd: string, cwd: string,
workflowName: string = DEFAULT_WORKFLOW_NAME workflowName: string = DEFAULT_WORKFLOW_NAME,
options?: TaskExecutionOptions,
): Promise<void> { ): Promise<void> {
const taskRunner = new TaskRunner(cwd); const taskRunner = new TaskRunner(cwd);
@ -155,7 +166,7 @@ export async function runAllTasks(
info(`=== Task: ${task.name} ===`); info(`=== Task: ${task.name} ===`);
console.log(); console.log();
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName); const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options);
if (taskSuccess) { if (taskSuccess) {
successCount++; successCount++;

View File

@ -16,12 +16,13 @@ import {
} from '../utils/ui.js'; } from '../utils/ui.js';
import { executeAndCompleteTask } from './taskExecution.js'; import { executeAndCompleteTask } from './taskExecution.js';
import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
import type { TaskExecutionOptions } from './taskExecution.js';
/** /**
* Watch for tasks and execute them as they appear. * Watch for tasks and execute them as they appear.
* Runs until Ctrl+C. * Runs until Ctrl+C.
*/ */
export async function watchTasks(cwd: string): Promise<void> { export async function watchTasks(cwd: string, options?: TaskExecutionOptions): Promise<void> {
const workflowName = getCurrentWorkflow(cwd) || DEFAULT_WORKFLOW_NAME; const workflowName = getCurrentWorkflow(cwd) || DEFAULT_WORKFLOW_NAME;
const taskRunner = new TaskRunner(cwd); const taskRunner = new TaskRunner(cwd);
const watcher = new TaskWatcher(cwd); const watcher = new TaskWatcher(cwd);
@ -51,7 +52,7 @@ export async function watchTasks(cwd: string): Promise<void> {
info(`=== Task ${taskCount}: ${task.name} ===`); info(`=== Task ${taskCount}: ${task.name} ===`);
console.log(); console.log();
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName); const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName, options);
if (taskSuccess) { if (taskSuccess) {
successCount++; successCount++;

View File

@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs';
import { WorkflowEngine } from '../workflow/engine.js'; import { WorkflowEngine } from '../workflow/engine.js';
import type { WorkflowConfig, Language } from '../models/types.js'; import type { WorkflowConfig, Language } from '../models/types.js';
import type { IterationLimitRequest } from '../workflow/types.js'; import type { IterationLimitRequest } from '../workflow/types.js';
import type { ProviderType } from '../providers/index.js';
import { import {
loadAgentSessions, loadAgentSessions,
updateAgentSession, updateAgentSession,
@ -73,6 +74,8 @@ export interface WorkflowExecutionOptions {
projectCwd?: string; projectCwd?: string;
/** Language for instruction metadata */ /** Language for instruction metadata */
language?: Language; language?: Language;
provider?: ProviderType;
model?: string;
} }
/** /**
@ -182,6 +185,8 @@ export async function executeWorkflow(
onIterationLimit: iterationLimitHandler, onIterationLimit: iterationLimitHandler,
projectCwd, projectCwd,
language: options.language, language: options.language,
provider: options.provider,
model: options.model,
}); });
let abortReason: string | undefined; let abortReason: string | undefined;

View File

@ -226,8 +226,8 @@ export class WorkflowEngine extends EventEmitter {
return { return {
cwd: this.cwd, cwd: this.cwd,
agentPath: step.agentPath, agentPath: step.agentPath,
provider: step.provider, provider: step.provider ?? this.options.provider,
model: step.model, model: step.model ?? this.options.model,
permissionMode: step.permissionMode, permissionMode: step.permissionMode,
onStream: this.options.onStream, onStream: this.options.onStream,
onPermissionRequest: this.options.onPermissionRequest, onPermissionRequest: this.options.onPermissionRequest,

View File

@ -8,6 +8,7 @@
import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js'; import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js';
import type { StreamCallback } from '../agents/runner.js'; import type { StreamCallback } from '../agents/runner.js';
import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js'; import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js';
import type { ProviderType } from '../providers/index.js';
/** Events emitted by workflow engine */ /** Events emitted by workflow engine */
export interface WorkflowEvents { export interface WorkflowEvents {
@ -75,6 +76,8 @@ export interface WorkflowEngineOptions {
projectCwd?: string; projectCwd?: string;
/** Language for instruction metadata. Defaults to 'en'. */ /** Language for instruction metadata. Defaults to 'en'. */
language?: Language; language?: Language;
provider?: ProviderType;
model?: string;
} }
/** Loop detection result */ /** Loop detection result */