From e57e5e7226799bad8119283843880832283cde0a Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:06:12 +0900 Subject: [PATCH] refactor --- bin/takt | 2 +- docs/data-flow-diagrams.md | 562 +++++++++ docs/data-flow.md | 1029 +++++++++++++++++ docs/vertical-slice-migration-map.md | 111 ++ package.json | 2 +- src/__tests__/engine-abort.test.ts | 2 +- src/__tests__/engine-agent-overrides.test.ts | 2 +- src/__tests__/engine-blocked.test.ts | 2 +- src/__tests__/engine-error.test.ts | 2 +- src/__tests__/engine-happy-path.test.ts | 2 +- src/__tests__/engine-parallel.test.ts | 2 +- src/__tests__/engine-test-helpers.ts | 2 +- src/__tests__/engine-worktree-report.test.ts | 4 +- src/__tests__/instructionBuilder.test.ts | 4 +- src/__tests__/it-error-recovery.test.ts | 2 +- src/__tests__/it-instruction-builder.test.ts | 2 +- src/__tests__/it-pipeline-modes.test.ts | 2 +- src/__tests__/it-pipeline.test.ts | 2 +- .../it-three-phase-execution.test.ts | 2 +- src/__tests__/it-workflow-execution.test.ts | 2 +- src/__tests__/it-workflow-patterns.test.ts | 2 +- src/__tests__/parallel-logger.test.ts | 2 +- src/__tests__/transitions.test.ts | 2 +- src/cli.ts | 323 ------ src/cli/commands.ts | 81 ++ src/cli/helpers.ts | 62 + src/cli/index.ts | 18 + src/cli/program.ts | 89 ++ src/cli/routing.ts | 98 ++ src/models/agent.ts | 12 +- src/models/config.ts | 16 +- src/models/index.ts | 9 - src/models/schemas.ts | 29 + src/models/workflow.ts | 53 +- src/workflow/engine/OptionsBuilder.ts | 2 +- src/workflow/engine/ParallelRunner.ts | 6 +- src/workflow/engine/StepExecutor.ts | 4 +- src/workflow/engine/WorkflowEngine.ts | 8 +- src/workflow/{ => engine}/blocked-handler.ts | 4 +- src/workflow/{ => engine}/loop-detector.ts | 4 +- src/workflow/{ => engine}/parallel-logger.ts | 2 +- src/workflow/{ => engine}/phase-runner.ts | 12 +- src/workflow/engine/state-manager.ts | 118 ++ src/workflow/{ => engine}/transitions.ts | 2 +- src/workflow/{ => evaluation}/rule-utils.ts | 2 +- src/workflow/index.ts | 20 +- .../instruction/InstructionBuilder.ts | 8 +- .../instruction/ReportInstructionBuilder.ts | 4 +- .../instruction/StatusJudgmentBuilder.ts | 2 +- src/workflow/instruction/escape.ts | 2 +- .../{ => instruction}/instruction-context.ts | 2 +- .../{ => instruction}/status-rules.ts | 2 +- src/workflow/state-manager.ts | 75 -- 53 files changed, 2270 insertions(+), 545 deletions(-) create mode 100644 docs/data-flow-diagrams.md create mode 100644 docs/data-flow.md create mode 100644 docs/vertical-slice-migration-map.md delete mode 100644 src/cli.ts create mode 100644 src/cli/commands.ts create mode 100644 src/cli/helpers.ts create mode 100644 src/cli/index.ts create mode 100644 src/cli/program.ts create mode 100644 src/cli/routing.ts rename src/workflow/{ => engine}/blocked-handler.ts (90%) rename src/workflow/{ => engine}/loop-detector.ts (92%) rename src/workflow/{ => engine}/parallel-logger.ts (98%) rename src/workflow/{ => engine}/phase-runner.ts (88%) create mode 100644 src/workflow/engine/state-manager.ts rename src/workflow/{ => engine}/transitions.ts (97%) rename src/workflow/{ => evaluation}/rule-utils.ts (90%) rename src/workflow/{ => instruction}/instruction-context.ts (98%) rename src/workflow/{ => instruction}/status-rules.ts (97%) delete mode 100644 src/workflow/state-manager.ts diff --git a/bin/takt b/bin/takt index b479137..5b920e4 100755 --- a/bin/takt +++ b/bin/takt @@ -16,7 +16,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Import the actual CLI from dist -const cliPath = join(__dirname, '..', 'dist', 'cli.js'); +const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.js'); try { await import(cliPath); diff --git a/docs/data-flow-diagrams.md b/docs/data-flow-diagrams.md new file mode 100644 index 0000000..1188dee --- /dev/null +++ b/docs/data-flow-diagrams.md @@ -0,0 +1,562 @@ +# TAKTデータフロー図解 + +このドキュメントでは、TAKTのデータフローをMermaid図で可視化します。 + +## 目次 + +1. [シーケンス図: インタラクティブモードからワークフロー実行まで](#シーケンス図-インタラクティブモードからワークフロー実行まで) +2. [フローチャート: 3フェーズステップ実行](#フローチャート-3フェーズステップ実行) +3. [フローチャート: ルール評価の5段階フォールバック](#フローチャート-ルール評価の5段階フォールバック) +4. [ステートマシン図: WorkflowEngineのステップ遷移](#ステートマシン図-workflowengineのステップ遷移) + +--- + +## シーケンス図: インタラクティブモードからワークフロー実行まで + +```mermaid +sequenceDiagram + participant User + participant CLI as CLI Layer + participant Interactive as Interactive Layer + participant Orchestration as Execution Orchestration + participant TaskExec as Task Execution + participant WorkflowExec as Workflow Execution + participant Engine as WorkflowEngine + participant StepExec as StepExecutor + participant Provider as Provider Layer + + User->>CLI: takt (短い入力 or 引数なし) + CLI->>Interactive: interactiveMode(cwd, initialInput?) + + loop 会話ループ + Interactive->>User: プロンプト表示 + User->>Interactive: メッセージ入力 + Interactive->>Provider: callAI(prompt) + Provider-->>Interactive: AIレスポンス + Interactive->>User: AIレスポンス表示 + end + + User->>Interactive: /go コマンド + Interactive->>Interactive: buildTaskFromHistory() + Interactive-->>CLI: { confirmed: true, task: string } + + CLI->>Orchestration: selectAndExecuteTask(cwd, task) + + Orchestration->>Orchestration: determineWorkflow() + Note over Orchestration: ワークフロー選択
(interactive or override) + + Orchestration->>Orchestration: confirmAndCreateWorktree() + Orchestration->>Provider: summarizeTaskName(task) + Provider-->>Orchestration: taskSlug + Orchestration->>Orchestration: createSharedClone() + + Orchestration->>TaskExec: executeTask(options) + TaskExec->>TaskExec: loadWorkflowByIdentifier() + TaskExec->>WorkflowExec: executeWorkflow(config, task, cwd) + + WorkflowExec->>WorkflowExec: セッション管理初期化 + Note over WorkflowExec: loadAgentSessions()
generateSessionId()
initNdjsonLog() + + WorkflowExec->>Engine: new WorkflowEngine(config, cwd, task, options) + WorkflowExec->>Engine: イベント購読 (step:start, step:complete, etc.) + WorkflowExec->>Engine: engine.run() + + loop ワークフローステップ + Engine->>StepExec: runStep(step) + + StepExec->>StepExec: InstructionBuilder.build() + Note over StepExec: コンテキスト → インストラクション + + StepExec->>Provider: runAgent(instruction) + Note over Provider: Phase 1: Main Execution + Provider-->>StepExec: AgentResponse + + opt step.report 定義あり + StepExec->>Provider: runReportPhase() + Note over Provider: Phase 2: Report Output
(Write-only) + end + + opt tag-based rules あり + StepExec->>Provider: runStatusJudgmentPhase() + Note over Provider: Phase 3: Status Judgment
(no tools) + Provider-->>StepExec: tagContent + end + + StepExec->>StepExec: detectMatchedRule() + Note over StepExec: ルール評価
(5段階フォールバック) + + StepExec-->>Engine: { response, instruction } + Engine->>Engine: resolveNextStep() + + alt nextStep === COMPLETE + Engine-->>WorkflowExec: ワークフロー完了 + else nextStep === ABORT + Engine-->>WorkflowExec: ワークフロー中断 + else 通常ステップ + Engine->>Engine: state.currentStep = nextStep + end + end + + WorkflowExec-->>TaskExec: { success: boolean } + TaskExec-->>Orchestration: taskSuccess + + opt taskSuccess && isWorktree + Orchestration->>Orchestration: autoCommitAndPush() + opt autoPr or user confirms + Orchestration->>Orchestration: createPullRequest() + end + end + + Orchestration-->>User: タスク完了 +``` + +--- + +## フローチャート: 3フェーズステップ実行 + +```mermaid +flowchart TD + Start([ステップ実行開始]) --> BuildInstruction[InstructionBuilder.build] + BuildInstruction --> Phase1{Phase 1:
Main Execution} + + Phase1 --> ContextBuild[コンテキスト収集] + ContextBuild --> |7セクション自動注入| AssemblePrompt[プロンプト組み立て] + AssemblePrompt --> |プレースホルダー置換| CompleteInstruction[完全なインストラクション] + + CompleteInstruction --> RunAgent[runAgent] + RunAgent --> ProviderCall[provider.call] + ProviderCall --> |onStream callback| StreamUI[UI表示] + ProviderCall --> Response1[AgentResponse] + + Response1 --> CheckReport{step.report
定義あり?} + CheckReport -->|Yes| Phase2[Phase 2:
Report Output] + CheckReport -->|No| CheckTag{tag-based
rules あり?} + + Phase2 --> ResumeSession1[セッション継続
sessionId同じ] + ResumeSession1 --> ReportBuilder[ReportInstructionBuilder.build] + ReportBuilder --> WriteOnly[Write-only tools] + WriteOnly --> RunReport[runAgent
レポート出力] + RunReport --> CheckTag + + CheckTag -->|Yes| Phase3[Phase 3:
Status Judgment] + CheckTag -->|No| RuleEval[detectMatchedRule] + + Phase3 --> ResumeSession2[セッション継続
sessionId同じ] + ResumeSession2 --> StatusBuilder[StatusJudgmentBuilder.build] + StatusBuilder --> NoTools[Tools: なし
判断のみ] + NoTools --> RunStatus[runAgent
ステータス出力] + RunStatus --> TagContent[tagContent:
STEP:N タグ] + + TagContent --> RuleEval + RuleEval --> FiveStageFallback[5段階フォールバック] + + FiveStageFallback --> Stage1{1. Aggregate?} + Stage1 -->|Yes| AllAny[all/any 評価] + Stage1 -->|No| Stage2{2. Phase 3 tag?} + + AllAny --> Matched[マッチ!] + + Stage2 -->|Yes| Phase3Tag[STEP:N from
status judgment] + Stage2 -->|No| Stage3{3. Phase 1 tag?} + + Phase3Tag --> Matched + + Stage3 -->|Yes| Phase1Tag[STEP:N from
main output] + Stage3 -->|No| Stage4{4. AI judge
ai rules?} + + Phase1Tag --> Matched + + Stage4 -->|Yes| AIJudge[AI evaluates
ai conditions] + Stage4 -->|No| Stage5[5. AI judge
fallback] + + AIJudge --> Matched + Stage5 --> AIFallback[AI evaluates
all conditions] + AIFallback --> Matched + + Matched --> UpdateResponse[response.matchedRuleIndex
response.matchedRuleMethod] + UpdateResponse --> StoreOutput[state.stepOutputs.set] + StoreOutput --> End([ステップ完了]) + + style Phase1 fill:#e1f5ff + style Phase2 fill:#fff4e6 + style Phase3 fill:#f3e5f5 + style Matched fill:#c8e6c9 +``` + +--- + +## フローチャート: ルール評価の5段階フォールバック + +```mermaid +flowchart TD + Start([ルール評価開始]) --> Input[入力:
step, content, tagContent] + + Input --> Stage1{Stage 1:
Aggregate評価
親ステップ?} + Stage1 -->|Yes| CheckAggregate{rules に
allまたはanyあり?} + CheckAggregate -->|Yes| EvalAggregate[AggregateEvaluator] + EvalAggregate --> CheckAggResult{マッチした?} + CheckAggResult -->|Yes| ReturnAgg[method: aggregate
返却] + CheckAggResult -->|No| Stage2 + + CheckAggregate -->|No| Stage2 + Stage1 -->|No| Stage2{Stage 2:
Phase 3 tag
tagContent に
STEP:N あり?} + + Stage2 -->|Yes| ExtractTag3[正規表現で抽出:
STEP:(\d+)] + ExtractTag3 --> ValidateIndex3{index が
rules 範囲内?} + ValidateIndex3 -->|Yes| ReturnTag3[method: phase3_tag
返却] + ValidateIndex3 -->|No| Stage3 + + Stage2 -->|No| Stage3{Stage 3:
Phase 1 tag
content に
STEP:N あり?} + + Stage3 -->|Yes| ExtractTag1[正規表現で抽出:
STEP:(\d+)] + ExtractTag1 --> ValidateIndex1{index が
rules 範囲内?} + ValidateIndex1 -->|Yes| ReturnTag1[method: phase1_tag
返却] + ValidateIndex1 -->|No| Stage4 + + Stage3 -->|No| Stage4{Stage 4:
AI judge
ai rules あり?} + + Stage4 -->|Yes| FilterAI[aiルールのみ抽出
ai 関数パース] + FilterAI --> CallAI[AIJudgeEvaluator
condition を評価] + CallAI --> CheckAIResult{マッチした?} + CheckAIResult -->|Yes| ReturnAI[method: ai_judge
返却] + CheckAIResult -->|No| Stage5 + + Stage4 -->|No| Stage5[Stage 5:
AI judge fallback
全条件を評価] + + Stage5 --> AllConditions[全ルール条件を収集] + AllConditions --> CallAIFallback[AIJudgeEvaluator
全条件を評価] + CallAIFallback --> CheckFallbackResult{マッチした?} + CheckFallbackResult -->|Yes| ReturnFallback[method: ai_judge_fallback
返却] + CheckFallbackResult -->|No| NoMatch[null 返却
マッチなし] + + ReturnAgg --> End([返却:
index, method]) + ReturnTag3 --> End + ReturnTag1 --> End + ReturnAI --> End + ReturnFallback --> End + NoMatch --> End + + style Stage1 fill:#e3f2fd + style Stage2 fill:#fff3e0 + style Stage3 fill:#fce4ec + style Stage4 fill:#f3e5f5 + style Stage5 fill:#e8f5e9 + style End fill:#c8e6c9 + style NoMatch fill:#ffcdd2 +``` + +--- + +## ステートマシン図: WorkflowEngineのステップ遷移 + +```mermaid +stateDiagram-v2 + [*] --> Initializing: new WorkflowEngine + + Initializing --> Running: engine.run() + note right of Initializing + state = { + status: 'running', + currentStep: initialStep, + iteration: 0, + ... + } + end note + + state Running { + [*] --> CheckAbort: while loop + + CheckAbort --> CheckIteration: abortRequested? + CheckAbort --> Aborted: Yes → abort + + CheckIteration --> CheckLoop: iteration < max? + CheckIteration --> IterationLimit: No → emit iteration:limit + + IterationLimit --> UserDecision: onIterationLimit callback + UserDecision --> CheckLoop: 追加イテレーション許可 + UserDecision --> Aborted: 拒否 + + CheckLoop --> GetStep: loopDetector.check() + CheckLoop --> Aborted: loop detected + + GetStep --> BuildInstruction: getStep(currentStep) + + BuildInstruction --> EmitStart: InstructionBuilder + + EmitStart --> RunStep: emit step:start + + RunStep --> EmitComplete: runStep(step) + note right of RunStep + - Normal: StepExecutor + - Parallel: ParallelRunner + 3-phase execution + end note + + EmitComplete --> CheckBlocked: emit step:complete + + CheckBlocked --> HandleBlocked: status === blocked? + CheckBlocked --> EvaluateRules: No + + HandleBlocked --> UserInput: handleBlocked() + UserInput --> CheckAbort: ユーザー入力追加 + UserInput --> Aborted: キャンセル + + EvaluateRules --> ResolveNext: detectMatchedRule() + + ResolveNext --> CheckNext: determineNextStepByRules() + + CheckNext --> Completed: nextStep === COMPLETE + CheckNext --> Aborted: nextStep === ABORT + CheckNext --> Transition: 通常ステップ + + Transition --> CheckAbort: state.currentStep = nextStep + } + + Running --> Completed: workflow:complete + Running --> Aborted: workflow:abort + + Completed --> [*]: return state + Aborted --> [*]: return state + + note right of Completed + state.status = 'completed' + emit workflow:complete + end note + + note right of Aborted + state.status = 'aborted' + emit workflow:abort + 原因: + - User abort (Ctrl+C) + - Iteration limit + - Loop detected + - Blocked without input + - Step execution error + end note +``` + +--- + +## データ変換の流れ + +```mermaid +flowchart LR + subgraph Input ["入力"] + A1[ユーザー入力
CLI引数] + A2[会話履歴
ConversationMessage] + end + + subgraph Transform1 ["変換1: タスク化"] + B1[isDirectTask判定] + B2[buildTaskFromHistory] + end + + subgraph Task ["タスク"] + C[task: string] + end + + subgraph Transform2 ["変換2: 環境準備"] + D1[determineWorkflow] + D2[summarizeTaskName
AI呼び出し] + D3[createSharedClone] + end + + subgraph Execution ["実行環境"] + E1[workflowIdentifier] + E2[execCwd, branch] + end + + subgraph Transform3 ["変換3: 設定読み込み"] + F1[loadWorkflowByIdentifier] + F2[loadAgentSessions] + end + + subgraph Config ["設定"] + G1[WorkflowConfig] + G2[initialSessions] + end + + subgraph Transform4 ["変換4: 状態初期化"] + H[createInitialState] + end + + subgraph State ["実行状態"] + I[WorkflowState] + end + + subgraph Transform5 ["変換5: インストラクション"] + J[InstructionBuilder.build] + end + + subgraph Instruction ["プロンプト"] + K[instruction: string] + end + + subgraph Transform6 ["変換6: AI実行"] + L[provider.call] + end + + subgraph Response ["応答"] + M[AgentResponse] + end + + subgraph Transform7 ["変換7: ルール評価"] + N[detectMatchedRule] + end + + subgraph Transition ["遷移"] + O[nextStep: string] + end + + A1 --> B1 + A2 --> B2 + B1 --> C + B2 --> C + + C --> D1 + C --> D2 + D1 --> E1 + D2 --> D3 + D3 --> E2 + + E1 --> F1 + E2 --> F2 + F1 --> G1 + F2 --> G2 + + G1 --> H + G2 --> H + H --> I + + I --> J + C --> J + J --> K + + K --> L + L --> M + + M --> N + I --> N + N --> O + + O -.-> I + + style Input fill:#e3f2fd + style Task fill:#fff3e0 + style Execution fill:#fce4ec + style Config fill:#f3e5f5 + style State fill:#e8f5e9 + style Instruction fill:#fff9c4 + style Response fill:#f1f8e9 + style Transition fill:#c8e6c9 +``` + +--- + +## コンテキスト蓄積の流れ + +```mermaid +flowchart TB + subgraph Initial ["初期入力"] + A[task: string] + end + + subgraph Context1 ["コンテキスト1: タスク"] + B[InstructionContext] + B1[task] + end + + subgraph Step1 ["ステップ1実行"] + C1[Phase 1: Main] + C2[Phase 2: Report] + C3[Phase 3: Status] + C4[AgentResponse] + end + + subgraph Context2 ["コンテキスト2: +前回応答"] + D[InstructionContext] + D1[task] + D2[previousOutput] + end + + subgraph Blocked ["Blocked発生"] + E[handleBlocked] + E1[ユーザー追加入力] + end + + subgraph Context3 ["コンテキスト3: +ユーザー入力"] + F[InstructionContext] + F1[task] + F2[previousOutput] + F3[userInputs] + end + + subgraph Step2 ["ステップ2実行"] + G1[Phase 1: Main] + G2[stepOutputs蓄積] + G3[AgentResponse] + end + + subgraph Context4 ["コンテキスト4: 完全"] + H[InstructionContext] + H1[task] + H2[previousOutput] + H3[userInputs] + H4[iteration] + H5[stepIteration] + H6[reportDir] + H7[...すべてのメタデータ] + end + + A --> B1 + B1 --> C1 + C1 --> C2 + C2 --> C3 + C3 --> C4 + + C4 --> D2 + B1 --> D1 + + D --> E + E --> E1 + E1 --> F3 + D1 --> F1 + D2 --> F2 + + F --> G1 + G1 --> G2 + G2 --> G3 + + G3 --> H2 + F1 --> H1 + F3 --> H3 + G2 --> H4 + G2 --> H5 + G2 --> H6 + + H -.繰り返し.-> Step2 + + style Initial fill:#e3f2fd + style Context1 fill:#fff3e0 + style Step1 fill:#fce4ec + style Context2 fill:#fff9c4 + style Blocked fill:#ffcdd2 + style Context3 fill:#f1f8e9 + style Step2 fill:#c8e6c9 + style Context4 fill:#dcedc8 +``` + +--- + +## まとめ + +これらの図は、TAKTのデータフローを以下の視点から可視化しています: + +1. **シーケンス図**: 時系列での各レイヤー間のやりとり +2. **3フェーズフローチャート**: ステップ実行の詳細な処理フロー +3. **ルール評価フローチャート**: 5段階フォールバックの意思決定ロジック +4. **ステートマシン**: WorkflowEngineの状態遷移 +5. **データ変換図**: 各段階でのデータ形式変換 +6. **コンテキスト蓄積図**: 実行が進むにつれてコンテキストが蓄積される様子 + +これらの図を `data-flow.md` と合わせて参照することで、TAKTのアーキテクチャを多角的に理解できます。 diff --git a/docs/data-flow.md b/docs/data-flow.md new file mode 100644 index 0000000..ec20d03 --- /dev/null +++ b/docs/data-flow.md @@ -0,0 +1,1029 @@ +# TAKTデータフロー解析 + +このドキュメントでは、TAKTにおけるデータフロー、特にインタラクティブモードからワークフロー実行に至るまでのデータの流れを説明します。 + +## 目次 + +1. [概要](#概要) +2. [全体フロー図](#全体フロー図) +3. [各レイヤーの詳細](#各レイヤーの詳細) +4. [データフローの段階](#データフローの段階) +5. [重要な変換ポイント](#重要な変換ポイント) + +--- + +## 概要 + +TAKTのデータフローは以下の7つの主要なレイヤーで構成されています: + +1. **CLI Layer** - ユーザー入力の受付 +2. **Interactive Layer** - タスクの対話的な明確化 +3. **Execution Orchestration Layer** - ワークフロー選択とworktree管理 +4. **Workflow Execution Layer** - セッション管理とイベント処理 +5. **Engine Layer** - ステートマシンによるステップ実行 +6. **Instruction Building Layer** - プロンプト生成 +7. **Provider Layer** - AIプロバイダーとの通信 + +--- + +## 全体フロー図 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 1. CLI Layer (cli.ts) │ +│ ユーザー入力 → 引数パース → コマンド振り分け │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ┌────────────┴──────────────┐ + │ │ + Direct Task Input Short Input / No Args + │ │ + │ ▼ + │ ┌─────────────────────────────────┐ + │ │ 2. Interactive Layer │ + │ │ (interactive.ts) │ + │ │ │ + │ │ ┌─────────────────────┐ │ + │ │ │ User Conversation │ │ + │ │ │ - Clarification │ │ + │ │ │ - Codebase Search │ │ + │ │ │ - AI Response │ │ + │ │ └──────┬──────────────┘ │ + │ │ │ │ + │ │ ▼ │ + │ │ User confirms with /go │ + │ │ │ │ + │ │ ▼ │ + │ │ buildTaskFromHistory() │ + │ │ (会話履歴 → タスク文字列) │ + │ └─────────┬───────────────────────┘ + │ │ + └────────────────────────┘ + │ + │ task: string + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. Execution Orchestration Layer │ +│ (selectAndExecute.ts) │ +│ │ +│ ┌──────────────────────┐ │ +│ │ determineWorkflow() │ ← workflow選択 (interactive/override) │ +│ └─────────┬────────────┘ │ +│ │ workflowIdentifier: string │ +│ ▼ │ +│ ┌──────────────────────────────────┐ │ +│ │ confirmAndCreateWorktree() │ │ +│ │ - AI branchname generation │ │ +│ │ - createSharedClone() │ │ +│ └─────────┬────────────────────────┘ │ +│ │ { execCwd, isWorktree, branch } │ +│ ▼ │ +│ ┌──────────────────────────────────┐ │ +│ │ executeTask() │ │ +│ │ - task: string │ │ +│ │ - cwd: string (実行ディレクトリ) │ │ +│ │ - workflowIdentifier: string │ │ +│ │ - projectCwd: string (.takt/在処) │ │ +│ └─────────┬────────────────────────┘ │ +└────────────┼────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 4. Workflow Execution Layer │ +│ (workflowExecution.ts, taskExecution.ts) │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ loadWorkflowByIdentifier() │ │ +│ │ → WorkflowConfig │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────┐ │ +│ │ Session Management │ │ +│ │ - loadAgentSessions() │ ← projectCwd or cwd │ +│ │ - generateSessionId() │ │ +│ │ - createSessionLog() │ │ +│ │ - initNdjsonLog() │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────┐ │ +│ │ WorkflowEngine initialization │ │ +│ │ │ │ +│ │ new WorkflowEngine( │ │ +│ │ config: WorkflowConfig, │ │ +│ │ cwd: string, │ │ +│ │ task: string, │ │ +│ │ options: { │ │ +│ │ onStream, │ ← StreamDisplay handler │ +│ │ initialSessions, │ ← 保存済みセッションID │ +│ │ onSessionUpdate, │ ← セッション更新callback │ +│ │ projectCwd, │ │ +│ │ language, │ │ +│ │ provider, │ │ +│ │ model │ │ +│ │ } │ │ +│ │ ) │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────┐ │ +│ │ Event Subscription │ │ +│ │ - step:start │ │ +│ │ - step:complete │ │ +│ │ - step:report │ │ +│ │ - workflow:complete │ │ +│ │ - workflow:abort │ │ +│ └────────┬───────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────┐ │ +│ │ engine.run() │ │ +│ └────────┬───────────────────────┘ │ +└───────────┼─────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 5. Engine Layer (WorkflowEngine.ts) │ +│ │ +│ ┌────────────────────────────────────────┐ │ +│ │ State Machine Loop │ │ +│ │ │ │ +│ │ while (state.status === 'running') { │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 1. Iteration & Loop Check │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 2. Get Current Step │ │ │ +│ │ │ step = getStep( │ │ │ +│ │ │ state.currentStep │ │ │ +│ │ │ ) │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 3. Build Instruction │ │ ← InstructionBuilder +│ │ │ (if not parallel) │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 4. Emit step:start │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 5. runStep() │ │ │ +│ │ │ ├─ Normal: StepExecutor │ │ ← 3-phase execution +│ │ │ └─ Parallel: ParallelRunner │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ │ { response, instruction } │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 6. Emit step:complete │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 7. Handle Blocked │ │ │ +│ │ │ (if status === 'blocked') │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 8. Rule Evaluation │ │ ← RuleEvaluator │ +│ │ │ resolveNextStep() │ │ │ +│ │ └────────────┬───────────────────┘ │ │ +│ │ │ │ │ +│ │ │ nextStep: string │ │ +│ │ ▼ │ │ +│ │ ┌────────────────────────────────┐ │ │ +│ │ │ 9. Transition │ │ │ +│ │ │ - COMPLETE → break │ │ │ +│ │ │ - ABORT → break │ │ │ +│ │ │ - other → update state │ │ │ +│ │ └────────────────────────────────┘ │ │ +│ │ } │ │ +│ └────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ (from runStep) +┌─────────────────────────────────────────────────────────────────┐ +│ 6. Instruction Building & Step Execution Layer │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐│ +│ │ StepExecutor.runNormalStep() ││ +│ │ ││ +│ │ ┌──────────────────────────────────────────────────────┐ ││ +│ │ │ Phase 1: Main Execution │ ││ +│ │ │ │ ││ +│ │ │ InstructionBuilder.build() │ ││ +│ │ │ ├─ Execution Context (cwd, permission) │ ││ +│ │ │ ├─ Workflow Context (iteration, step, report) │ ││ +│ │ │ ├─ User Request ({task}) │ ││ +│ │ │ ├─ Previous Response ({previous_response}) │ ││ +│ │ │ ├─ Additional User Inputs ({user_inputs}) │ ││ +│ │ │ ├─ Instructions (instruction_template) │ ││ +│ │ │ └─ Status Output Rules (tag-based) │ ││ +│ │ │ │ ││ +│ │ │ → instruction: string │ ││ +│ │ │ │ ││ +│ │ │ runAgent(agent, instruction, options) │ ││ +│ │ │ → response: AgentResponse │ ││ +│ │ └──────────────────────┬───────────────────────────────┘ ││ +│ │ │ ││ +│ │ ▼ ││ +│ │ ┌──────────────────────────────────────────────────────┐ ││ +│ │ │ Phase 2: Report Output (if step.report defined) │ ││ +│ │ │ │ ││ +│ │ │ runReportPhase() │ ││ +│ │ │ - Resume session │ ││ +│ │ │ - Write-only tools │ ││ +│ │ │ - ReportInstructionBuilder │ ││ +│ │ └──────────────────────┬───────────────────────────────┘ ││ +│ │ │ ││ +│ │ ▼ ││ +│ │ ┌──────────────────────────────────────────────────────┐ ││ +│ │ │ Phase 3: Status Judgment (if tag-based rules) │ ││ +│ │ │ │ ││ +│ │ │ runStatusJudgmentPhase() │ ││ +│ │ │ - Resume session │ ││ +│ │ │ - No tools (judgment only) │ ││ +│ │ │ - StatusJudgmentBuilder │ ││ +│ │ │ → tagContent: string │ ││ +│ │ └──────────────────────┬───────────────────────────────┘ ││ +│ │ │ ││ +│ │ ▼ ││ +│ │ ┌──────────────────────────────────────────────────────┐ ││ +│ │ │ Rule Evaluation │ ││ +│ │ │ │ ││ +│ │ │ detectMatchedRule(step, content, tagContent) │ ││ +│ │ │ 1. Aggregate (all()/any()) │ ││ +│ │ │ 2. Phase 3 tag ([STEP:N]) │ ││ +│ │ │ 3. Phase 1 tag (fallback) │ ││ +│ │ │ 4. AI judge (ai("...")) │ ││ +│ │ │ 5. AI judge fallback (all conditions) │ ││ +│ │ │ → { index, method } │ ││ +│ │ └──────────────────────┬───────────────────────────────┘ ││ +│ │ │ ││ +│ │ ▼ ││ +│ │ response with matchedRuleIndex & matchedRuleMethod ││ +│ └────────────────────────────────────────────────────────────┘│ +└────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 7. Provider Layer (agents/runner.ts → providers/) │ +│ │ +│ ┌────────────────────────────────────────┐ │ +│ │ runAgent() │ │ +│ │ - Resolve agent spec │ │ +│ │ - Get provider │ │ +│ │ - Call provider.call() │ │ +│ └────────────┬───────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────┐ │ +│ │ Provider.call() │ │ +│ │ (ClaudeProvider / CodexProvider) │ │ +│ │ │ │ +│ │ - Build system prompt │ │ +│ │ - Call SDK (callClaude / callCodex) │ │ +│ │ - Stream handling (onStream callback) │ │ +│ │ - Error propagation │ │ +│ │ │ │ +│ │ → { status, content, sessionId, ... } │ │ +│ └────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 各レイヤーの詳細 + +### 1. CLI Layer (`src/cli.ts`) + +**役割**: ユーザー入力の受付とコマンド振り分け + +**主要な処理**: +- コマンドライン引数のパース +- 入力タイプの判定: + - `isDirectTask()`: 複数単語またはissue参照 → 直接実行 + - 短い単語または引数なし → インタラクティブモード +- グローバル設定の初期化 (`initGlobalDirs`, `initProjectDirs`) +- パイプラインモード vs 通常モードの判定 + +**データ入力**: +- CLI引数: `task`, `--workflow`, `--issue`, など + +**データ出力**: +- `task: string` (タスク記述) +- `workflow: string | undefined` (ワークフロー名またはパス) +- `createWorktree: boolean | undefined` +- その他オプション + +--- + +### 2. Interactive Layer (`src/commands/interactive/interactive.ts`) + +**役割**: タスクの対話的な明確化 + +**主要な処理**: +1. **会話ループ**: + - `readLine()`: ユーザー入力を1行ずつ読み込み + - `callAI()`: AIプロバイダーを呼び出し + - 履歴管理: `ConversationMessage[]` + +2. **セッション管理**: + - `loadAgentSessions()`: 過去のセッションを復元 + - `updateAgentSession()`: セッションIDを更新・保存 + +3. **スラッシュコマンド**: + - `/go`: タスク確定、実行へ進む + - `/cancel`: キャンセル + - `Ctrl+D`: EOF、キャンセル + +4. **タスク組み立て**: + - `buildTaskFromHistory()`: 会話履歴を結合してタスク文字列を生成 + +**データ入力**: +- `initialInput?: string` (CLI引数から) +- ユーザーの対話入力 + +**データ出力**: +- `InteractiveModeResult`: + - `confirmed: boolean` + - `task: string` (会話履歴全体を結合した文字列) + +--- + +### 3. Execution Orchestration Layer (`src/commands/execution/selectAndExecute.ts`) + +**役割**: ワークフロー選択とworktree管理 + +**主要な処理**: + +1. **ワークフロー決定** (`determineWorkflow()`): + - オーバーライド指定がある場合: + - パス形式 → そのまま使用 + - 名前形式 → バリデーション + - オーバーライドなし → インタラクティブ選択 (`selectWorkflow()`) + +2. **Worktree作成** (`confirmAndCreateWorktree()`): + - ユーザー確認 (または `--create-worktree` フラグ) + - ブランチ名生成 (`summarizeTaskName()` - AIでタスクから英語スラグ生成) + - `createSharedClone()`: git clone --shared で軽量クローン作成 + +3. **タスク実行開始** (`selectAndExecuteTask()`): + - `executeTask()` を呼び出し + - 成功時: Auto-commit & Push + - PR作成 (オプション) + +**データ入力**: +- `task: string` +- `options?: SelectAndExecuteOptions`: + - `workflow?: string` + - `createWorktree?: boolean` + - `autoPr?: boolean` +- `agentOverrides?: TaskExecutionOptions` + +**データ出力**: +- `{ execCwd, isWorktree, branch }` +- タスク実行成功/失敗 + +--- + +### 4. Workflow Execution Layer + +#### 4.1 Task Execution (`src/commands/execution/taskExecution.ts`) + +**役割**: ワークフロー読み込みと実行の橋渡し + +**主要な処理**: +1. `loadWorkflowByIdentifier()`: YAMLまたは名前からワークフロー設定を読み込み +2. `executeWorkflow()` を呼び出し + +**データ入力**: +- `ExecuteTaskOptions`: + - `task: string` + - `cwd: string` (実行ディレクトリ、cloneまたはプロジェクトルート) + - `workflowIdentifier: string` + - `projectCwd: string` (`.takt/`がある場所) + - `agentOverrides?: TaskExecutionOptions` + +**データ出力**: +- `boolean` (成功/失敗) + +#### 4.2 Workflow Execution (`src/commands/execution/workflowExecution.ts`) + +**役割**: セッション管理、イベント購読、ログ記録 + +**主要な処理**: + +1. **セッション管理**: + - `generateSessionId()`: ワークフローセッションID生成 + - `loadAgentSessions()` / `loadWorktreeSessions()`: エージェントセッション復元 + - `updateAgentSession()` / `updateWorktreeSession()`: セッション保存 + +2. **ログ初期化**: + - `createSessionLog()`: セッションログオブジェクト作成 + - `initNdjsonLog()`: NDJSON形式のログファイル初期化 + - `updateLatestPointer()`: `latest.json` ポインタ更新 + +3. **WorkflowEngine初期化**: + ```typescript + new WorkflowEngine(workflowConfig, cwd, task, { + onStream: streamHandler, // UI表示用ストリームハンドラ + initialSessions: savedSessions, // 保存済みセッションID + onSessionUpdate: sessionUpdateHandler, + onIterationLimit: iterationLimitHandler, + projectCwd, + language, + provider, + model + }) + ``` + +4. **イベント購読**: + - `step:start`: ステップ開始 → UI表示、NDJSON記録 + - `step:complete`: ステップ完了 → UI表示、NDJSON記録、セッション更新 + - `step:report`: レポートファイル出力 + - `workflow:complete`: ワークフロー完了 → 通知 + - `workflow:abort`: ワークフロー中断 → エラー通知 + +5. **SIGINT処理**: + - 1回目: Graceful abort (`engine.abort()`) + - 2回目: 強制終了 + +**データ入力**: +- `WorkflowConfig` +- `task: string` +- `cwd: string` +- `WorkflowExecutionOptions` + +**データ出力**: +- `WorkflowExecutionResult`: + - `success: boolean` + - `reason?: string` + +--- + +### 5. Engine Layer (`src/workflow/engine/WorkflowEngine.ts`) + +**役割**: ステートマシンによるワークフロー実行制御 + +**主要な構成要素**: + +1. **State管理** (`WorkflowState`): + - `status`: 'running' | 'completed' | 'aborted' + - `currentStep`: 現在実行中のステップ名 + - `iteration`: ワークフロー全体のイテレーション数 + - `stepIterations`: Map (ステップごとの実行回数) + - `agentSessions`: Map (エージェントごとのセッションID) + - `stepOutputs`: Map (各ステップの出力) + - `userInputs`: string[] (blocked時のユーザー追加入力) + +2. **コンポーネント**: + - `OptionsBuilder`: エージェント実行オプション構築 + - `StepExecutor`: 通常ステップの3フェーズ実行 + - `ParallelRunner`: 並列ステップの実行 + +3. **主要メソッド**: + + **`run()`**: メインループ + ```typescript + while (state.status === 'running') { + // 1. Abort & Iteration チェック + if (abortRequested) { ... } + if (iteration >= maxIterations) { ... } + + // 2. ステップ取得 + const step = getStep(state.currentStep); + + // 3. ループ検出 + const loopCheck = loopDetector.check(step.name); + + // 4. インストラクション構築 (非並列の場合) + const instruction = stepExecutor.buildInstruction(...); + + // 5. イベント発行 + emit('step:start', step, iteration, instruction); + + // 6. ステップ実行 + const { response, instruction } = await runStep(step, instruction); + + // 7. イベント発行 + emit('step:complete', step, response, instruction); + + // 8. Blocked処理 + if (response.status === 'blocked') { ... } + + // 9. ルール評価 + const nextStep = resolveNextStep(step, response); + + // 10. 遷移 + if (nextStep === COMPLETE_STEP) { break; } + if (nextStep === ABORT_STEP) { break; } + state.currentStep = nextStep; + } + ``` + + **`runStep()`**: ステップ実行の委譲 + - 並列ステップ → `ParallelRunner.runParallelStep()` + - 通常ステップ → `StepExecutor.runNormalStep()` + + **`resolveNextStep()`**: ルール評価によるステップ遷移決定 + - `response.matchedRuleIndex` を使用 + - `determineNextStepByRules()` で次ステップ名を取得 + +**データ入力**: +- `WorkflowConfig` +- `cwd: string` +- `task: string` +- `WorkflowEngineOptions` + +**データ出力**: +- `WorkflowState` (最終状態) +- イベント発行 (各ステップの進捗) + +--- + +### 6. Instruction Building & Step Execution Layer + +#### 6.1 Step Execution (`src/workflow/engine/StepExecutor.ts`) + +**役割**: 3フェーズモデルによるステップ実行 + +**3フェーズの詳細**: + +**Phase 1: Main Execution** +- 目的: エージェントのメインタスク実行 +- Tools: ステップで指定されたツール (ただし `step.report` がある場合は Write を除外) +- インストラクション: `InstructionBuilder.build()` + +**Phase 2: Report Output** (オプション、`step.report` がある場合のみ) +- 目的: レポートファイルへの出力 +- Tools: **Writeのみ** +- インストラクション: `ReportInstructionBuilder.build()` +- セッション: Phase 1と同じセッションを継続 (resume) + +**Phase 3: Status Judgment** (オプション、tag-based rulesがある場合のみ) +- 目的: ステータスタグの出力 +- Tools: **なし** (判断のみ) +- インストラクション: `StatusJudgmentBuilder.build()` +- セッション: Phase 1と同じセッションを継続 (resume) +- 出力: `[STEP:N]` 形式のタグ + +**主要メソッド**: + +**`runNormalStep()`**: +```typescript +// Phase 1 +const response = await runAgent(step.agent, instruction, options); +updateAgentSession(step.agent, response.sessionId); + +// Phase 2 (if step.report) +if (step.report) { + await runReportPhase(step, stepIteration, context); +} + +// Phase 3 (if tag-based rules) +let tagContent = ''; +if (needsStatusJudgmentPhase(step)) { + tagContent = await runStatusJudgmentPhase(step, context); +} + +// Rule evaluation +const match = await detectMatchedRule(step, response.content, tagContent, {...}); +``` + +**`buildInstruction()`**: +- `InstructionBuilder` を使用してインストラクション文字列を生成 +- コンテキスト情報を渡す + +#### 6.2 Instruction Building (`src/workflow/instruction/InstructionBuilder.ts`) + +**役割**: Phase 1用のインストラクション文字列生成 + +**自動注入セクション**: + +1. **Execution Context** (実行環境メタデータ): + - Working directory + - Permission rules (edit mode) + +2. **Workflow Context**: + - Iteration (workflow-wide) + - Step Iteration (per-step) + - Step name + - Report Directory/File info + +3. **User Request** (タスク本文): + - `{task}` プレースホルダーがテンプレートにない場合のみ自動注入 + +4. **Previous Response** (前ステップの出力): + - `step.passPreviousResponse === true` かつ + - `{previous_response}` プレースホルダーがテンプレートにない場合のみ自動注入 + +5. **Additional User Inputs** (blocked時の追加入力): + - `{user_inputs}` プレースホルダーがテンプレートにない場合のみ自動注入 + +6. **Instructions** (ステップ固有のテンプレート): + - `step.instructionTemplate` の内容 + - プレースホルダー置換: `{task}`, `{previous_response}`, `{iteration}`, など + +7. **Status Output Rules** (tag-based rules用): + - `hasTagBasedRules(step)` の場合のみ + - `generateStatusRulesFromRules()` で生成 + +**プレースホルダー置換**: +- `{task}`: ユーザーリクエスト +- `{previous_response}`: 前ステップの出力 +- `{user_inputs}`: 追加ユーザー入力 +- `{iteration}`: ワークフロー全体のイテレーション +- `{max_iterations}`: 最大イテレーション +- `{step_iteration}`: ステップのイテレーション +- `{report_dir}`: レポートディレクトリ + +**ロケール対応**: +- `language: 'en' | 'ja'` +- セクション見出しや説明文が言語に応じて切り替わる + +--- + +### 7. Provider Layer + +#### 7.1 Agent Runner (`src/agents/runner.ts`) + +**役割**: エージェント仕様の解決とプロバイダー呼び出し + +**主要な処理**: +1. **エージェント仕様解決**: + - ビルトインエージェント (`coder`, `architect`, など) + - カスタムエージェント (`.takt/agents.yaml`) + - プロンプトファイル (`.md`) + +2. **プロバイダー取得**: + - `getProvider(providerType)`: ClaudeProvider / CodexProvider / MockProvider + +3. **エージェント呼び出し**: + - `provider.call(agentName, instruction, options)` + +**データ入力**: +- `agent: string` (エージェント名またはパス) +- `instruction: string` (構築済みインストラクション) +- `AgentRunOptions`: + - `cwd: string` + - `sessionId?: string` + - `allowedTools?: string[]` + - `provider?: ProviderType` + - `model?: string` + - `onStream?: StreamHandler` + +**データ出力**: +- `AgentResponse`: + - `agent: string` + - `status: 'success' | 'blocked'` + - `content: string` + - `sessionId?: string` + - `error?: string` + - `timestamp: Date` + +#### 7.2 Provider (`src/providers/`) + +**役割**: AIプロバイダー(Claude, Codex)とのSDK通信 + +**主要なプロバイダー**: +- `ClaudeProvider`: Claude Code SDK (`@anthropic-ai/claude-agent-sdk`) +- `CodexProvider`: Codex API +- `MockProvider`: テスト用 + +**主要メソッド**: + +**`call()`**: +```typescript +async call( + agentName: string, + instruction: string, + options: ProviderCallOptions +): Promise +``` + +**処理内容**: +1. システムプロンプト構築 +2. SDK呼び出し (`callClaude()` / `callCodex()`) +3. ストリーミング処理 (`onStream` callback) +4. エラーハンドリング +5. レスポンス変換 + +**データ入力**: +- `agentName: string` +- `instruction: string` +- `ProviderCallOptions`: + - `cwd: string` + - `sessionId?: string` + - `systemPrompt?: string` + - `allowedTools?: string[]` + - `model?: string` + - `onStream?: StreamHandler` + +**データ出力**: +- `AgentResponse` (上記と同じ) + +--- + +## データフローの段階 + +### ステージ1: タスク入力 + +**入力方法**: +1. **直接タスク**: `takt "Fix the login bug"` +2. **Issue参照**: `takt #123` +3. **インタラクティブモード**: `takt` または `takt a` + +**データ変換**: +- インタラクティブモード: `ConversationMessage[]` → `task: string` + - `buildTaskFromHistory()`: 会話履歴を結合 + +**出力**: `task: string` + +--- + +### ステージ2: 実行環境準備 + +**ワークフロー選択**: +- `--workflow` フラグ → 検証 +- なし → インタラクティブ選択 (`selectWorkflow()`) + +**Worktree作成** (オプション): +- `confirmAndCreateWorktree()`: + - ユーザー確認または `--create-worktree` フラグ + - `summarizeTaskName()`: タスク → 英語スラグ (AI呼び出し) + - `createSharedClone()`: git clone --shared + +**データ**: +- `workflowIdentifier: string` +- `{ execCwd, isWorktree, branch }` + +--- + +### ステージ3: ワークフロー実行初期化 + +**セッション管理**: +- `loadAgentSessions()`: 保存済みセッション復元 +- `generateSessionId()`: ワークフローセッションID生成 +- `initNdjsonLog()`: NDJSON ログファイル作成 + +**WorkflowEngine作成**: +```typescript +new WorkflowEngine(workflowConfig, cwd, task, { + onStream, + initialSessions, + onSessionUpdate, + projectCwd, + language, + provider, + model +}) +``` + +**データ**: +- `WorkflowState`: 初期状態 + - `currentStep = config.initialStep` + - `iteration = 0` + - `agentSessions = initialSessions` + +--- + +### ステージ4: ステップ実行ループ + +**各イテレーション**: + +1. **ステップ取得**: `getStep(state.currentStep)` +2. **インストラクション構築**: `InstructionBuilder.build()` +3. **ステップ実行**: 3フェーズ実行 +4. **ルール評価**: `detectMatchedRule()` +5. **ステップ遷移**: `resolveNextStep()` → 次のステップ名 + +**データ変換**: +- `task + context` → `instruction: string` +- `instruction` → `AgentResponse` (via Provider) +- `AgentResponse + rules` → `matchedRuleIndex` +- `matchedRuleIndex` → `nextStep: string` + +--- + +### ステージ5: インストラクション生成 + +**InstructionBuilder処理**: + +1. **コンテキスト収集**: + - `task`: 元のユーザーリクエスト + - `iteration`, `maxIterations`: イテレーション情報 + - `stepIteration`: ステップごとの実行回数 + - `cwd`, `projectCwd`: ディレクトリ情報 + - `userInputs`: blocked時の追加入力 + - `previousOutput`: 前ステップの出力 + - `reportDir`: レポートディレクトリ + +2. **セクション組み立て**: + - 自動注入セクション (上記7つ) + - プレースホルダー置換 + +3. **出力**: 完全なインストラクション文字列 + +--- + +### ステージ6: エージェント実行 + +**Phase 1: Main Execution**: +- `runAgent()` → `provider.call()` +- ストリーミング → `onStream` callback → UI表示 +- 結果: `AgentResponse` + +**Phase 2: Report Output** (オプション): +- 同じセッションを継続 (resume) +- Write-only ツール +- レポートファイル出力 + +**Phase 3: Status Judgment** (オプション): +- 同じセッションを継続 (resume) +- ツールなし +- `[STEP:N]` タグ出力 + +--- + +### ステージ7: ルール評価と遷移 + +**ルール評価** (`detectMatchedRule()`): + +5段階のフォールバック: +1. **Aggregate** (`all()`/`any()`) - 並列ステップ用 +2. **Phase 3 tag** - `[STEP:N]` from status judgment +3. **Phase 1 tag** - `[STEP:N]` from main output (fallback) +4. **AI judge** - `ai("condition text")` rules +5. **AI judge fallback** - すべての条件をAIで評価 + +**出力**: `{ index: number, method: RuleMatchMethod }` + +**遷移**: +- `determineNextStepByRules()`: `rules[index].next` を取得 +- 特殊ステップ: + - `COMPLETE`: ワークフロー完了 + - `ABORT`: ワークフロー中断 +- 通常ステップ: `state.currentStep = nextStep` + +--- + +## 重要な変換ポイント + +### 1. 会話履歴 → タスク文字列 + +**場所**: `src/commands/interactive/interactive.ts` + +```typescript +function buildTaskFromHistory(history: ConversationMessage[]): string { + return history + .map((msg) => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`) + .join('\n\n'); +} +``` + +**重要性**: インタラクティブモードで蓄積された会話全体が、後続のワークフロー実行で単一の `task` 文字列として扱われる。 + +--- + +### 2. タスク → ブランチスラグ (AI生成) + +**場所**: `src/task/summarize.ts` (呼び出し: `selectAndExecute.ts`, `taskExecution.ts`) + +```typescript +await summarizeTaskName(task, { cwd }) +``` + +**処理**: +- タスク文字列をAIに渡す +- 英語の短いスラグに要約 (例: `fix-login-bug`) +- ブランチ名として使用 + +**重要性**: ユーザーが日本語でタスクを書いても、Git-friendlyなブランチ名が自動生成される。 + +--- + +### 3. ワークフロー設定 → WorkflowState + +**場所**: `src/workflow/state-manager.ts` + +```typescript +function createInitialState( + config: WorkflowConfig, + options: WorkflowEngineOptions +): WorkflowState { + return { + status: 'running', + currentStep: config.initialStep, + iteration: 0, + stepIterations: new Map(), + agentSessions: new Map(Object.entries(options.initialSessions ?? {})), + stepOutputs: new Map(), + userInputs: [], + }; +} +``` + +**重要性**: YAMLで定義された静的な設定が、実行時のミュータブルな状態に変換される。 + +--- + +### 4. コンテキスト → インストラクション文字列 + +**場所**: `src/workflow/instruction/InstructionBuilder.ts` + +**入力**: +- `step: WorkflowStep` +- `context: InstructionContext` (task, iteration, previousOutput, userInputs, など) + +**処理**: +1. 7つのセクションを組み立て +2. プレースホルダー置換 +3. ロケール対応 + +**出力**: 完全なMarkdown形式のインストラクション文字列 + +**重要性**: 散在するコンテキスト情報が、エージェントが理解できる単一の文字列に統合される。 + +--- + +### 5. AgentResponse → ルールマッチ + +**場所**: `src/workflow/evaluation/RuleEvaluator.ts` + +**入力**: +- `step: WorkflowStep` +- `content: string` (Phase 1 output) +- `tagContent: string` (Phase 3 output) +- `state: WorkflowState` + +**処理**: +1. タグ検出 (`[STEP:0]`, `[STEP:1]`, ...) +2. AI判断 (`ai("condition")` ルール) +3. 集約評価 (`all()`, `any()`) + +**出力**: `{ index: number, method: RuleMatchMethod } | null` + +**重要性**: 自然言語の出力が、構造化されたステップ遷移決定に変換される。 + +--- + +### 6. ルールマッチ → 次ステップ名 + +**場所**: `src/workflow/transitions.ts` + +```typescript +function determineNextStepByRules( + step: WorkflowStep, + matchedRuleIndex: number +): string | null { + const rule = step.rules?.[matchedRuleIndex]; + return rule?.next ?? null; +} +``` + +**重要性**: インデックス番号が、実際に実行すべきステップ名に変換される。 + +--- + +### 7. Provider Response → AgentResponse + +**場所**: `src/providers/claude.ts`, `src/providers/codex.ts` + +**入力**: SDKレスポンス (`ClaudeResult`) + +**処理**: +- `status` 変換 +- `content` 抽出 +- `error` 伝播 (重要!) +- `sessionId` 保存 + +**出力**: `AgentResponse` (統一インターフェース) + +**重要性**: 異なるプロバイダーのレスポンスが統一形式に正規化される。 + +--- + +## まとめ + +TAKTのデータフローは、**7つのレイヤー**を通じて、ユーザーの自然な入力を段階的に変換し、最終的にAIエージェントの協調的な実行に変えていきます。 + +**主要な設計原則**: + +1. **Progressive Transformation**: データは各レイヤーで少しずつ変換され、次のレイヤーに渡される +2. **Context Accumulation**: タスク、イテレーション、ユーザー入力などのコンテキストが蓄積される +3. **Session Continuity**: エージェントセッションIDが保存・復元され、会話の継続性を保つ +4. **Event-Driven Architecture**: WorkflowEngineがイベントを発行し、UI、ログ、通知が連携 +5. **3-Phase Execution**: メイン実行、レポート出力、ステータス判断の3段階で、明確な責任分離 +6. **Rule-Based Routing**: ルール評価の5段階フォールバックで、柔軟かつ予測可能な遷移 + +このアーキテクチャにより、TAKTは複雑な多エージェント協調を、ユーザーには透明で、開発者には拡張可能な形で実現しています。 \ No newline at end of file diff --git a/docs/vertical-slice-migration-map.md b/docs/vertical-slice-migration-map.md new file mode 100644 index 0000000..3b67970 --- /dev/null +++ b/docs/vertical-slice-migration-map.md @@ -0,0 +1,111 @@ +# Vertical Slice + Core ハイブリッド構成 マッピング案 + +## 目的 +- CLI中心の機能(コマンド)を slice 化し、変更影響を局所化する。 +- Workflow Engine などのコアは内向き依存(Clean)で保護する。 +- Public API(`index.ts`)で境界を固定し、深い import を避ける。 + +## 依存ルール(簡易) +- `core` は外側に依存しない。 +- `infra` は `core` に依存できる。 +- `features` は `core` / `infra` / `shared` に依存できる。 +- `app` は配線専用(入口)。 + +## 移行マップ + +### 1) app/cli(CLI入口・配線) +``` +src/cli/index.ts -> src/app/cli/index.ts +src/cli/program.ts -> src/app/cli/program.ts +src/cli/commands.ts -> src/app/cli/commands.ts +src/cli/routing.ts -> src/app/cli/routing.ts +src/cli/helpers.ts -> src/app/cli/helpers.ts +``` +- `app/cli/index.ts` は CLI エントリのみ。 +- ルーティングは `features` の Public API を呼ぶだけにする。 + +### 2) features(コマンド単位) +``` +src/commands/index.ts -> src/features/tasks/index.ts +src/commands/runAllTasks.ts -> src/features/tasks/run/index.ts +src/commands/watchTasks.ts -> src/features/tasks/watch/index.ts +src/commands/addTask.ts -> src/features/tasks/add/index.ts +src/commands/listTasks.ts -> src/features/tasks/list/index.ts +src/commands/execution/selectAndExecute.ts -> src/features/tasks/execute/selectAndExecute.ts +src/commands/execution/types.ts -> src/features/tasks/execute/types.ts + +src/commands/pipeline/executePipeline.ts -> src/features/pipeline/execute.ts +src/commands/pipeline/index.ts -> src/features/pipeline/index.ts + +src/commands/switchWorkflow.ts -> src/features/config/switchWorkflow.ts +src/commands/switchConfig.ts -> src/features/config/switchConfig.ts +src/commands/ejectBuiltin.ts -> src/features/config/ejectBuiltin.ts +``` +- `features/tasks` は run/watch/add/list の共通入口を持つ。 +- `features/pipeline` は pipeline モードの専用 slice。 +- `features/config` は設定系(switch/eject)を集約。 + +### 3) core/workflow(中核ロジック) +``` +src/workflow/engine/* -> src/core/workflow/engine/* +src/workflow/instruction/* -> src/core/workflow/instruction/* +src/workflow/evaluation/* -> src/core/workflow/evaluation/* +src/workflow/types.ts -> src/core/workflow/types.ts +src/workflow/constants.ts -> src/core/workflow/constants.ts +src/workflow/index.ts -> src/core/workflow/index.ts +``` +- `core/workflow/index.ts` だけを Public API として使用。 +- `engine/`, `instruction/`, `evaluation/` 間の依存は内向き(core 内のみ)。 + +### 4) core/models(型・スキーマ) +``` +src/models/schemas.ts -> src/core/models/schemas.ts +src/models/types.ts -> src/core/models/types.ts +src/models/workflow-types.ts -> src/core/models/workflow-types.ts +src/models/index.ts -> src/core/models/index.ts +``` +- `core/models/index.ts` を Public API 化。 + +### 5) infra(外部I/O) +``` +src/providers/* -> src/infra/providers/* +src/github/* -> src/infra/github/* +src/config/* -> src/infra/config/* +src/task/* -> src/infra/task/* +src/utils/session.ts -> src/infra/fs/session.ts +src/utils/git/* -> src/infra/git/* +``` +- GitHub API / FS / Git / Provider など外部依存は `infra` に集約。 + +### 6) shared(横断ユーティリティ) +``` +src/utils/error.ts -> src/shared/utils/error.ts +src/utils/debug.ts -> src/shared/utils/debug.ts +src/utils/ui.ts -> src/shared/ui/index.ts +src/utils/* -> src/shared/utils/* (外部I/O以外) +``` +- 共有は `shared` に集めるが、肥大化は避ける。 + +### 7) docs(参照パス修正) +``` +docs/data-flow.md -> パス参照を app/core/features に合わせて更新 +`src/cli.ts` 参照 -> `src/app/cli/index.ts` に更新 +`src/workflow/state-manager.ts` 参照 -> `src/core/workflow/engine/state-manager.ts` +`src/workflow/transitions.ts` 参照 -> `src/core/workflow/engine/transitions.ts` +``` + +## Public API ルール +- `core/*` と `features/*` は **必ず `index.ts` から import**。 +- 深い import(`../engine/xxx` など)は禁止。 + +## 移行順序(推奨) +1. `core/` に workflow + models を集約 +2. `infra/` に外部I/Oを移動 +3. `features/` にコマンド単位で集約 +4. `app/cli` にエントリを移す +5. Public API を整理し、深い import を排除 +6. docs の参照を更新 + +## 備考 +- `src/workflow/index.ts` は `core/workflow/index.ts` に移し、外部からはここだけを参照。 +- `src/models/workflow.ts` のようなプレースホルダは廃止するか、`core/models/index.ts` へ統合する。 diff --git a/package.json b/package.json index 340608b..b0f6a93 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "bin": { "takt": "./bin/takt", "takt-dev": "./bin/takt", - "takt-cli": "./dist/cli.js" + "takt-cli": "./dist/cli/index.js" }, "scripts": { "build": "tsc", diff --git a/src/__tests__/engine-abort.test.ts b/src/__tests__/engine-abort.test.ts index 2bda3ab..aad3a42 100644 --- a/src/__tests__/engine-abort.test.ts +++ b/src/__tests__/engine-abort.test.ts @@ -22,7 +22,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/engine-agent-overrides.test.ts b/src/__tests__/engine-agent-overrides.test.ts index 1f51dcf..48913b1 100644 --- a/src/__tests__/engine-agent-overrides.test.ts +++ b/src/__tests__/engine-agent-overrides.test.ts @@ -15,7 +15,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn(), runReportPhase: vi.fn(), runStatusJudgmentPhase: vi.fn(), diff --git a/src/__tests__/engine-blocked.test.ts b/src/__tests__/engine-blocked.test.ts index f46c213..1e084e5 100644 --- a/src/__tests__/engine-blocked.test.ts +++ b/src/__tests__/engine-blocked.test.ts @@ -20,7 +20,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/engine-error.test.ts b/src/__tests__/engine-error.test.ts index 2fd202a..632e679 100644 --- a/src/__tests__/engine-error.test.ts +++ b/src/__tests__/engine-error.test.ts @@ -21,7 +21,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/engine-happy-path.test.ts b/src/__tests__/engine-happy-path.test.ts index cc3ec94..51cc97c 100644 --- a/src/__tests__/engine-happy-path.test.ts +++ b/src/__tests__/engine-happy-path.test.ts @@ -25,7 +25,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/engine-parallel.test.ts b/src/__tests__/engine-parallel.test.ts index 146cc6d..c2b3446 100644 --- a/src/__tests__/engine-parallel.test.ts +++ b/src/__tests__/engine-parallel.test.ts @@ -20,7 +20,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/engine-test-helpers.ts b/src/__tests__/engine-test-helpers.ts index 3e31eeb..1d1ab7d 100644 --- a/src/__tests__/engine-test-helpers.ts +++ b/src/__tests__/engine-test-helpers.ts @@ -17,7 +17,7 @@ import type { WorkflowConfig, WorkflowStep, AgentResponse, WorkflowRule } from ' import { runAgent } from '../agents/runner.js'; import { detectMatchedRule } from '../workflow/evaluation/index.js'; import type { RuleMatch } from '../workflow/evaluation/index.js'; -import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../workflow/phase-runner.js'; +import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../workflow/engine/phase-runner.js'; import { generateReportDir } from '../utils/session.js'; // --- Factory functions --- diff --git a/src/__tests__/engine-worktree-report.test.ts b/src/__tests__/engine-worktree-report.test.ts index 896f896..bd6f28f 100644 --- a/src/__tests__/engine-worktree-report.test.ts +++ b/src/__tests__/engine-worktree-report.test.ts @@ -21,7 +21,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -34,7 +34,7 @@ vi.mock('../utils/session.js', () => ({ // --- Imports (after mocks) --- import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; -import { runReportPhase } from '../workflow/phase-runner.js'; +import { runReportPhase } from '../workflow/engine/phase-runner.js'; import { makeResponse, makeStep, diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 09a4796..55e5875 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -13,8 +13,8 @@ import { buildExecutionMetadata, renderExecutionMetadata, type InstructionContext, -} from '../workflow/instruction-context.js'; -import { generateStatusRulesFromRules } from '../workflow/status-rules.js'; +} from '../workflow/instruction/instruction-context.js'; +import { generateStatusRulesFromRules } from '../workflow/instruction/status-rules.js'; // Backward-compatible function wrappers for test readability function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string { diff --git a/src/__tests__/it-error-recovery.test.ts b/src/__tests__/it-error-recovery.test.ts index f46225a..6374f47 100644 --- a/src/__tests__/it-error-recovery.test.ts +++ b/src/__tests__/it-error-recovery.test.ts @@ -25,7 +25,7 @@ vi.mock('../claude/client.js', async (importOriginal) => { }; }); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/it-instruction-builder.test.ts b/src/__tests__/it-instruction-builder.test.ts index dd98e86..49b1cd2 100644 --- a/src/__tests__/it-instruction-builder.test.ts +++ b/src/__tests__/it-instruction-builder.test.ts @@ -18,7 +18,7 @@ vi.mock('../config/global/globalConfig.js', () => ({ import { InstructionBuilder } from '../workflow/instruction/InstructionBuilder.js'; import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js'; import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../workflow/instruction/StatusJudgmentBuilder.js'; -import type { InstructionContext } from '../workflow/instruction-context.js'; +import type { InstructionContext } from '../workflow/instruction/instruction-context.js'; // Function wrappers for test readability function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string { diff --git a/src/__tests__/it-pipeline-modes.test.ts b/src/__tests__/it-pipeline-modes.test.ts index 539d0be..638c093 100644 --- a/src/__tests__/it-pipeline-modes.test.ts +++ b/src/__tests__/it-pipeline-modes.test.ts @@ -131,7 +131,7 @@ vi.mock('../prompt/index.js', () => ({ promptInput: vi.fn().mockResolvedValue(null), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/it-pipeline.test.ts b/src/__tests__/it-pipeline.test.ts index 6db621b..ebf7ab4 100644 --- a/src/__tests__/it-pipeline.test.ts +++ b/src/__tests__/it-pipeline.test.ts @@ -113,7 +113,7 @@ vi.mock('../prompt/index.js', () => ({ promptInput: vi.fn().mockResolvedValue(null), })); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/it-three-phase-execution.test.ts b/src/__tests__/it-three-phase-execution.test.ts index b80d21f..cca0ed4 100644 --- a/src/__tests__/it-three-phase-execution.test.ts +++ b/src/__tests__/it-three-phase-execution.test.ts @@ -30,7 +30,7 @@ const mockNeedsStatusJudgmentPhase = vi.fn(); const mockRunReportPhase = vi.fn(); const mockRunStatusJudgmentPhase = vi.fn(); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: (...args: unknown[]) => mockNeedsStatusJudgmentPhase(...args), runReportPhase: (...args: unknown[]) => mockRunReportPhase(...args), runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...args), diff --git a/src/__tests__/it-workflow-execution.test.ts b/src/__tests__/it-workflow-execution.test.ts index 596e831..67bf1c1 100644 --- a/src/__tests__/it-workflow-execution.test.ts +++ b/src/__tests__/it-workflow-execution.test.ts @@ -29,7 +29,7 @@ vi.mock('../claude/client.js', async (importOriginal) => { }; }); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/it-workflow-patterns.test.ts b/src/__tests__/it-workflow-patterns.test.ts index 8611e15..58f2ee6 100644 --- a/src/__tests__/it-workflow-patterns.test.ts +++ b/src/__tests__/it-workflow-patterns.test.ts @@ -24,7 +24,7 @@ vi.mock('../claude/client.js', async (importOriginal) => { }; }); -vi.mock('../workflow/phase-runner.js', () => ({ +vi.mock('../workflow/engine/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), diff --git a/src/__tests__/parallel-logger.test.ts b/src/__tests__/parallel-logger.test.ts index 893954f..9f454ac 100644 --- a/src/__tests__/parallel-logger.test.ts +++ b/src/__tests__/parallel-logger.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { ParallelLogger } from '../workflow/parallel-logger.js'; +import { ParallelLogger } from '../workflow/engine/parallel-logger.js'; import type { StreamEvent } from '../claude/types.js'; describe('ParallelLogger', () => { diff --git a/src/__tests__/transitions.test.ts b/src/__tests__/transitions.test.ts index 7ea899a..d81351f 100644 --- a/src/__tests__/transitions.test.ts +++ b/src/__tests__/transitions.test.ts @@ -3,7 +3,7 @@ */ import { describe, it, expect } from 'vitest'; -import { determineNextStepByRules } from '../workflow/transitions.js'; +import { determineNextStepByRules } from '../workflow/engine/transitions.js'; import type { WorkflowStep } from '../models/types.js'; function createStepWithRules(rules: { condition: string; next: string }[]): WorkflowStep { diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index 0eae095..0000000 --- a/src/cli.ts +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env node - -/** - * TAKT CLI - Task Agent Koordination Tool - * - * Usage: - * takt {task} - Execute task with current workflow (continues session) - * takt #99 - Execute task from GitHub issue - * takt run - Run all pending tasks from .takt/tasks/ - * takt switch - Switch workflow interactively - * takt clear - Clear agent conversation sessions (reset to initial state) - * takt --help - Show help - * takt config - Select permission mode interactively - * - * Pipeline (non-interactive): - * takt --task "fix bug" -w magi --auto-pr - * takt --task "fix bug" --issue 99 --auto-pr - */ - -import { createRequire } from 'node:module'; -import { Command } from 'commander'; -import { resolve } from 'node:path'; -import { - initGlobalDirs, - initProjectDirs, - loadGlobalConfig, - getEffectiveDebugConfig, -} from './config/index.js'; -import { clearAgentSessions, getCurrentWorkflow, isVerboseMode } from './config/paths.js'; -import { setQuietMode } from './context.js'; -import { info, error, success, setLogLevel } from './utils/ui.js'; -import { initDebugLogger, createLogger, setVerboseConsole } from './utils/debug.js'; -import { - runAllTasks, - switchWorkflow, - switchConfig, - addTask, - ejectBuiltin, - watchTasks, - listTasks, - interactiveMode, - executePipeline, -} from './commands/index.js'; -import { DEFAULT_WORKFLOW_NAME } from './constants.js'; -import { checkForUpdates } from './utils/updateNotifier.js'; -import { getErrorMessage } from './utils/error.js'; -import { resolveIssueTask, isIssueReference } from './github/issue.js'; -import { selectAndExecuteTask } from './commands/execution/selectAndExecute.js'; -import type { TaskExecutionOptions, SelectAndExecuteOptions } from './commands/execution/types.js'; -import type { ProviderType } from './providers/index.js'; - -const require = createRequire(import.meta.url); -const { version: cliVersion } = require('../package.json') as { version: string }; - -const log = createLogger('cli'); - -checkForUpdates(); - -/** Resolved cwd shared across commands via preAction hook */ -let resolvedCwd = ''; - -/** Whether pipeline mode is active (--task specified, set in preAction) */ -let pipelineMode = false; - -/** Whether quiet mode is active (--quiet flag or config, set in preAction) */ -let quietMode = false; - -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 }; -} - -function parseCreateWorktreeOption(value?: string): boolean | undefined { - if (!value) { - return undefined; - } - - const normalized = value.toLowerCase(); - if (normalized === 'yes' || normalized === 'true') { - return true; - } - if (normalized === 'no' || normalized === 'false') { - return false; - } - - error('Invalid value for --create-worktree. Use yes or no.'); - process.exit(1); -} - -program - .name('takt') - .description('TAKT: Task Agent Koordination Tool') - .version(cliVersion); - -// --- Global options --- -program - .option('-i, --issue ', 'GitHub issue number (equivalent to #N)', (val: string) => parseInt(val, 10)) - .option('-w, --workflow ', 'Workflow name or path to workflow file') - .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 (as alternative to GitHub issue)') - .option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation') - .option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)') - .option('--create-worktree ', 'Skip the worktree prompt by explicitly specifying yes or no') - .option('-q, --quiet', 'Minimal output mode: suppress AI output (for CI)'); - -// Common initialization for all commands -program.hook('preAction', async () => { - resolvedCwd = resolve(process.cwd()); - - // Pipeline mode: triggered by --pipeline flag - const rootOpts = program.opts(); - pipelineMode = rootOpts.pipeline === true; - - await initGlobalDirs({ nonInteractive: pipelineMode }); - initProjectDirs(resolvedCwd); - - const verbose = isVerboseMode(resolvedCwd); - let debugConfig = getEffectiveDebugConfig(resolvedCwd); - - if (verbose && (!debugConfig || !debugConfig.enabled)) { - debugConfig = { enabled: true }; - } - - initDebugLogger(debugConfig, resolvedCwd); - - // Load config once for both log level and quiet mode - const config = loadGlobalConfig(); - - if (verbose) { - setVerboseConsole(true); - setLogLevel('debug'); - } else { - setLogLevel(config.logLevel); - } - - // Quiet mode: CLI flag takes precedence over config - quietMode = rootOpts.quiet === true || config.minimalOutput === true; - setQuietMode(quietMode); - - log.info('TAKT CLI starting', { version: cliVersion, cwd: resolvedCwd, verbose, pipelineMode, quietMode }); -}); - -// isQuietMode is now exported from context.ts to avoid circular dependencies - -// --- Subcommands --- - -program - .command('run') - .description('Run all pending tasks from .takt/tasks/') - .action(async () => { - const workflow = getCurrentWorkflow(resolvedCwd); - await runAllTasks(resolvedCwd, workflow, resolveAgentOverrides()); - }); - -program - .command('watch') - .description('Watch for tasks and auto-execute') - .action(async () => { - await watchTasks(resolvedCwd, resolveAgentOverrides()); - }); - -program - .command('add') - .description('Add a new task (interactive AI conversation)') - .argument('[task]', 'Task description or GitHub issue reference (e.g. "#28")') - .action(async (task?: string) => { - await addTask(resolvedCwd, task); - }); - -program - .command('list') - .description('List task branches (merge/delete)') - .action(async () => { - await listTasks(resolvedCwd, resolveAgentOverrides()); - }); - -program - .command('switch') - .description('Switch workflow interactively') - .argument('[workflow]', 'Workflow name') - .action(async (workflow?: string) => { - await switchWorkflow(resolvedCwd, workflow); - }); - -program - .command('clear') - .description('Clear agent conversation sessions') - .action(() => { - clearAgentSessions(resolvedCwd); - success('Agent sessions cleared'); - }); - -program - .command('eject') - .description('Copy builtin workflow/agents to ~/.takt/ for customization') - .argument('[name]', 'Specific builtin to eject') - .action(async (name?: string) => { - await ejectBuiltin(name); - }); - -program - .command('config') - .description('Configure settings (permission mode)') - .argument('[key]', 'Configuration key') - .action(async (key?: string) => { - await switchConfig(resolvedCwd, key); - }); - -// --- Default action: task execution, interactive mode, or pipeline --- - -/** - * Check if the input is a task description (should execute directly) - * vs a short input that should enter interactive mode as initial input. - * - * Task descriptions: contain spaces, or are issue references (#N). - * Short single words: routed to interactive mode as first message. - */ -function isDirectTask(input: string): boolean { - // Multi-word input is a task description - if (input.includes(' ')) return true; - // Issue references are direct tasks - if (isIssueReference(input) || input.trim().split(/\s+/).every((t: string) => isIssueReference(t))) return true; - return false; -} - - -program - .argument('[task]', 'Task to execute (or GitHub issue reference like "#6")') - .action(async (task?: string) => { - const opts = program.opts(); - const agentOverrides = resolveAgentOverrides(); - const createWorktreeOverride = parseCreateWorktreeOption(opts.createWorktree as string | undefined); - const selectOptions: SelectAndExecuteOptions = { - autoPr: opts.autoPr === true, - repo: opts.repo as string | undefined, - workflow: opts.workflow as string | undefined, - createWorktree: createWorktreeOverride, - }; - - // --- Pipeline mode (non-interactive): triggered by --pipeline --- - if (pipelineMode) { - const exitCode = await executePipeline({ - issueNumber: opts.issue as number | undefined, - task: opts.task as string | undefined, - workflow: (opts.workflow as string | undefined) ?? DEFAULT_WORKFLOW_NAME, - branch: opts.branch as string | undefined, - autoPr: opts.autoPr === true, - repo: opts.repo as string | undefined, - skipGit: opts.skipGit === true, - cwd: resolvedCwd, - provider: agentOverrides?.provider, - model: agentOverrides?.model, - }); - - if (exitCode !== 0) { - process.exit(exitCode); - } - return; - } - - // --- Normal (interactive) mode --- - - // Resolve --task option to task text - const taskFromOption = opts.task as string | undefined; - if (taskFromOption) { - await selectAndExecuteTask(resolvedCwd, taskFromOption, selectOptions, agentOverrides); - return; - } - - // Resolve --issue N to task text (same as #N) - const issueFromOption = opts.issue as number | undefined; - if (issueFromOption) { - try { - const resolvedTask = resolveIssueTask(`#${issueFromOption}`); - await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); - } catch (e) { - error(getErrorMessage(e)); - process.exit(1); - } - return; - } - - if (task && isDirectTask(task)) { - // Resolve #N issue references to task text - let resolvedTask: string = task; - if (isIssueReference(task) || task.trim().split(/\s+/).every((t: string) => isIssueReference(t))) { - try { - info('Fetching GitHub Issue...'); - resolvedTask = resolveIssueTask(task); - } catch (e) { - error(getErrorMessage(e)); - process.exit(1); - } - } - - await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); - return; - } - - // Short single word or no task → interactive mode (with optional initial input) - const result = await interactiveMode(resolvedCwd, task); - - if (!result.confirmed) { - return; - } - - await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides); - }); - -program.parse(); diff --git a/src/cli/commands.ts b/src/cli/commands.ts new file mode 100644 index 0000000..1078f98 --- /dev/null +++ b/src/cli/commands.ts @@ -0,0 +1,81 @@ +/** + * CLI subcommand definitions + * + * Registers all named subcommands (run, watch, add, list, switch, clear, eject, config). + */ + +import { clearAgentSessions, getCurrentWorkflow } from '../config/paths.js'; +import { success } from '../utils/ui.js'; +import { + runAllTasks, + switchWorkflow, + switchConfig, + addTask, + ejectBuiltin, + watchTasks, + listTasks, +} from '../commands/index.js'; +import { program, resolvedCwd } from './program.js'; +import { resolveAgentOverrides } from './helpers.js'; + +program + .command('run') + .description('Run all pending tasks from .takt/tasks/') + .action(async () => { + const workflow = getCurrentWorkflow(resolvedCwd); + await runAllTasks(resolvedCwd, workflow, resolveAgentOverrides(program)); + }); + +program + .command('watch') + .description('Watch for tasks and auto-execute') + .action(async () => { + await watchTasks(resolvedCwd, resolveAgentOverrides(program)); + }); + +program + .command('add') + .description('Add a new task (interactive AI conversation)') + .argument('[task]', 'Task description or GitHub issue reference (e.g. "#28")') + .action(async (task?: string) => { + await addTask(resolvedCwd, task); + }); + +program + .command('list') + .description('List task branches (merge/delete)') + .action(async () => { + await listTasks(resolvedCwd, resolveAgentOverrides(program)); + }); + +program + .command('switch') + .description('Switch workflow interactively') + .argument('[workflow]', 'Workflow name') + .action(async (workflow?: string) => { + await switchWorkflow(resolvedCwd, workflow); + }); + +program + .command('clear') + .description('Clear agent conversation sessions') + .action(() => { + clearAgentSessions(resolvedCwd); + success('Agent sessions cleared'); + }); + +program + .command('eject') + .description('Copy builtin workflow/agents to ~/.takt/ for customization') + .argument('[name]', 'Specific builtin to eject') + .action(async (name?: string) => { + await ejectBuiltin(name); + }); + +program + .command('config') + .description('Configure settings (permission mode)') + .argument('[key]', 'Configuration key') + .action(async (key?: string) => { + await switchConfig(resolvedCwd, key); + }); diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts new file mode 100644 index 0000000..94602d9 --- /dev/null +++ b/src/cli/helpers.ts @@ -0,0 +1,62 @@ +/** + * CLI helper functions + * + * Utility functions for option parsing and task classification. + */ + +import type { Command } from 'commander'; +import type { TaskExecutionOptions } from '../commands/execution/types.js'; +import type { ProviderType } from '../providers/index.js'; +import { error } from '../utils/ui.js'; +import { isIssueReference } from '../github/issue.js'; + +/** + * Resolve --provider and --model options into TaskExecutionOptions. + * Returns undefined if neither is specified. + */ +export function resolveAgentOverrides(program: Command): 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 }; +} + +/** + * Parse --create-worktree option value (yes/no/true/false). + * Returns undefined if not specified, boolean otherwise. + * Exits with error on invalid value. + */ +export function parseCreateWorktreeOption(value?: string): boolean | undefined { + if (!value) { + return undefined; + } + + const normalized = value.toLowerCase(); + if (normalized === 'yes' || normalized === 'true') { + return true; + } + if (normalized === 'no' || normalized === 'false') { + return false; + } + + error('Invalid value for --create-worktree. Use yes or no.'); + process.exit(1); +} + +/** + * Check if the input is a task description (should execute directly) + * vs a short input that should enter interactive mode as initial input. + * + * Task descriptions: contain spaces, or are issue references (#N). + * Short single words: routed to interactive mode as first message. + */ +export function isDirectTask(input: string): boolean { + if (input.includes(' ')) return true; + if (isIssueReference(input) || input.trim().split(/\s+/).every((t: string) => isIssueReference(t))) return true; + return false; +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..b4a903f --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +/** + * TAKT CLI entry point + * + * Import order matters: program setup → commands → routing → parse. + */ + +import { checkForUpdates } from '../utils/updateNotifier.js'; + +checkForUpdates(); + +// Import in dependency order +import { program } from './program.js'; +import './commands.js'; +import './routing.js'; + +program.parse(); diff --git a/src/cli/program.ts b/src/cli/program.ts new file mode 100644 index 0000000..1fc7240 --- /dev/null +++ b/src/cli/program.ts @@ -0,0 +1,89 @@ +/** + * Commander program setup + * + * Creates the Command instance, registers global options, + * and sets up the preAction hook for initialization. + */ + +import { createRequire } from 'node:module'; +import { Command } from 'commander'; +import { resolve } from 'node:path'; +import { + initGlobalDirs, + initProjectDirs, + loadGlobalConfig, + getEffectiveDebugConfig, + isVerboseMode, +} from '../config/index.js'; +import { setQuietMode } from '../context.js'; +import { setLogLevel } from '../utils/ui.js'; +import { initDebugLogger, createLogger, setVerboseConsole } from '../utils/debug.js'; + +const require = createRequire(import.meta.url); +const { version: cliVersion } = require('../../package.json') as { version: string }; + +const log = createLogger('cli'); + +/** Resolved cwd shared across commands via preAction hook */ +export let resolvedCwd = ''; + +/** Whether pipeline mode is active (--task specified, set in preAction) */ +export let pipelineMode = false; + +export { cliVersion }; + +export const program = new Command(); + +program + .name('takt') + .description('TAKT: Task Agent Koordination Tool') + .version(cliVersion); + +// --- Global options --- +program + .option('-i, --issue ', 'GitHub issue number (equivalent to #N)', (val: string) => parseInt(val, 10)) + .option('-w, --workflow ', 'Workflow name or path to workflow file') + .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 (as alternative to GitHub issue)') + .option('--pipeline', 'Pipeline mode: non-interactive, no worktree, direct branch creation') + .option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)') + .option('--create-worktree ', 'Skip the worktree prompt by explicitly specifying yes or no') + .option('-q, --quiet', 'Minimal output mode: suppress AI output (for CI)'); + +// Common initialization for all commands +program.hook('preAction', async () => { + resolvedCwd = resolve(process.cwd()); + + const rootOpts = program.opts(); + pipelineMode = rootOpts.pipeline === true; + + await initGlobalDirs({ nonInteractive: pipelineMode }); + initProjectDirs(resolvedCwd); + + const verbose = isVerboseMode(resolvedCwd); + let debugConfig = getEffectiveDebugConfig(resolvedCwd); + + if (verbose && (!debugConfig || !debugConfig.enabled)) { + debugConfig = { enabled: true }; + } + + initDebugLogger(debugConfig, resolvedCwd); + + const config = loadGlobalConfig(); + + if (verbose) { + setVerboseConsole(true); + setLogLevel('debug'); + } else { + setLogLevel(config.logLevel); + } + + const quietMode = rootOpts.quiet === true || config.minimalOutput === true; + setQuietMode(quietMode); + + log.info('TAKT CLI starting', { version: cliVersion, cwd: resolvedCwd, verbose, pipelineMode, quietMode }); +}); diff --git a/src/cli/routing.ts b/src/cli/routing.ts new file mode 100644 index 0000000..aa9abee --- /dev/null +++ b/src/cli/routing.ts @@ -0,0 +1,98 @@ +/** + * Default action routing + * + * Handles the default (no subcommand) action: task execution, + * pipeline mode, or interactive mode. + */ + +import { info, error } from '../utils/ui.js'; +import { getErrorMessage } from '../utils/error.js'; +import { resolveIssueTask, isIssueReference } from '../github/issue.js'; +import { selectAndExecuteTask } from '../commands/execution/selectAndExecute.js'; +import { executePipeline, interactiveMode } from '../commands/index.js'; +import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; +import type { SelectAndExecuteOptions } from '../commands/execution/types.js'; +import { program, resolvedCwd, pipelineMode } from './program.js'; +import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js'; + +program + .argument('[task]', 'Task to execute (or GitHub issue reference like "#6")') + .action(async (task?: string) => { + const opts = program.opts(); + const agentOverrides = resolveAgentOverrides(program); + const createWorktreeOverride = parseCreateWorktreeOption(opts.createWorktree as string | undefined); + const selectOptions: SelectAndExecuteOptions = { + autoPr: opts.autoPr === true, + repo: opts.repo as string | undefined, + workflow: opts.workflow as string | undefined, + createWorktree: createWorktreeOverride, + }; + + // --- Pipeline mode (non-interactive): triggered by --pipeline --- + if (pipelineMode) { + const exitCode = await executePipeline({ + issueNumber: opts.issue as number | undefined, + task: opts.task as string | undefined, + workflow: (opts.workflow as string | undefined) ?? DEFAULT_WORKFLOW_NAME, + branch: opts.branch as string | undefined, + autoPr: opts.autoPr === true, + repo: opts.repo as string | undefined, + skipGit: opts.skipGit === true, + cwd: resolvedCwd, + provider: agentOverrides?.provider, + model: agentOverrides?.model, + }); + + if (exitCode !== 0) { + process.exit(exitCode); + } + return; + } + + // --- Normal (interactive) mode --- + + // Resolve --task option to task text + const taskFromOption = opts.task as string | undefined; + if (taskFromOption) { + await selectAndExecuteTask(resolvedCwd, taskFromOption, selectOptions, agentOverrides); + return; + } + + // Resolve --issue N to task text (same as #N) + const issueFromOption = opts.issue as number | undefined; + if (issueFromOption) { + try { + const resolvedTask = resolveIssueTask(`#${issueFromOption}`); + await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); + } catch (e) { + error(getErrorMessage(e)); + process.exit(1); + } + return; + } + + if (task && isDirectTask(task)) { + let resolvedTask: string = task; + if (isIssueReference(task) || task.trim().split(/\s+/).every((t: string) => isIssueReference(t))) { + try { + info('Fetching GitHub Issue...'); + resolvedTask = resolveIssueTask(task); + } catch (e) { + error(getErrorMessage(e)); + process.exit(1); + } + } + + await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); + return; + } + + // Short single word or no task → interactive mode (with optional initial input) + const result = await interactiveMode(resolvedCwd, task); + + if (!result.confirmed) { + return; + } + + await selectAndExecuteTask(resolvedCwd, result.task, selectOptions, agentOverrides); + }); diff --git a/src/models/agent.ts b/src/models/agent.ts index b3c4822..e363fd8 100644 --- a/src/models/agent.ts +++ b/src/models/agent.ts @@ -1,15 +1,7 @@ import { z } from 'zod/v4'; +import { AgentModelSchema, AgentConfigSchema } from './schemas.js'; -export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet'); - -export const AgentConfigSchema = z.object({ - name: z.string().min(1), - description: z.string().optional(), - model: AgentModelSchema, - systemPrompt: z.string().optional(), - allowedTools: z.array(z.string()).optional(), - maxTurns: z.number().int().positive().optional(), -}); +export { AgentModelSchema, AgentConfigSchema }; export type AgentModel = z.infer; export type AgentConfig = z.infer; diff --git a/src/models/config.ts b/src/models/config.ts index 0948b42..479c91b 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -1,19 +1,7 @@ import { z } from 'zod/v4'; -import { AgentModelSchema } from './agent.js'; +import { TaktConfigSchema } from './schemas.js'; -const ClaudeConfigSchema = z.object({ - command: z.string().default('claude'), - timeout: z.number().int().positive().default(300000), -}); - -export const TaktConfigSchema = z.object({ - defaultModel: AgentModelSchema, - defaultWorkflow: z.string().default('default'), - agentDirs: z.array(z.string()).default([]), - workflowDirs: z.array(z.string()).default([]), - sessionDir: z.string().optional(), - claude: ClaudeConfigSchema.default({ command: 'claude', timeout: 300000 }), -}); +export { TaktConfigSchema }; export type TaktConfig = z.infer; diff --git a/src/models/index.ts b/src/models/index.ts index b43dd20..915df0d 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -18,15 +18,6 @@ export type { // Re-export from agent.ts export * from './agent.js'; -// Re-export from workflow.ts (Zod schemas only, not types) -export { - WorkflowStepSchema, - WorkflowConfigSchema, - type WorkflowDefinition, - type WorkflowContext, - type StepResult, -} from './workflow.js'; - // Re-export from config.ts export * from './config.js'; diff --git a/src/models/schemas.ts b/src/models/schemas.ts index 4532da3..3f1d7d9 100644 --- a/src/models/schemas.ts +++ b/src/models/schemas.ts @@ -7,6 +7,35 @@ import { z } from 'zod/v4'; import { DEFAULT_LANGUAGE } from '../constants.js'; +/** Agent model schema (opus, sonnet, haiku) */ +export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet'); + +/** Agent configuration schema */ +export const AgentConfigSchema = z.object({ + name: z.string().min(1), + description: z.string().optional(), + model: AgentModelSchema, + systemPrompt: z.string().optional(), + allowedTools: z.array(z.string()).optional(), + maxTurns: z.number().int().positive().optional(), +}); + +/** Claude CLI configuration schema */ +export const ClaudeConfigSchema = z.object({ + command: z.string().default('claude'), + timeout: z.number().int().positive().default(300000), +}); + +/** TAKT global tool configuration schema */ +export const TaktConfigSchema = z.object({ + defaultModel: AgentModelSchema, + defaultWorkflow: z.string().default('default'), + agentDirs: z.array(z.string()).default([]), + workflowDirs: z.array(z.string()).default([]), + sessionDir: z.string().optional(), + claude: ClaudeConfigSchema.default({ command: 'claude', timeout: 300000 }), +}); + /** Agent type schema */ export const AgentTypeSchema = z.enum(['coder', 'architect', 'supervisor', 'custom']); diff --git a/src/models/workflow.ts b/src/models/workflow.ts index 35842b2..e262334 100644 --- a/src/models/workflow.ts +++ b/src/models/workflow.ts @@ -1,49 +1,4 @@ -import { z } from 'zod/v4'; -import { AgentModelSchema } from './agent.js'; - -export const WorkflowStepSchema = z.object({ - agent: z.string().min(1), - model: AgentModelSchema.optional(), - prompt: z.string().optional(), - condition: z.string().optional(), - onSuccess: z.string().optional(), - onFailure: z.string().optional(), -}); - -export const WorkflowConfigSchema = z.object({ - name: z.string().min(1), - description: z.string().optional(), - version: z.string().optional().default('1.0.0'), - steps: z.array(WorkflowStepSchema).min(1), - entryPoint: z.string().optional(), - variables: z.record(z.string(), z.string()).optional(), -}); - -export type WorkflowStep = z.infer; -export type WorkflowConfig = z.infer; - -export interface WorkflowDefinition { - name: string; - description?: string; - version: string; - steps: WorkflowStep[]; - entryPoint?: string; - variables?: Record; - filePath?: string; -} - -export interface WorkflowContext { - workflowName: string; - currentStep: string; - variables: Record; - history: StepResult[]; - userPrompt: string; -} - -export interface StepResult { - stepName: string; - agentName: string; - success: boolean; - output: string; - timestamp: Date; -} +// Workflow schemas and types are defined in: +// - schemas.ts (Zod schemas for YAML parsing) +// - workflow-types.ts (runtime types) +// This file is kept as a namespace placeholder. diff --git a/src/workflow/engine/OptionsBuilder.ts b/src/workflow/engine/OptionsBuilder.ts index ce247ac..4bcb543 100644 --- a/src/workflow/engine/OptionsBuilder.ts +++ b/src/workflow/engine/OptionsBuilder.ts @@ -8,7 +8,7 @@ import { join } from 'node:path'; import type { WorkflowStep, WorkflowState, Language } from '../../models/types.js'; import type { RunAgentOptions } from '../../agents/runner.js'; -import type { PhaseRunnerContext } from '../phase-runner.js'; +import type { PhaseRunnerContext } from './phase-runner.js'; import type { WorkflowEngineOptions } from '../types.js'; export class OptionsBuilder { diff --git a/src/workflow/engine/ParallelRunner.ts b/src/workflow/engine/ParallelRunner.ts index cda282a..1e2f84e 100644 --- a/src/workflow/engine/ParallelRunner.ts +++ b/src/workflow/engine/ParallelRunner.ts @@ -11,10 +11,10 @@ import type { AgentResponse, } from '../../models/types.js'; import { runAgent } from '../../agents/runner.js'; -import { ParallelLogger } from '../parallel-logger.js'; -import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; +import { ParallelLogger } from './parallel-logger.js'; +import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from './phase-runner.js'; import { detectMatchedRule } from '../evaluation/index.js'; -import { incrementStepIteration } from '../state-manager.js'; +import { incrementStepIteration } from './state-manager.js'; import { createLogger } from '../../utils/debug.js'; import type { OptionsBuilder } from './OptionsBuilder.js'; import type { StepExecutor } from './StepExecutor.js'; diff --git a/src/workflow/engine/StepExecutor.ts b/src/workflow/engine/StepExecutor.ts index 9130945..987c7ac 100644 --- a/src/workflow/engine/StepExecutor.ts +++ b/src/workflow/engine/StepExecutor.ts @@ -16,9 +16,9 @@ import type { } from '../../models/types.js'; import { runAgent } from '../../agents/runner.js'; import { InstructionBuilder, isReportObjectConfig } from '../instruction/InstructionBuilder.js'; -import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; +import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from './phase-runner.js'; import { detectMatchedRule } from '../evaluation/index.js'; -import { incrementStepIteration, getPreviousOutput } from '../state-manager.js'; +import { incrementStepIteration, getPreviousOutput } from './state-manager.js'; import { createLogger } from '../../utils/debug.js'; import type { OptionsBuilder } from './OptionsBuilder.js'; diff --git a/src/workflow/engine/WorkflowEngine.ts b/src/workflow/engine/WorkflowEngine.ts index d97a769..7409e36 100644 --- a/src/workflow/engine/WorkflowEngine.ts +++ b/src/workflow/engine/WorkflowEngine.ts @@ -17,14 +17,14 @@ import type { } from '../../models/types.js'; import { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from '../constants.js'; import type { WorkflowEngineOptions } from '../types.js'; -import { determineNextStepByRules } from '../transitions.js'; -import { LoopDetector } from '../loop-detector.js'; -import { handleBlocked } from '../blocked-handler.js'; +import { determineNextStepByRules } from './transitions.js'; +import { LoopDetector } from './loop-detector.js'; +import { handleBlocked } from './blocked-handler.js'; import { createInitialState, addUserInput as addUserInputToState, incrementStepIteration, -} from '../state-manager.js'; +} from './state-manager.js'; import { generateReportDir } from '../../utils/session.js'; import { getErrorMessage } from '../../utils/error.js'; import { createLogger } from '../../utils/debug.js'; diff --git a/src/workflow/blocked-handler.ts b/src/workflow/engine/blocked-handler.ts similarity index 90% rename from src/workflow/blocked-handler.ts rename to src/workflow/engine/blocked-handler.ts index bc34878..d04222c 100644 --- a/src/workflow/blocked-handler.ts +++ b/src/workflow/engine/blocked-handler.ts @@ -5,8 +5,8 @@ * requesting user input to continue. */ -import type { WorkflowStep, AgentResponse } from '../models/types.js'; -import type { UserInputRequest, WorkflowEngineOptions } from './types.js'; +import type { WorkflowStep, AgentResponse } from '../../models/types.js'; +import type { UserInputRequest, WorkflowEngineOptions } from '../types.js'; import { extractBlockedPrompt } from './transitions.js'; /** diff --git a/src/workflow/loop-detector.ts b/src/workflow/engine/loop-detector.ts similarity index 92% rename from src/workflow/loop-detector.ts rename to src/workflow/engine/loop-detector.ts index 8e1fc71..1f0cd85 100644 --- a/src/workflow/loop-detector.ts +++ b/src/workflow/engine/loop-detector.ts @@ -5,8 +5,8 @@ * which may indicate an infinite loop. */ -import type { LoopDetectionConfig } from '../models/types.js'; -import type { LoopCheckResult } from './types.js'; +import type { LoopDetectionConfig } from '../../models/types.js'; +import type { LoopCheckResult } from '../types.js'; /** Default loop detection settings */ const DEFAULT_LOOP_DETECTION: Required = { diff --git a/src/workflow/parallel-logger.ts b/src/workflow/engine/parallel-logger.ts similarity index 98% rename from src/workflow/parallel-logger.ts rename to src/workflow/engine/parallel-logger.ts index 31d14a3..5160868 100644 --- a/src/workflow/parallel-logger.ts +++ b/src/workflow/engine/parallel-logger.ts @@ -6,7 +6,7 @@ * aligned to the longest sub-step name. */ -import type { StreamCallback, StreamEvent } from '../claude/types.js'; +import type { StreamCallback, StreamEvent } from '../../claude/types.js'; /** ANSI color codes for sub-step prefixes (cycled in order) */ const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const; // cyan, yellow, magenta, green diff --git a/src/workflow/phase-runner.ts b/src/workflow/engine/phase-runner.ts similarity index 88% rename from src/workflow/phase-runner.ts rename to src/workflow/engine/phase-runner.ts index 6a0630e..cbfb518 100644 --- a/src/workflow/phase-runner.ts +++ b/src/workflow/engine/phase-runner.ts @@ -5,12 +5,12 @@ * as session-resume operations. */ -import type { WorkflowStep, Language } from '../models/types.js'; -import { runAgent, type RunAgentOptions } from '../agents/runner.js'; -import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js'; -import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js'; -import { hasTagBasedRules } from './rule-utils.js'; -import { createLogger } from '../utils/debug.js'; +import type { WorkflowStep, Language } from '../../models/types.js'; +import { runAgent, type RunAgentOptions } from '../../agents/runner.js'; +import { ReportInstructionBuilder } from '../instruction/ReportInstructionBuilder.js'; +import { StatusJudgmentBuilder } from '../instruction/StatusJudgmentBuilder.js'; +import { hasTagBasedRules } from '../evaluation/rule-utils.js'; +import { createLogger } from '../../utils/debug.js'; const log = createLogger('phase-runner'); diff --git a/src/workflow/engine/state-manager.ts b/src/workflow/engine/state-manager.ts new file mode 100644 index 0000000..7716b00 --- /dev/null +++ b/src/workflow/engine/state-manager.ts @@ -0,0 +1,118 @@ +/** + * Workflow state management + * + * Manages the mutable state of a workflow execution including + * user inputs and agent sessions. + */ + +import type { WorkflowState, WorkflowConfig, AgentResponse } from '../../models/types.js'; +import { + MAX_USER_INPUTS, + MAX_INPUT_LENGTH, +} from '../constants.js'; +import type { WorkflowEngineOptions } from '../types.js'; + +/** + * Manages workflow execution state. + * + * Encapsulates WorkflowState and provides methods for state mutations. + */ +export class StateManager { + readonly state: WorkflowState; + + constructor(config: WorkflowConfig, options: WorkflowEngineOptions) { + // Restore agent sessions from options if provided + const agentSessions = new Map(); + if (options.initialSessions) { + for (const [agent, sessionId] of Object.entries(options.initialSessions)) { + agentSessions.set(agent, sessionId); + } + } + + // Initialize user inputs from options if provided + const userInputs = options.initialUserInputs + ? [...options.initialUserInputs] + : []; + + this.state = { + workflowName: config.name, + currentStep: config.initialStep, + iteration: 0, + stepOutputs: new Map(), + userInputs, + agentSessions, + stepIterations: new Map(), + status: 'running', + }; + } + + /** + * Increment the iteration counter for a step and return the new value. + */ + incrementStepIteration(stepName: string): number { + const current = this.state.stepIterations.get(stepName) ?? 0; + const next = current + 1; + this.state.stepIterations.set(stepName, next); + return next; + } + + /** + * Add user input to state with truncation and limit handling. + */ + addUserInput(input: string): void { + if (this.state.userInputs.length >= MAX_USER_INPUTS) { + this.state.userInputs.shift(); + } + const truncated = input.slice(0, MAX_INPUT_LENGTH); + this.state.userInputs.push(truncated); + } + + /** + * Get the most recent step output. + */ + getPreviousOutput(): AgentResponse | undefined { + const outputs = Array.from(this.state.stepOutputs.values()); + return outputs[outputs.length - 1]; + } +} + +// --- Backward-compatible function facades --- + +/** + * Create initial workflow state from config and options. + */ +export function createInitialState( + config: WorkflowConfig, + options: WorkflowEngineOptions, +): WorkflowState { + return new StateManager(config, options).state; +} + +/** + * Increment the iteration counter for a step and return the new value. + */ +export function incrementStepIteration(state: WorkflowState, stepName: string): number { + const current = state.stepIterations.get(stepName) ?? 0; + const next = current + 1; + state.stepIterations.set(stepName, next); + return next; +} + +/** + * Add user input to state with truncation and limit handling. + */ +export function addUserInput(state: WorkflowState, input: string): void { + if (state.userInputs.length >= MAX_USER_INPUTS) { + state.userInputs.shift(); + } + const truncated = input.slice(0, MAX_INPUT_LENGTH); + state.userInputs.push(truncated); +} + +/** + * Get the most recent step output. + */ +export function getPreviousOutput(state: WorkflowState): AgentResponse | undefined { + const outputs = Array.from(state.stepOutputs.values()); + return outputs[outputs.length - 1]; +} diff --git a/src/workflow/transitions.ts b/src/workflow/engine/transitions.ts similarity index 97% rename from src/workflow/transitions.ts rename to src/workflow/engine/transitions.ts index 7dc34ab..22234b4 100644 --- a/src/workflow/transitions.ts +++ b/src/workflow/engine/transitions.ts @@ -6,7 +6,7 @@ import type { WorkflowStep, -} from '../models/types.js'; +} from '../../models/types.js'; /** * Determine next step using rules-based detection. diff --git a/src/workflow/rule-utils.ts b/src/workflow/evaluation/rule-utils.ts similarity index 90% rename from src/workflow/rule-utils.ts rename to src/workflow/evaluation/rule-utils.ts index 0d147ba..b2fdf0b 100644 --- a/src/workflow/rule-utils.ts +++ b/src/workflow/evaluation/rule-utils.ts @@ -2,7 +2,7 @@ * Shared rule utility functions used by both engine.ts and instruction-builder.ts. */ -import type { WorkflowStep } from '../models/types.js'; +import type { WorkflowStep } from '../../models/types.js'; /** * Check whether a step has tag-based rules (i.e., rules that require diff --git a/src/workflow/index.ts b/src/workflow/index.ts index cb699b8..85d99e0 100644 --- a/src/workflow/index.ts +++ b/src/workflow/index.ts @@ -22,28 +22,28 @@ export type { LoopCheckResult, } from './types.js'; -// Transitions -export { determineNextStepByRules, extractBlockedPrompt } from './transitions.js'; +// Transitions (engine/) +export { determineNextStepByRules, extractBlockedPrompt } from './engine/transitions.js'; -// Loop detection -export { LoopDetector } from './loop-detector.js'; +// Loop detection (engine/) +export { LoopDetector } from './engine/loop-detector.js'; -// State management +// State management (engine/) export { createInitialState, addUserInput, getPreviousOutput, -} from './state-manager.js'; +} from './engine/state-manager.js'; + +// Blocked handling (engine/) +export { handleBlocked, type BlockedHandlerResult } from './engine/blocked-handler.js'; // Instruction building export { InstructionBuilder, isReportObjectConfig } from './instruction/InstructionBuilder.js'; export { ReportInstructionBuilder, type ReportInstructionContext } from './instruction/ReportInstructionBuilder.js'; export { StatusJudgmentBuilder, type StatusJudgmentContext } from './instruction/StatusJudgmentBuilder.js'; -export { buildExecutionMetadata, renderExecutionMetadata, type InstructionContext, type ExecutionMetadata } from './instruction-context.js'; +export { buildExecutionMetadata, renderExecutionMetadata, type InstructionContext, type ExecutionMetadata } from './instruction/instruction-context.js'; // Rule evaluation export { RuleEvaluator, type RuleMatch, type RuleEvaluatorContext, detectMatchedRule, evaluateAggregateConditions } from './evaluation/index.js'; export { AggregateEvaluator } from './evaluation/AggregateEvaluator.js'; - -// Blocked handling -export { handleBlocked, type BlockedHandlerResult } from './blocked-handler.js'; diff --git a/src/workflow/instruction/InstructionBuilder.ts b/src/workflow/instruction/InstructionBuilder.ts index 42554e5..ab341ea 100644 --- a/src/workflow/instruction/InstructionBuilder.ts +++ b/src/workflow/instruction/InstructionBuilder.ts @@ -9,10 +9,10 @@ */ import type { WorkflowStep, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js'; -import { hasTagBasedRules } from '../rule-utils.js'; -import type { InstructionContext } from '../instruction-context.js'; -import { buildExecutionMetadata, renderExecutionMetadata } from '../instruction-context.js'; -import { generateStatusRulesFromRules } from '../status-rules.js'; +import { hasTagBasedRules } from '../evaluation/rule-utils.js'; +import type { InstructionContext } from './instruction-context.js'; +import { buildExecutionMetadata, renderExecutionMetadata } from './instruction-context.js'; +import { generateStatusRulesFromRules } from './status-rules.js'; import { escapeTemplateChars, replaceTemplatePlaceholders } from './escape.js'; /** diff --git a/src/workflow/instruction/ReportInstructionBuilder.ts b/src/workflow/instruction/ReportInstructionBuilder.ts index 78ba5f9..0960102 100644 --- a/src/workflow/instruction/ReportInstructionBuilder.ts +++ b/src/workflow/instruction/ReportInstructionBuilder.ts @@ -11,8 +11,8 @@ */ import type { WorkflowStep, Language } from '../../models/types.js'; -import type { InstructionContext } from '../instruction-context.js'; -import { METADATA_STRINGS } from '../instruction-context.js'; +import type { InstructionContext } from './instruction-context.js'; +import { METADATA_STRINGS } from './instruction-context.js'; import { replaceTemplatePlaceholders } from './escape.js'; import { isReportObjectConfig, renderReportContext, renderReportOutputInstruction } from './InstructionBuilder.js'; diff --git a/src/workflow/instruction/StatusJudgmentBuilder.ts b/src/workflow/instruction/StatusJudgmentBuilder.ts index 7c0f52f..17690e8 100644 --- a/src/workflow/instruction/StatusJudgmentBuilder.ts +++ b/src/workflow/instruction/StatusJudgmentBuilder.ts @@ -10,7 +10,7 @@ */ import type { WorkflowStep, Language } from '../../models/types.js'; -import { generateStatusRulesFromRules } from '../status-rules.js'; +import { generateStatusRulesFromRules } from './status-rules.js'; /** Localized strings for status judgment phase */ const STATUS_JUDGMENT_STRINGS = { diff --git a/src/workflow/instruction/escape.ts b/src/workflow/instruction/escape.ts index 11aa2be..839618e 100644 --- a/src/workflow/instruction/escape.ts +++ b/src/workflow/instruction/escape.ts @@ -5,7 +5,7 @@ */ import type { WorkflowStep } from '../../models/types.js'; -import type { InstructionContext } from '../instruction-context.js'; +import type { InstructionContext } from './instruction-context.js'; /** * Escape special characters in dynamic content to prevent template injection. diff --git a/src/workflow/instruction-context.ts b/src/workflow/instruction/instruction-context.ts similarity index 98% rename from src/workflow/instruction-context.ts rename to src/workflow/instruction/instruction-context.ts index 0b55997..e265306 100644 --- a/src/workflow/instruction-context.ts +++ b/src/workflow/instruction/instruction-context.ts @@ -5,7 +5,7 @@ * and renders execution metadata (working directory, rules) as markdown. */ -import type { AgentResponse, Language } from '../models/types.js'; +import type { AgentResponse, Language } from '../../models/types.js'; /** * Context for building instruction from template. diff --git a/src/workflow/status-rules.ts b/src/workflow/instruction/status-rules.ts similarity index 97% rename from src/workflow/status-rules.ts rename to src/workflow/instruction/status-rules.ts index 975af2a..0e00201 100644 --- a/src/workflow/status-rules.ts +++ b/src/workflow/instruction/status-rules.ts @@ -5,7 +5,7 @@ * based on the step's rule configuration. */ -import type { WorkflowRule, Language } from '../models/types.js'; +import type { WorkflowRule, Language } from '../../models/types.js'; /** Localized strings for rules-based status prompt */ const RULES_PROMPT_STRINGS = { diff --git a/src/workflow/state-manager.ts b/src/workflow/state-manager.ts deleted file mode 100644 index ad094d4..0000000 --- a/src/workflow/state-manager.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Workflow state management - * - * Manages the mutable state of a workflow execution including - * user inputs and agent sessions. - */ - -import type { WorkflowState, WorkflowConfig, AgentResponse } from '../models/types.js'; -import { - MAX_USER_INPUTS, - MAX_INPUT_LENGTH, -} from './constants.js'; -import type { WorkflowEngineOptions } from './types.js'; - -/** - * Create initial workflow state from config and options. - */ -export function createInitialState( - config: WorkflowConfig, - options: WorkflowEngineOptions -): WorkflowState { - // Restore agent sessions from options if provided - const agentSessions = new Map(); - if (options.initialSessions) { - for (const [agent, sessionId] of Object.entries(options.initialSessions)) { - agentSessions.set(agent, sessionId); - } - } - - // Initialize user inputs from options if provided - const userInputs = options.initialUserInputs - ? [...options.initialUserInputs] - : []; - - return { - workflowName: config.name, - currentStep: config.initialStep, - iteration: 0, - stepOutputs: new Map(), - userInputs, - agentSessions, - stepIterations: new Map(), - status: 'running', - }; -} - -/** - * Increment the iteration counter for a step and return the new value. - */ -export function incrementStepIteration(state: WorkflowState, stepName: string): number { - const current = state.stepIterations.get(stepName) ?? 0; - const next = current + 1; - state.stepIterations.set(stepName, next); - return next; -} - -/** - * Add user input to state with truncation and limit handling. - */ -export function addUserInput(state: WorkflowState, input: string): void { - if (state.userInputs.length >= MAX_USER_INPUTS) { - state.userInputs.shift(); // Remove oldest - } - const truncated = input.slice(0, MAX_INPUT_LENGTH); - state.userInputs.push(truncated); -} - -/** - * Get the most recent step output. - */ -export function getPreviousOutput(state: WorkflowState): AgentResponse | undefined { - const outputs = Array.from(state.stepOutputs.values()); - return outputs[outputs.length - 1]; -} -