This commit is contained in:
nrslib 2026-02-02 13:06:12 +09:00
parent f04a950c9e
commit e57e5e7226
53 changed files with 2270 additions and 545 deletions

View File

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

562
docs/data-flow-diagrams.md Normal file
View File

@ -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: ワークフロー選択<br/>(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()<br/>generateSessionId()<br/>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<br/>(Write-only)
end
opt tag-based rules あり
StepExec->>Provider: runStatusJudgmentPhase()
Note over Provider: Phase 3: Status Judgment<br/>(no tools)
Provider-->>StepExec: tagContent
end
StepExec->>StepExec: detectMatchedRule()
Note over StepExec: ルール評価<br/>(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:<br/>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<br/>定義あり?}
CheckReport -->|Yes| Phase2[Phase 2:<br/>Report Output]
CheckReport -->|No| CheckTag{tag-based<br/>rules あり?}
Phase2 --> ResumeSession1[セッション継続<br/>sessionId同じ]
ResumeSession1 --> ReportBuilder[ReportInstructionBuilder.build]
ReportBuilder --> WriteOnly[Write-only tools]
WriteOnly --> RunReport[runAgent<br/>レポート出力]
RunReport --> CheckTag
CheckTag -->|Yes| Phase3[Phase 3:<br/>Status Judgment]
CheckTag -->|No| RuleEval[detectMatchedRule]
Phase3 --> ResumeSession2[セッション継続<br/>sessionId同じ]
ResumeSession2 --> StatusBuilder[StatusJudgmentBuilder.build]
StatusBuilder --> NoTools[Tools: なし<br/>判断のみ]
NoTools --> RunStatus[runAgent<br/>ステータス出力]
RunStatus --> TagContent[tagContent:<br/>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<br/>status judgment]
Stage2 -->|No| Stage3{3. Phase 1 tag?}
Phase3Tag --> Matched
Stage3 -->|Yes| Phase1Tag[STEP:N from<br/>main output]
Stage3 -->|No| Stage4{4. AI judge<br/>ai rules?}
Phase1Tag --> Matched
Stage4 -->|Yes| AIJudge[AI evaluates<br/>ai conditions]
Stage4 -->|No| Stage5[5. AI judge<br/>fallback]
AIJudge --> Matched
Stage5 --> AIFallback[AI evaluates<br/>all conditions]
AIFallback --> Matched
Matched --> UpdateResponse[response.matchedRuleIndex<br/>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[入力:<br/>step, content, tagContent]
Input --> Stage1{Stage 1:<br/>Aggregate評価<br/>親ステップ?}
Stage1 -->|Yes| CheckAggregate{rules に<br/>allまたはanyあり?}
CheckAggregate -->|Yes| EvalAggregate[AggregateEvaluator]
EvalAggregate --> CheckAggResult{マッチした?}
CheckAggResult -->|Yes| ReturnAgg[method: aggregate<br/>返却]
CheckAggResult -->|No| Stage2
CheckAggregate -->|No| Stage2
Stage1 -->|No| Stage2{Stage 2:<br/>Phase 3 tag<br/>tagContent に<br/>STEP:N あり?}
Stage2 -->|Yes| ExtractTag3[正規表現で抽出:<br/>STEP:(\d+)]
ExtractTag3 --> ValidateIndex3{index が<br/>rules 範囲内?}
ValidateIndex3 -->|Yes| ReturnTag3[method: phase3_tag<br/>返却]
ValidateIndex3 -->|No| Stage3
Stage2 -->|No| Stage3{Stage 3:<br/>Phase 1 tag<br/>content に<br/>STEP:N あり?}
Stage3 -->|Yes| ExtractTag1[正規表現で抽出:<br/>STEP:(\d+)]
ExtractTag1 --> ValidateIndex1{index が<br/>rules 範囲内?}
ValidateIndex1 -->|Yes| ReturnTag1[method: phase1_tag<br/>返却]
ValidateIndex1 -->|No| Stage4
Stage3 -->|No| Stage4{Stage 4:<br/>AI judge<br/>ai rules あり?}
Stage4 -->|Yes| FilterAI[aiルールのみ抽出<br/>ai 関数パース]
FilterAI --> CallAI[AIJudgeEvaluator<br/>condition を評価]
CallAI --> CheckAIResult{マッチした?}
CheckAIResult -->|Yes| ReturnAI[method: ai_judge<br/>返却]
CheckAIResult -->|No| Stage5
Stage4 -->|No| Stage5[Stage 5:<br/>AI judge fallback<br/>全条件を評価]
Stage5 --> AllConditions[全ルール条件を収集]
AllConditions --> CallAIFallback[AIJudgeEvaluator<br/>全条件を評価]
CallAIFallback --> CheckFallbackResult{マッチした?}
CheckFallbackResult -->|Yes| ReturnFallback[method: ai_judge_fallback<br/>返却]
CheckFallbackResult -->|No| NoMatch[null 返却<br/>マッチなし]
ReturnAgg --> End([返却:<br/>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[ユーザー入力<br/>CLI引数]
A2[会話履歴<br/>ConversationMessage]
end
subgraph Transform1 ["変換1: タスク化"]
B1[isDirectTask判定]
B2[buildTaskFromHistory]
end
subgraph Task ["タスク"]
C[task: string]
end
subgraph Transform2 ["変換2: 環境準備"]
D1[determineWorkflow]
D2[summarizeTaskName<br/>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のアーキテクチャを多角的に理解できます。

1029
docs/data-flow.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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/cliCLI入口・配線
```
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` へ統合する。

View File

@ -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",

View File

@ -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(''),

View File

@ -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(),

View File

@ -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(''),

View File

@ -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(''),

View File

@ -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(''),

View File

@ -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(''),

View File

@ -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 ---

View File

@ -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,

View File

@ -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 {

View File

@ -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(''),

View File

@ -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 {

View File

@ -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(''),

View File

@ -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(''),

View File

@ -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),

View File

@ -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(''),

View File

@ -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(''),

View File

@ -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', () => {

View File

@ -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 {

View File

@ -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 <number>', 'GitHub issue number (equivalent to #N)', (val: string) => parseInt(val, 10))
.option('-w, --workflow <name>', 'Workflow name or path to workflow file')
.option('-b, --branch <name>', 'Branch name (auto-generated if omitted)')
.option('--auto-pr', 'Create PR after successful execution')
.option('--repo <owner/repo>', 'Repository (defaults to current)')
.option('--provider <name>', 'Override agent provider (claude|codex|mock)')
.option('--model <name>', 'Override agent model')
.option('-t, --task <string>', 'Task content (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 <yes|no>', '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();

81
src/cli/commands.ts Normal file
View File

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

62
src/cli/helpers.ts Normal file
View File

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

18
src/cli/index.ts Normal file
View File

@ -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();

89
src/cli/program.ts Normal file
View File

@ -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 <number>', 'GitHub issue number (equivalent to #N)', (val: string) => parseInt(val, 10))
.option('-w, --workflow <name>', 'Workflow name or path to workflow file')
.option('-b, --branch <name>', 'Branch name (auto-generated if omitted)')
.option('--auto-pr', 'Create PR after successful execution')
.option('--repo <owner/repo>', 'Repository (defaults to current)')
.option('--provider <name>', 'Override agent provider (claude|codex|mock)')
.option('--model <name>', 'Override agent model')
.option('-t, --task <string>', 'Task content (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 <yes|no>', '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 });
});

98
src/cli/routing.ts Normal file
View File

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

View File

@ -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<typeof AgentModelSchema>;
export type AgentConfig = z.infer<typeof AgentConfigSchema>;

View File

@ -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<typeof TaktConfigSchema>;

View File

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

View File

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

View File

@ -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<typeof WorkflowStepSchema>;
export type WorkflowConfig = z.infer<typeof WorkflowConfigSchema>;
export interface WorkflowDefinition {
name: string;
description?: string;
version: string;
steps: WorkflowStep[];
entryPoint?: string;
variables?: Record<string, string>;
filePath?: string;
}
export interface WorkflowContext {
workflowName: string;
currentStep: string;
variables: Record<string, string>;
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.

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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';
/**

View File

@ -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<LoopDetectionConfig> = {

View File

@ -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

View File

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

View File

@ -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<string, string>();
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];
}

View File

@ -6,7 +6,7 @@
import type {
WorkflowStep,
} from '../models/types.js';
} from '../../models/types.js';
/**
* Determine next step using rules-based detection.

View File

@ -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

View File

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

View File

@ -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';
/**

View File

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

View File

@ -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 = {

View File

@ -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.

View File

@ -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.

View File

@ -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 = {

View File

@ -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<string, string>();
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];
}