refactor
This commit is contained in:
parent
f04a950c9e
commit
e57e5e7226
2
bin/takt
2
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);
|
||||
|
||||
562
docs/data-flow-diagrams.md
Normal file
562
docs/data-flow-diagrams.md
Normal 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
1029
docs/data-flow.md
Normal file
File diff suppressed because it is too large
Load Diff
111
docs/vertical-slice-migration-map.md
Normal file
111
docs/vertical-slice-migration-map.md
Normal 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/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` へ統合する。
|
||||
@ -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",
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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(''),
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
323
src/cli.ts
323
src/cli.ts
@ -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
81
src/cli/commands.ts
Normal 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
62
src/cli/helpers.ts
Normal 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
18
src/cli/index.ts
Normal 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
89
src/cli/program.ts
Normal 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
98
src/cli/routing.ts
Normal 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);
|
||||
});
|
||||
@ -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>;
|
||||
|
||||
@ -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>;
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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']);
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
/**
|
||||
@ -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> = {
|
||||
@ -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
|
||||
@ -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');
|
||||
|
||||
118
src/workflow/engine/state-manager.ts
Normal file
118
src/workflow/engine/state-manager.ts
Normal 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];
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
import type {
|
||||
WorkflowStep,
|
||||
} from '../models/types.js';
|
||||
} from '../../models/types.js';
|
||||
|
||||
/**
|
||||
* Determine next step using rules-based detection.
|
||||
@ -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
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
@ -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 = {
|
||||
@ -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];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user