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);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
// Import the actual CLI from dist
|
// Import the actual CLI from dist
|
||||||
const cliPath = join(__dirname, '..', 'dist', 'cli.js');
|
const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.js');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await import(cliPath);
|
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": {
|
"bin": {
|
||||||
"takt": "./bin/takt",
|
"takt": "./bin/takt",
|
||||||
"takt-dev": "./bin/takt",
|
"takt-dev": "./bin/takt",
|
||||||
"takt-cli": "./dist/cli.js"
|
"takt-cli": "./dist/cli/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
|||||||
@ -22,7 +22,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({
|
|||||||
detectMatchedRule: vi.fn(),
|
detectMatchedRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -15,7 +15,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({
|
|||||||
detectMatchedRule: vi.fn(),
|
detectMatchedRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn(),
|
needsStatusJudgmentPhase: vi.fn(),
|
||||||
runReportPhase: vi.fn(),
|
runReportPhase: vi.fn(),
|
||||||
runStatusJudgmentPhase: vi.fn(),
|
runStatusJudgmentPhase: vi.fn(),
|
||||||
|
|||||||
@ -20,7 +20,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({
|
|||||||
detectMatchedRule: vi.fn(),
|
detectMatchedRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -21,7 +21,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({
|
|||||||
detectMatchedRule: vi.fn(),
|
detectMatchedRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -25,7 +25,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({
|
|||||||
detectMatchedRule: vi.fn(),
|
detectMatchedRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -20,7 +20,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({
|
|||||||
detectMatchedRule: vi.fn(),
|
detectMatchedRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import type { WorkflowConfig, WorkflowStep, AgentResponse, WorkflowRule } from '
|
|||||||
import { runAgent } from '../agents/runner.js';
|
import { runAgent } from '../agents/runner.js';
|
||||||
import { detectMatchedRule } from '../workflow/evaluation/index.js';
|
import { detectMatchedRule } from '../workflow/evaluation/index.js';
|
||||||
import type { RuleMatch } 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';
|
import { generateReportDir } from '../utils/session.js';
|
||||||
|
|
||||||
// --- Factory functions ---
|
// --- Factory functions ---
|
||||||
|
|||||||
@ -21,7 +21,7 @@ vi.mock('../workflow/evaluation/index.js', () => ({
|
|||||||
detectMatchedRule: vi.fn(),
|
detectMatchedRule: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
@ -34,7 +34,7 @@ vi.mock('../utils/session.js', () => ({
|
|||||||
// --- Imports (after mocks) ---
|
// --- Imports (after mocks) ---
|
||||||
|
|
||||||
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js';
|
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js';
|
||||||
import { runReportPhase } from '../workflow/phase-runner.js';
|
import { runReportPhase } from '../workflow/engine/phase-runner.js';
|
||||||
import {
|
import {
|
||||||
makeResponse,
|
makeResponse,
|
||||||
makeStep,
|
makeStep,
|
||||||
|
|||||||
@ -13,8 +13,8 @@ import {
|
|||||||
buildExecutionMetadata,
|
buildExecutionMetadata,
|
||||||
renderExecutionMetadata,
|
renderExecutionMetadata,
|
||||||
type InstructionContext,
|
type InstructionContext,
|
||||||
} from '../workflow/instruction-context.js';
|
} from '../workflow/instruction/instruction-context.js';
|
||||||
import { generateStatusRulesFromRules } from '../workflow/status-rules.js';
|
import { generateStatusRulesFromRules } from '../workflow/instruction/status-rules.js';
|
||||||
|
|
||||||
// Backward-compatible function wrappers for test readability
|
// Backward-compatible function wrappers for test readability
|
||||||
function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string {
|
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),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -18,7 +18,7 @@ vi.mock('../config/global/globalConfig.js', () => ({
|
|||||||
import { InstructionBuilder } from '../workflow/instruction/InstructionBuilder.js';
|
import { InstructionBuilder } from '../workflow/instruction/InstructionBuilder.js';
|
||||||
import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js';
|
import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js';
|
||||||
import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../workflow/instruction/StatusJudgmentBuilder.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 wrappers for test readability
|
||||||
function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string {
|
function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string {
|
||||||
|
|||||||
@ -131,7 +131,7 @@ vi.mock('../prompt/index.js', () => ({
|
|||||||
promptInput: vi.fn().mockResolvedValue(null),
|
promptInput: vi.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -113,7 +113,7 @@ vi.mock('../prompt/index.js', () => ({
|
|||||||
promptInput: vi.fn().mockResolvedValue(null),
|
promptInput: vi.fn().mockResolvedValue(null),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const mockNeedsStatusJudgmentPhase = vi.fn();
|
|||||||
const mockRunReportPhase = vi.fn();
|
const mockRunReportPhase = vi.fn();
|
||||||
const mockRunStatusJudgmentPhase = vi.fn();
|
const mockRunStatusJudgmentPhase = vi.fn();
|
||||||
|
|
||||||
vi.mock('../workflow/phase-runner.js', () => ({
|
vi.mock('../workflow/engine/phase-runner.js', () => ({
|
||||||
needsStatusJudgmentPhase: (...args: unknown[]) => mockNeedsStatusJudgmentPhase(...args),
|
needsStatusJudgmentPhase: (...args: unknown[]) => mockNeedsStatusJudgmentPhase(...args),
|
||||||
runReportPhase: (...args: unknown[]) => mockRunReportPhase(...args),
|
runReportPhase: (...args: unknown[]) => mockRunReportPhase(...args),
|
||||||
runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...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),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
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),
|
needsStatusJudgmentPhase: vi.fn().mockReturnValue(false),
|
||||||
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
runReportPhase: vi.fn().mockResolvedValue(undefined),
|
||||||
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
runStatusJudgmentPhase: vi.fn().mockResolvedValue(''),
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
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';
|
import type { StreamEvent } from '../claude/types.js';
|
||||||
|
|
||||||
describe('ParallelLogger', () => {
|
describe('ParallelLogger', () => {
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
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';
|
import type { WorkflowStep } from '../models/types.js';
|
||||||
|
|
||||||
function createStepWithRules(rules: { condition: string; next: string }[]): WorkflowStep {
|
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 { z } from 'zod/v4';
|
||||||
|
import { AgentModelSchema, AgentConfigSchema } from './schemas.js';
|
||||||
|
|
||||||
export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet');
|
export { AgentModelSchema, AgentConfigSchema };
|
||||||
|
|
||||||
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 type AgentModel = z.infer<typeof AgentModelSchema>;
|
export type AgentModel = z.infer<typeof AgentModelSchema>;
|
||||||
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
export type AgentConfig = z.infer<typeof AgentConfigSchema>;
|
||||||
|
|||||||
@ -1,19 +1,7 @@
|
|||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import { AgentModelSchema } from './agent.js';
|
import { TaktConfigSchema } from './schemas.js';
|
||||||
|
|
||||||
const ClaudeConfigSchema = z.object({
|
export { TaktConfigSchema };
|
||||||
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 type TaktConfig = z.infer<typeof TaktConfigSchema>;
|
export type TaktConfig = z.infer<typeof TaktConfigSchema>;
|
||||||
|
|
||||||
|
|||||||
@ -18,15 +18,6 @@ export type {
|
|||||||
// Re-export from agent.ts
|
// Re-export from agent.ts
|
||||||
export * from './agent.js';
|
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
|
// Re-export from config.ts
|
||||||
export * from './config.js';
|
export * from './config.js';
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,35 @@
|
|||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
import { DEFAULT_LANGUAGE } from '../constants.js';
|
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 */
|
/** Agent type schema */
|
||||||
export const AgentTypeSchema = z.enum(['coder', 'architect', 'supervisor', 'custom']);
|
export const AgentTypeSchema = z.enum(['coder', 'architect', 'supervisor', 'custom']);
|
||||||
|
|
||||||
|
|||||||
@ -1,49 +1,4 @@
|
|||||||
import { z } from 'zod/v4';
|
// Workflow schemas and types are defined in:
|
||||||
import { AgentModelSchema } from './agent.js';
|
// - schemas.ts (Zod schemas for YAML parsing)
|
||||||
|
// - workflow-types.ts (runtime types)
|
||||||
export const WorkflowStepSchema = z.object({
|
// This file is kept as a namespace placeholder.
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import type { WorkflowStep, WorkflowState, Language } from '../../models/types.js';
|
import type { WorkflowStep, WorkflowState, Language } from '../../models/types.js';
|
||||||
import type { RunAgentOptions } from '../../agents/runner.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';
|
import type { WorkflowEngineOptions } from '../types.js';
|
||||||
|
|
||||||
export class OptionsBuilder {
|
export class OptionsBuilder {
|
||||||
|
|||||||
@ -11,10 +11,10 @@ import type {
|
|||||||
AgentResponse,
|
AgentResponse,
|
||||||
} from '../../models/types.js';
|
} from '../../models/types.js';
|
||||||
import { runAgent } from '../../agents/runner.js';
|
import { runAgent } from '../../agents/runner.js';
|
||||||
import { ParallelLogger } from '../parallel-logger.js';
|
import { ParallelLogger } from './parallel-logger.js';
|
||||||
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js';
|
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from './phase-runner.js';
|
||||||
import { detectMatchedRule } from '../evaluation/index.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 { createLogger } from '../../utils/debug.js';
|
||||||
import type { OptionsBuilder } from './OptionsBuilder.js';
|
import type { OptionsBuilder } from './OptionsBuilder.js';
|
||||||
import type { StepExecutor } from './StepExecutor.js';
|
import type { StepExecutor } from './StepExecutor.js';
|
||||||
|
|||||||
@ -16,9 +16,9 @@ import type {
|
|||||||
} from '../../models/types.js';
|
} from '../../models/types.js';
|
||||||
import { runAgent } from '../../agents/runner.js';
|
import { runAgent } from '../../agents/runner.js';
|
||||||
import { InstructionBuilder, isReportObjectConfig } from '../instruction/InstructionBuilder.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 { 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 { createLogger } from '../../utils/debug.js';
|
||||||
import type { OptionsBuilder } from './OptionsBuilder.js';
|
import type { OptionsBuilder } from './OptionsBuilder.js';
|
||||||
|
|
||||||
|
|||||||
@ -17,14 +17,14 @@ import type {
|
|||||||
} from '../../models/types.js';
|
} from '../../models/types.js';
|
||||||
import { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from '../constants.js';
|
import { COMPLETE_STEP, ABORT_STEP, ERROR_MESSAGES } from '../constants.js';
|
||||||
import type { WorkflowEngineOptions } from '../types.js';
|
import type { WorkflowEngineOptions } from '../types.js';
|
||||||
import { determineNextStepByRules } from '../transitions.js';
|
import { determineNextStepByRules } from './transitions.js';
|
||||||
import { LoopDetector } from '../loop-detector.js';
|
import { LoopDetector } from './loop-detector.js';
|
||||||
import { handleBlocked } from '../blocked-handler.js';
|
import { handleBlocked } from './blocked-handler.js';
|
||||||
import {
|
import {
|
||||||
createInitialState,
|
createInitialState,
|
||||||
addUserInput as addUserInputToState,
|
addUserInput as addUserInputToState,
|
||||||
incrementStepIteration,
|
incrementStepIteration,
|
||||||
} from '../state-manager.js';
|
} from './state-manager.js';
|
||||||
import { generateReportDir } from '../../utils/session.js';
|
import { generateReportDir } from '../../utils/session.js';
|
||||||
import { getErrorMessage } from '../../utils/error.js';
|
import { getErrorMessage } from '../../utils/error.js';
|
||||||
import { createLogger } from '../../utils/debug.js';
|
import { createLogger } from '../../utils/debug.js';
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
* requesting user input to continue.
|
* requesting user input to continue.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WorkflowStep, AgentResponse } from '../models/types.js';
|
import type { WorkflowStep, AgentResponse } from '../../models/types.js';
|
||||||
import type { UserInputRequest, WorkflowEngineOptions } from './types.js';
|
import type { UserInputRequest, WorkflowEngineOptions } from '../types.js';
|
||||||
import { extractBlockedPrompt } from './transitions.js';
|
import { extractBlockedPrompt } from './transitions.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -5,8 +5,8 @@
|
|||||||
* which may indicate an infinite loop.
|
* which may indicate an infinite loop.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { LoopDetectionConfig } from '../models/types.js';
|
import type { LoopDetectionConfig } from '../../models/types.js';
|
||||||
import type { LoopCheckResult } from './types.js';
|
import type { LoopCheckResult } from '../types.js';
|
||||||
|
|
||||||
/** Default loop detection settings */
|
/** Default loop detection settings */
|
||||||
const DEFAULT_LOOP_DETECTION: Required<LoopDetectionConfig> = {
|
const DEFAULT_LOOP_DETECTION: Required<LoopDetectionConfig> = {
|
||||||
@ -6,7 +6,7 @@
|
|||||||
* aligned to the longest sub-step name.
|
* 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) */
|
/** 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
|
const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const; // cyan, yellow, magenta, green
|
||||||
@ -5,12 +5,12 @@
|
|||||||
* as session-resume operations.
|
* as session-resume operations.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WorkflowStep, Language } from '../models/types.js';
|
import type { WorkflowStep, Language } from '../../models/types.js';
|
||||||
import { runAgent, type RunAgentOptions } from '../agents/runner.js';
|
import { runAgent, type RunAgentOptions } from '../../agents/runner.js';
|
||||||
import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js';
|
import { ReportInstructionBuilder } from '../instruction/ReportInstructionBuilder.js';
|
||||||
import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js';
|
import { StatusJudgmentBuilder } from '../instruction/StatusJudgmentBuilder.js';
|
||||||
import { hasTagBasedRules } from './rule-utils.js';
|
import { hasTagBasedRules } from '../evaluation/rule-utils.js';
|
||||||
import { createLogger } from '../utils/debug.js';
|
import { createLogger } from '../../utils/debug.js';
|
||||||
|
|
||||||
const log = createLogger('phase-runner');
|
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 {
|
import type {
|
||||||
WorkflowStep,
|
WorkflowStep,
|
||||||
} from '../models/types.js';
|
} from '../../models/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine next step using rules-based detection.
|
* Determine next step using rules-based detection.
|
||||||
@ -2,7 +2,7 @@
|
|||||||
* Shared rule utility functions used by both engine.ts and instruction-builder.ts.
|
* 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
|
* Check whether a step has tag-based rules (i.e., rules that require
|
||||||
@ -22,28 +22,28 @@ export type {
|
|||||||
LoopCheckResult,
|
LoopCheckResult,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// Transitions
|
// Transitions (engine/)
|
||||||
export { determineNextStepByRules, extractBlockedPrompt } from './transitions.js';
|
export { determineNextStepByRules, extractBlockedPrompt } from './engine/transitions.js';
|
||||||
|
|
||||||
// Loop detection
|
// Loop detection (engine/)
|
||||||
export { LoopDetector } from './loop-detector.js';
|
export { LoopDetector } from './engine/loop-detector.js';
|
||||||
|
|
||||||
// State management
|
// State management (engine/)
|
||||||
export {
|
export {
|
||||||
createInitialState,
|
createInitialState,
|
||||||
addUserInput,
|
addUserInput,
|
||||||
getPreviousOutput,
|
getPreviousOutput,
|
||||||
} from './state-manager.js';
|
} from './engine/state-manager.js';
|
||||||
|
|
||||||
|
// Blocked handling (engine/)
|
||||||
|
export { handleBlocked, type BlockedHandlerResult } from './engine/blocked-handler.js';
|
||||||
|
|
||||||
// Instruction building
|
// Instruction building
|
||||||
export { InstructionBuilder, isReportObjectConfig } from './instruction/InstructionBuilder.js';
|
export { InstructionBuilder, isReportObjectConfig } from './instruction/InstructionBuilder.js';
|
||||||
export { ReportInstructionBuilder, type ReportInstructionContext } from './instruction/ReportInstructionBuilder.js';
|
export { ReportInstructionBuilder, type ReportInstructionContext } from './instruction/ReportInstructionBuilder.js';
|
||||||
export { StatusJudgmentBuilder, type StatusJudgmentContext } from './instruction/StatusJudgmentBuilder.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
|
// Rule evaluation
|
||||||
export { RuleEvaluator, type RuleMatch, type RuleEvaluatorContext, detectMatchedRule, evaluateAggregateConditions } from './evaluation/index.js';
|
export { RuleEvaluator, type RuleMatch, type RuleEvaluatorContext, detectMatchedRule, evaluateAggregateConditions } from './evaluation/index.js';
|
||||||
export { AggregateEvaluator } from './evaluation/AggregateEvaluator.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 type { WorkflowStep, Language, ReportConfig, ReportObjectConfig } from '../../models/types.js';
|
||||||
import { hasTagBasedRules } from '../rule-utils.js';
|
import { hasTagBasedRules } from '../evaluation/rule-utils.js';
|
||||||
import type { InstructionContext } from '../instruction-context.js';
|
import type { InstructionContext } from './instruction-context.js';
|
||||||
import { buildExecutionMetadata, renderExecutionMetadata } from '../instruction-context.js';
|
import { buildExecutionMetadata, renderExecutionMetadata } from './instruction-context.js';
|
||||||
import { generateStatusRulesFromRules } from '../status-rules.js';
|
import { generateStatusRulesFromRules } from './status-rules.js';
|
||||||
import { escapeTemplateChars, replaceTemplatePlaceholders } from './escape.js';
|
import { escapeTemplateChars, replaceTemplatePlaceholders } from './escape.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -11,8 +11,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WorkflowStep, Language } from '../../models/types.js';
|
import type { WorkflowStep, Language } from '../../models/types.js';
|
||||||
import type { InstructionContext } from '../instruction-context.js';
|
import type { InstructionContext } from './instruction-context.js';
|
||||||
import { METADATA_STRINGS } from '../instruction-context.js';
|
import { METADATA_STRINGS } from './instruction-context.js';
|
||||||
import { replaceTemplatePlaceholders } from './escape.js';
|
import { replaceTemplatePlaceholders } from './escape.js';
|
||||||
import { isReportObjectConfig, renderReportContext, renderReportOutputInstruction } from './InstructionBuilder.js';
|
import { isReportObjectConfig, renderReportContext, renderReportOutputInstruction } from './InstructionBuilder.js';
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WorkflowStep, Language } from '../../models/types.js';
|
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 */
|
/** Localized strings for status judgment phase */
|
||||||
const STATUS_JUDGMENT_STRINGS = {
|
const STATUS_JUDGMENT_STRINGS = {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { WorkflowStep } from '../../models/types.js';
|
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.
|
* Escape special characters in dynamic content to prevent template injection.
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* and renders execution metadata (working directory, rules) as markdown.
|
* 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.
|
* Context for building instruction from template.
|
||||||
@ -5,7 +5,7 @@
|
|||||||
* based on the step's rule configuration.
|
* 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 */
|
/** Localized strings for rules-based status prompt */
|
||||||
const RULES_PROMPT_STRINGS = {
|
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