This commit is contained in:
nrslib 2026-02-02 17:11:42 +09:00
parent e57e5e7226
commit 7d8ba10abb
195 changed files with 1576 additions and 850 deletions

View File

@ -2,7 +2,7 @@
このリポジトリに貢献する際の基本的な構成と期待値をまとめています。短い説明と例で各セクションを完結に示します。 このリポジトリに貢献する際の基本的な構成と期待値をまとめています。短い説明と例で各セクションを完結に示します。
## プロジェクト構成とモジュール整理 ## プロジェクト構成とモジュール整理
- 主要ソースは `src/` にあり、エントリポイントは `src/index.ts`、CLI は `src/cli.ts` です。 - 主要ソースは `src/` にあり、エントリポイントは `src/index.ts`、CLI は `src/app/cli/index.ts` です。
- テストは `src/__tests__/` に置き、ファイル名は対象機能が一目でわかるようにします(例: `client.test.ts`)。 - テストは `src/__tests__/` に置き、ファイル名は対象機能が一目でわかるようにします(例: `client.test.ts`)。
- ビルド成果物は `dist/`、実行スクリプトは `bin/`、静的リソースは `resources/`、ドキュメントは `docs/` で管理します。 - ビルド成果物は `dist/`、実行スクリプトは `bin/`、静的リソースは `resources/`、ドキュメントは `docs/` で管理します。
- 設定やキャッシュを使う際は `~/.takt/` 以下(実行時)や `.takt/`(プロジェクト固有)を参照します。 - 設定やキャッシュを使う際は `~/.takt/` 以下(実行時)や `.takt/`(プロジェクト固有)を参照します。

View File

@ -31,7 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Internal ### Internal
- Refactored instruction builder: extracted context assembly and status rules logic (#44) - Refactored instruction builder: extracted context assembly and status rules logic (#44)
- Introduced `src/task/git.ts` for DRY git commit operations - Introduced `src/infra/task/git.ts` for DRY git commit operations
- Unified error handling with `getErrorMessage()` - Unified error handling with `getErrorMessage()`
- Made `projectCwd` required throughout codebase - Made `projectCwd` required throughout codebase
- Removed deprecated `sacrificeMode` - Removed deprecated `sacrificeMode`

View File

@ -61,7 +61,7 @@ Each step executes in up to 3 phases (session is resumed across phases):
| Phase 2 | Report output | Write only | When `step.report` is defined | | Phase 2 | Report output | Write only | When `step.report` is defined |
| Phase 3 | Status judgment | None (judgment only) | When step has tag-based rules | | Phase 3 | Status judgment | None (judgment only) | When step has tag-based rules |
Phase 2/3 are implemented in `src/workflow/phase-runner.ts`. The session is resumed so the agent retains context from Phase 1. Phase 2/3 are implemented in `src/core/workflow/engine/phase-runner.ts`. The session is resumed so the agent retains context from Phase 1.
### Rule Evaluation (5-Stage Fallback) ### Rule Evaluation (5-Stage Fallback)
@ -73,11 +73,11 @@ After step execution, rules are evaluated to determine the next step. Evaluation
4. **AI judge (ai() only)** - AI evaluates `ai("condition text")` rules 4. **AI judge (ai() only)** - AI evaluates `ai("condition text")` rules
5. **AI judge fallback** - AI evaluates ALL conditions as final resort 5. **AI judge fallback** - AI evaluates ALL conditions as final resort
Implemented in `src/workflow/rule-evaluator.ts`. The matched method is tracked as `RuleMatchMethod` type. Implemented in `src/core/workflow/evaluation/RuleEvaluator.ts`. The matched method is tracked as `RuleMatchMethod` type.
### Key Components ### Key Components
**WorkflowEngine** (`src/workflow/engine.ts`) **WorkflowEngine** (`src/core/workflow/engine/WorkflowEngine.ts`)
- State machine that orchestrates agent execution via EventEmitter - State machine that orchestrates agent execution via EventEmitter
- Manages step transitions based on rule evaluation results - Manages step transitions based on rule evaluation results
- Emits events: `step:start`, `step:complete`, `step:blocked`, `step:loop_detected`, `workflow:complete`, `workflow:abort`, `iteration:limit` - Emits events: `step:start`, `step:complete`, `step:blocked`, `step:loop_detected`, `workflow:complete`, `workflow:abort`, `iteration:limit`
@ -85,7 +85,7 @@ Implemented in `src/workflow/rule-evaluator.ts`. The matched method is tracked a
- Maintains agent sessions per step for conversation continuity - Maintains agent sessions per step for conversation continuity
- Parallel step execution via `runParallelStep()` with `Promise.all()` - Parallel step execution via `runParallelStep()` with `Promise.all()`
**Instruction Builder** (`src/workflow/instruction-builder.ts`) **Instruction Builder** (`src/core/workflow/instruction/InstructionBuilder.ts`)
- Auto-injects standard sections into every instruction (no need for `{task}` or `{previous_response}` placeholders in templates): - Auto-injects standard sections into every instruction (no need for `{task}` or `{previous_response}` placeholders in templates):
1. Execution context (working dir, edit permission rules) 1. Execution context (working dir, edit permission rules)
2. Workflow context (iteration counts, report dir) 2. Workflow context (iteration counts, report dir)
@ -111,18 +111,18 @@ Implemented in `src/workflow/rule-evaluator.ts`. The matched method is tracked a
- `executor.ts` - Query execution using `@anthropic-ai/claude-agent-sdk` - `executor.ts` - Query execution using `@anthropic-ai/claude-agent-sdk`
- `query-manager.ts` - Concurrent query tracking with query IDs - `query-manager.ts` - Concurrent query tracking with query IDs
**Configuration** (`src/config/`) **Configuration** (`src/infra/config/`)
- `loader.ts` - Custom agent loading from `.takt/agents.yaml` - `loader.ts` - Custom agent loading from `.takt/agents.yaml`
- `workflowLoader.ts` - YAML workflow parsing with Zod validation; resolves user workflows (`~/.takt/workflows/`) with builtin fallback (`resources/global/{lang}/workflows/`) - `workflowLoader.ts` - YAML workflow parsing with Zod validation; resolves user workflows (`~/.takt/workflows/`) with builtin fallback (`resources/global/{lang}/workflows/`)
- `agentLoader.ts` - Agent prompt file loading - `agentLoader.ts` - Agent prompt file loading
- `paths.ts` - Directory structure (`.takt/`, `~/.takt/`), session management - `paths.ts` - Directory structure (`.takt/`, `~/.takt/`), session management
**Task Management** (`src/task/`) **Task Management** (`src/infra/task/`)
- `runner.ts` - TaskRunner class for managing task files (`.takt/tasks/`) - `runner.ts` - TaskRunner class for managing task files (`.takt/tasks/`)
- `watcher.ts` - TaskWatcher class for polling and auto-executing tasks (used by `/watch`) - `watcher.ts` - TaskWatcher class for polling and auto-executing tasks (used by `/watch`)
- `index.ts` - Task operations (getNextTask, completeTask, addTask) - `index.ts` - Task operations (getNextTask, completeTask, addTask)
**GitHub Integration** (`src/github/issue.ts`) **GitHub Integration** (`src/infra/github/issue.ts`)
- Fetches issues via `gh` CLI, formats as task text with title/body/labels/comments - Fetches issues via `gh` CLI, formats as task text with title/body/labels/comments
### Data Flow ### Data Flow
@ -273,7 +273,7 @@ Files: `.takt/logs/{sessionId}.jsonl`, with `latest.json` pointer. Legacy `.json
- ESM modules with `.js` extensions in imports - ESM modules with `.js` extensions in imports
- Strict TypeScript with `noUncheckedIndexedAccess` - Strict TypeScript with `noUncheckedIndexedAccess`
- Zod schemas for runtime validation (`src/models/schemas.ts`) - Zod schemas for runtime validation (`src/core/models/schemas.ts`)
- Uses `@anthropic-ai/claude-agent-sdk` for Claude integration - Uses `@anthropic-ai/claude-agent-sdk` for Claude integration
## Design Principles ## Design Principles

View File

@ -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', 'index.js'); const cliPath = join(__dirname, '..', 'dist', 'app', 'cli', 'index.js');
try { try {
await import(cliPath); await import(cliPath);

View File

@ -30,7 +30,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
``` ```
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
│ 1. CLI Layer (cli.ts) │ 1. CLI Layer (src/app/cli/index.ts) │
│ ユーザー入力 → 引数パース → コマンド振り分け │ │ ユーザー入力 → 引数パース → コマンド振り分け │
└────────────────────────────┬────────────────────────────────────┘ └────────────────────────────┬────────────────────────────────────┘
@ -304,7 +304,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
## 各レイヤーの詳細 ## 各レイヤーの詳細
### 1. CLI Layer (`src/cli.ts`) ### 1. CLI Layer (`src/app/cli/index.ts`)
**役割**: ユーザー入力の受付とコマンド振り分け **役割**: ユーザー入力の受付とコマンド振り分け
@ -327,7 +327,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
--- ---
### 2. Interactive Layer (`src/commands/interactive/interactive.ts`) ### 2. Interactive Layer (`src/features/interactive/interactive.ts`)
**役割**: タスクの対話的な明確化 **役割**: タスクの対話的な明確化
@ -360,7 +360,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
--- ---
### 3. Execution Orchestration Layer (`src/commands/execution/selectAndExecute.ts`) ### 3. Execution Orchestration Layer (`src/features/tasks/execute/selectAndExecute.ts`)
**役割**: ワークフロー選択とworktree管理 **役割**: ワークフロー選択とworktree管理
@ -398,7 +398,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
### 4. Workflow Execution Layer ### 4. Workflow Execution Layer
#### 4.1 Task Execution (`src/commands/execution/taskExecution.ts`) #### 4.1 Task Execution (`src/features/tasks/execute/taskExecution.ts`)
**役割**: ワークフロー読み込みと実行の橋渡し **役割**: ワークフロー読み込みと実行の橋渡し
@ -417,7 +417,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
**データ出力**: **データ出力**:
- `boolean` (成功/失敗) - `boolean` (成功/失敗)
#### 4.2 Workflow Execution (`src/commands/execution/workflowExecution.ts`) #### 4.2 Workflow Execution (`src/features/tasks/execute/workflowExecution.ts`)
**役割**: セッション管理、イベント購読、ログ記録 **役割**: セッション管理、イベント購読、ログ記録
@ -471,7 +471,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
--- ---
### 5. Engine Layer (`src/workflow/engine/WorkflowEngine.ts`) ### 5. Engine Layer (`src/core/workflow/engine/WorkflowEngine.ts`)
**役割**: ステートマシンによるワークフロー実行制御 **役割**: ステートマシンによるワークフロー実行制御
@ -553,7 +553,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され
### 6. Instruction Building & Step Execution Layer ### 6. Instruction Building & Step Execution Layer
#### 6.1 Step Execution (`src/workflow/engine/StepExecutor.ts`) #### 6.1 Step Execution (`src/core/workflow/engine/StepExecutor.ts`)
**役割**: 3フェーズモデルによるステップ実行 **役割**: 3フェーズモデルによるステップ実行
@ -604,7 +604,7 @@ const match = await detectMatchedRule(step, response.content, tagContent, {...})
- `InstructionBuilder` を使用してインストラクション文字列を生成 - `InstructionBuilder` を使用してインストラクション文字列を生成
- コンテキスト情報を渡す - コンテキスト情報を渡す
#### 6.2 Instruction Building (`src/workflow/instruction/InstructionBuilder.ts`) #### 6.2 Instruction Building (`src/core/workflow/instruction/InstructionBuilder.ts`)
**役割**: Phase 1用のインストラクション文字列生成 **役割**: Phase 1用のインストラクション文字列生成
@ -691,7 +691,7 @@ const match = await detectMatchedRule(step, response.content, tagContent, {...})
- `error?: string` - `error?: string`
- `timestamp: Date` - `timestamp: Date`
#### 7.2 Provider (`src/providers/`) #### 7.2 Provider (`src/infra/providers/`)
**役割**: AIプロバイダー(Claude, Codex)とのSDK通信 **役割**: AIプロバイダー(Claude, Codex)とのSDK通信
@ -881,7 +881,7 @@ new WorkflowEngine(workflowConfig, cwd, task, {
### 1. 会話履歴 → タスク文字列 ### 1. 会話履歴 → タスク文字列
**場所**: `src/commands/interactive/interactive.ts` **場所**: `src/features/interactive/interactive.ts`
```typescript ```typescript
function buildTaskFromHistory(history: ConversationMessage[]): string { function buildTaskFromHistory(history: ConversationMessage[]): string {
@ -897,7 +897,7 @@ function buildTaskFromHistory(history: ConversationMessage[]): string {
### 2. タスク → ブランチスラグ (AI生成) ### 2. タスク → ブランチスラグ (AI生成)
**場所**: `src/task/summarize.ts` (呼び出し: `selectAndExecute.ts`, `taskExecution.ts`) **場所**: `src/infra/task/summarize.ts` (呼び出し: `selectAndExecute.ts`, `taskExecution.ts`)
```typescript ```typescript
await summarizeTaskName(task, { cwd }) await summarizeTaskName(task, { cwd })
@ -914,7 +914,7 @@ await summarizeTaskName(task, { cwd })
### 3. ワークフロー設定 → WorkflowState ### 3. ワークフロー設定 → WorkflowState
**場所**: `src/workflow/state-manager.ts` **場所**: `src/core/workflow/engine/state-manager.ts`
```typescript ```typescript
function createInitialState( function createInitialState(
@ -939,7 +939,7 @@ function createInitialState(
### 4. コンテキスト → インストラクション文字列 ### 4. コンテキスト → インストラクション文字列
**場所**: `src/workflow/instruction/InstructionBuilder.ts` **場所**: `src/core/workflow/instruction/InstructionBuilder.ts`
**入力**: **入力**:
- `step: WorkflowStep` - `step: WorkflowStep`
@ -958,7 +958,7 @@ function createInitialState(
### 5. AgentResponse → ルールマッチ ### 5. AgentResponse → ルールマッチ
**場所**: `src/workflow/evaluation/RuleEvaluator.ts` **場所**: `src/core/workflow/evaluation/RuleEvaluator.ts`
**入力**: **入力**:
- `step: WorkflowStep` - `step: WorkflowStep`
@ -979,7 +979,7 @@ function createInitialState(
### 6. ルールマッチ → 次ステップ名 ### 6. ルールマッチ → 次ステップ名
**場所**: `src/workflow/transitions.ts` **場所**: `src/core/workflow/engine/transitions.ts`
```typescript ```typescript
function determineNextStepByRules( function determineNextStepByRules(
@ -997,7 +997,7 @@ function determineNextStepByRules(
### 7. Provider Response → AgentResponse ### 7. Provider Response → AgentResponse
**場所**: `src/providers/claude.ts`, `src/providers/codex.ts` **場所**: `src/infra/providers/claude.ts`, `src/infra/providers/codex.ts`
**入力**: SDKレスポンス (`ClaudeResult`) **入力**: SDKレスポンス (`ClaudeResult`)

View File

@ -7,7 +7,7 @@
"bin": { "bin": {
"takt": "./bin/takt", "takt": "./bin/takt",
"takt-dev": "./bin/takt", "takt-dev": "./bin/takt",
"takt-cli": "./dist/cli/index.js" "takt-cli": "./dist/app/cli/index.js"
}, },
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",

View File

@ -0,0 +1,7 @@
You are a task summarizer. Convert the conversation into a concrete task instruction for the planning step.
Requirements:
- Output only the final task instruction (no preamble).
- Be specific about scope and targets (files/modules) if mentioned.
- Preserve constraints and "do not" instructions.
- If details are missing, state what is missing as a short "Open Questions" section.

View File

@ -0,0 +1,16 @@
You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process.
## Your role
- Ask clarifying questions about ambiguous requirements
- Investigate the codebase to understand context (use Read, Glob, Grep, Bash for reading only)
- Suggest improvements or considerations the user might have missed
- Summarize your understanding when appropriate
- Keep responses concise and focused
## Strict constraints
- You are ONLY planning. Do NOT execute the task.
- Do NOT create, edit, or delete any files.
- Do NOT run build, test, install, or any commands that modify state.
- Bash is allowed ONLY for read-only investigation (e.g. ls, cat, git log, git diff). Never run destructive or write commands.
- Do NOT mention or reference any slash commands. You have no knowledge of them.
- When the user is satisfied with the plan, they will proceed on their own. Do NOT instruct them on what to do next.

View File

@ -103,11 +103,14 @@ steps:
rules: rules:
- condition: Implementation complete - condition: Implementation complete
next: ai_review next: ai_review
- condition: No implementation (report only)
next: plan
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: plan next: plan
instruction_template: | instruction_template: |
Follow the plan from the plan step and implement. Follow the plan from the plan step and implement.
Refer to the plan report (00-plan.md) and proceed with implementation. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory.
**Scope report format (create at implementation start):** **Scope report format (create at implementation start):**
```markdown ```markdown
@ -139,6 +142,17 @@ steps:
- **Reason**: {Why this option was chosen} - **Reason**: {Why this option was chosen}
``` ```
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
**No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
- name: ai_review - name: ai_review
edit: false edit: false
agent: ../agents/default/ai-antipattern-reviewer.md agent: ../agents/default/ai-antipattern-reviewer.md
@ -193,6 +207,7 @@ steps:
- name: ai_fix - name: ai_fix
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
session: refresh
allowed_tools: allowed_tools:
- Read - Read
- Glob - Glob
@ -206,6 +221,8 @@ steps:
rules: rules:
- condition: AI issues fixed - condition: AI issues fixed
next: ai_review next: ai_review
- condition: No fix needed (verified target files/spec)
next: plan
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: plan next: plan
instruction_template: | instruction_template: |
@ -232,6 +249,21 @@ steps:
**Absolutely prohibited:** **Absolutely prohibited:**
- Reporting "fixed" without opening files - Reporting "fixed" without opening files
**Handling "no fix needed" (required)**
- Do not claim "no fix needed" unless you can show the checked target file(s) for each AI Review issue
- If an issue involves generated code or spec sync, and you cannot verify the source spec, output the tag for "Unable to proceed with fixes"
- When "no fix needed", output the tag for "Unable to proceed with fixes" and include the reason + checked scope
**Required output (include headings)**
## Files checked
- {path:line}
## Searches run
- {command and summary}
## Fixes applied
- {what changed}
## Test results
- {command and outcome}
- Judging based on assumptions - Judging based on assumptions
- Leaving problems that AI Reviewer REJECTED - Leaving problems that AI Reviewer REJECTED
pass_previous_response: true pass_previous_response: true
@ -389,7 +421,7 @@ steps:
Run tests, verify the build, and perform final approval. Run tests, verify the build, and perform final approval.
**Workflow Overall Review:** **Workflow Overall Review:**
1. Does the implementation match the plan (00-plan.md)? 1. Does the implementation match the plan ({report:00-plan.md})?
2. Were all review step issues addressed? 2. Were all review step issues addressed?
3. Was the original task objective achieved? 3. Was the original task objective achieved?

View File

@ -100,7 +100,8 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
Follow the plan from the plan step and implement. Follow the plan from the plan step and implement.
Refer to the plan report (00-plan.md) and proceed with implementation. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory.
**Scope report format (create at implementation start):** **Scope report format (create at implementation start):**
```markdown ```markdown
@ -131,9 +132,19 @@ steps:
- **Options Considered**: {List of options} - **Options Considered**: {List of options}
- **Reason**: {Why this option was chosen} - **Reason**: {Why this option was chosen}
``` ```
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
rules: rules:
- condition: Implementation is complete - condition: Implementation is complete
next: ai_review next: ai_review
- condition: No implementation (report only)
next: plan
- condition: Cannot proceed with implementation - condition: Cannot proceed with implementation
next: plan next: plan
@ -194,6 +205,7 @@ steps:
- name: ai_fix - name: ai_fix
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
session: refresh
allowed_tools: allowed_tools:
- Read - Read
- Glob - Glob
@ -229,10 +241,30 @@ steps:
- Reporting "fixed" without opening files - Reporting "fixed" without opening files
- Judging based on assumptions - Judging based on assumptions
- Leaving problems that AI Reviewer REJECTED - Leaving problems that AI Reviewer REJECTED
**Handling "no fix needed" (required)**
- Do not claim "no fix needed" unless you can show the checked target file(s) for each AI Review issue
- If an issue involves generated code or spec sync, and you cannot verify the source spec, output the tag for "Unable to proceed with fixes"
- When "no fix needed", output the tag for "Unable to proceed with fixes" and include the reason + checked scope
**Required output (include headings)**
## Files checked
- {path:line}
## Searches run
- {command and summary}
## Fixes applied
- {what changed}
## Test results
- {command and outcome}
**No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewer's issues have been fixed - condition: AI Reviewer's issues have been fixed
next: ai_review next: ai_review
- condition: No fix needed (verified target files/spec)
next: plan
- condition: Unable to proceed with fixes - condition: Unable to proceed with fixes
next: plan next: plan
@ -507,7 +539,7 @@ steps:
Run tests, verify the build, and perform final approval. Run tests, verify the build, and perform final approval.
**Workflow Overall Review:** **Workflow Overall Review:**
1. Does the implementation match the plan (00-plan.md)? 1. Does the implementation match the plan ({report:00-plan.md})?
2. Were all review step issues addressed? 2. Were all review step issues addressed?
3. Was the original task objective achieved? 3. Was the original task objective achieved?

View File

@ -112,7 +112,8 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
Follow the plan from the plan step and implement. Follow the plan from the plan step and implement.
Refer to the plan report (00-plan.md) and proceed with implementation. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory.
**Scope report format (create at implementation start):** **Scope report format (create at implementation start):**
```markdown ```markdown
@ -143,9 +144,19 @@ steps:
- **Options Considered**: {List of options} - **Options Considered**: {List of options}
- **Reason**: {Why this option was chosen} - **Reason**: {Why this option was chosen}
``` ```
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
rules: rules:
- condition: Implementation is complete - condition: Implementation is complete
next: ai_review next: ai_review
- condition: No implementation (report only)
next: plan
- condition: Cannot proceed with implementation - condition: Cannot proceed with implementation
next: plan next: plan
@ -206,6 +217,7 @@ steps:
- name: ai_fix - name: ai_fix
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
session: refresh
allowed_tools: allowed_tools:
- Read - Read
- Glob - Glob
@ -242,10 +254,30 @@ steps:
- Judging based on assumptions - Judging based on assumptions
- Leaving problems that AI Reviewer REJECTED - Leaving problems that AI Reviewer REJECTED
- Removing scope creep - Removing scope creep
**Handling "no fix needed" (required)**
- Do not claim "no fix needed" unless you can show the checked target file(s) for each AI Review issue
- If an issue involves generated code or spec sync, and you cannot verify the source spec, output the tag for "Unable to proceed with fixes"
- When "no fix needed", output the tag for "Unable to proceed with fixes" and include the reason + checked scope
**Required output (include headings)**
## Files checked
- {path:line}
## Searches run
- {command and summary}
## Fixes applied
- {what changed}
## Test results
- {command and outcome}
**No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewer's issues have been fixed - condition: AI Reviewer's issues have been fixed
next: ai_review next: ai_review
- condition: No fix needed (verified target files/spec)
next: plan
- condition: Unable to proceed with fixes - condition: Unable to proceed with fixes
next: plan next: plan
@ -520,7 +552,7 @@ steps:
Run tests, verify the build, and perform final approval. Run tests, verify the build, and perform final approval.
**Workflow Overall Review:** **Workflow Overall Review:**
1. Does the implementation match the plan (00-plan.md)? 1. Does the implementation match the plan ({report:00-plan.md})?
2. Were all review step issues addressed? 2. Were all review step issues addressed?
3. Was the original task objective achieved? 3. Was the original task objective achieved?

View File

@ -99,11 +99,14 @@ steps:
rules: rules:
- condition: Implementation complete - condition: Implementation complete
next: ai_review next: ai_review
- condition: No implementation (report only)
next: plan
- condition: Cannot proceed, insufficient info - condition: Cannot proceed, insufficient info
next: plan next: plan
instruction_template: | instruction_template: |
Follow the plan from the plan step and implement. Follow the plan from the plan step and implement.
Refer to the plan report (00-plan.md) and proceed with implementation. Refer to the plan report ({report:00-plan.md}) and proceed with implementation.
Use only the Report Directory files shown in Workflow Context. Do not search or open reports outside that directory.
**Scope report format (create at implementation start):** **Scope report format (create at implementation start):**
```markdown ```markdown
@ -135,6 +138,25 @@ steps:
- **Reason**: {Why this option was chosen} - **Reason**: {Why this option was chosen}
``` ```
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
**No-implementation handling (required)**
- If you only produced reports and made no code changes, output the tag for "No implementation (report only)"
**Required output (include headings)**
## Work done
- {summary of work performed}
## Changes made
- {summary of code changes}
## Test results
- {command and outcome}
- name: ai_review - name: ai_review
edit: false edit: false
agent: ../agents/default/ai-antipattern-reviewer.md agent: ../agents/default/ai-antipattern-reviewer.md
@ -260,7 +282,7 @@ steps:
Run tests, verify the build, and perform final approval. Run tests, verify the build, and perform final approval.
**Workflow Overall Review:** **Workflow Overall Review:**
1. Does the implementation match the plan (00-plan.md)? 1. Does the implementation match the plan ({report:00-plan.md})?
2. Were all review step issues addressed? 2. Were all review step issues addressed?
3. Was the original task objective achieved? 3. Was the original task objective achieved?

View File

@ -0,0 +1,7 @@
あなたはタスク要約者です。会話を計画ステップ向けの具体的なタスク指示に変換してください。
要件:
- 出力は最終的な指示のみ(前置き不要)
- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く
- 制約や「やらないこと」を保持する
- 情報不足があれば「Open Questions」セクションを短く付ける

View File

@ -0,0 +1,16 @@
あなたはタスク計画のアシスタントです。会話を通じて要件の明確化・整理を手伝います。今は計画フェーズで、実行は別プロセスで行われます。
## 役割
- あいまいな要求に対して確認質問をする
- コードベースの前提を把握するRead/Glob/Grep/Bash は読み取りのみ)
- 見落としそうな点や改善点を提案する
- 必要に応じて理解した内容を簡潔にまとめる
- 返答は簡潔で要点のみ
## 厳守事項
- 計画のみを行い、実装はしない
- ファイルの作成/編集/削除はしない
- build/test/install など状態を変えるコマンドは実行しない
- Bash は読み取り専用ls/cat/git log/git diff など)に限定
- スラッシュコマンドに言及しない(存在を知らない前提)
- ユーザーが満足したら次工程に進む。次の指示はしない

View File

@ -94,11 +94,14 @@ steps:
rules: rules:
- condition: 実装完了 - condition: 実装完了
next: ai_review next: ai_review
- condition: 実装未着手(レポートのみ)
next: plan
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
next: plan next: plan
instruction_template: | instruction_template: |
planステップで立てた計画に従って実装してください。 planステップで立てた計画に従って実装してください。
計画レポート00-plan.mdを参照し、実装を進めてください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。
Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
**重要**: 実装と同時に単体テストを追加してください。 **重要**: 実装と同時に単体テストを追加してください。
- 新規作成したクラス・関数には単体テストを追加 - 新規作成したクラス・関数には単体テストを追加
@ -135,6 +138,17 @@ steps:
- **理由**: {選んだ理由} - **理由**: {選んだ理由}
``` ```
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
- name: ai_review - name: ai_review
edit: false edit: false
agent: ../agents/default/ai-antipattern-reviewer.md agent: ../agents/default/ai-antipattern-reviewer.md
@ -189,6 +203,7 @@ steps:
- name: ai_fix - name: ai_fix
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
session: refresh
allowed_tools: allowed_tools:
- Read - Read
- Glob - Glob
@ -199,9 +214,12 @@ steps:
- WebSearch - WebSearch
- WebFetch - WebFetch
permission_mode: acceptEdits permission_mode: acceptEdits
pass_previous_response: true
rules: rules:
- condition: AI問題の修正完了 - condition: AI問題の修正完了
next: ai_review next: ai_review
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
next: plan
- condition: 判断できない、情報不足 - condition: 判断できない、情報不足
next: plan next: plan
instruction_template: | instruction_template: |
@ -228,9 +246,23 @@ steps:
**絶対に禁止:** **絶対に禁止:**
- ファイルを開かずに「修正済み」と報告 - ファイルを開かずに「修正済み」と報告
**修正不要の扱い(必須)**
- AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない
- 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「判断できない、情報不足」に対応するタグを出力する
- 修正不要の場合は「判断できない、情報不足」に対応するタグを出力し、理由と確認範囲を明記する
**必須出力(見出しを含める)**
## 確認したファイル
- {ファイルパス:行番号}
## 実行した検索
- {コマンドと要約}
## 修正内容
- {変更内容}
## テスト結果
- {実行コマンドと結果}
- 思い込みで判断 - 思い込みで判断
- AI Reviewer が REJECT した問題の放置 - AI Reviewer が REJECT した問題の放置
pass_previous_response: true
- name: reviewers - name: reviewers
parallel: parallel:
@ -395,7 +427,7 @@ steps:
テスト実行、ビルド確認、最終承認を行ってください。 テスト実行、ビルド確認、最終承認を行ってください。
**ワークフロー全体の確認:** **ワークフロー全体の確認:**
1. 計画(00-plan.mdと実装結果が一致しているか 1. 計画({report:00-plan.md})と実装結果が一致しているか
2. 各レビューステップの指摘が対応されているか 2. 各レビューステップの指摘が対応されているか
3. 元のタスク目的が達成されているか 3. 元のタスク目的が達成されているか

View File

@ -109,7 +109,8 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
planステップで立てた計画に従って実装してください。 planステップで立てた計画に従って実装してください。
計画レポート00-plan.mdを参照し、実装を進めてください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。
Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
**Scopeレポートフォーマット実装開始時に作成:** **Scopeレポートフォーマット実装開始時に作成:**
```markdown ```markdown
@ -140,9 +141,19 @@ steps:
- **検討した選択肢**: {選択肢リスト} - **検討した選択肢**: {選択肢リスト}
- **理由**: {選んだ理由} - **理由**: {選んだ理由}
``` ```
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
rules: rules:
- condition: 実装が完了した - condition: 実装が完了した
next: ai_review next: ai_review
- condition: 実装未着手(レポートのみ)
next: plan
- condition: 実装を進行できない - condition: 実装を進行できない
next: plan next: plan
@ -203,6 +214,7 @@ steps:
- name: ai_fix - name: ai_fix
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
session: refresh
allowed_tools: allowed_tools:
- Read - Read
- Glob - Glob
@ -238,10 +250,30 @@ steps:
- ファイルを開かずに「修正済み」と報告 - ファイルを開かずに「修正済み」と報告
- 思い込みで判断 - 思い込みで判断
- AI Reviewer が REJECT した問題の放置 - AI Reviewer が REJECT した問題の放置
**修正不要の扱い(必須)**
- AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない
- 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「修正を進行できない」に対応するタグを出力する
- 修正不要の場合は「修正を進行できない」に対応するタグを出力し、理由と確認範囲を明記する
**必須出力(見出しを含める)**
## 確認したファイル
- {ファイルパス:行番号}
## 実行した検索
- {コマンドと要約}
## 修正内容
- {変更内容}
## テスト結果
- {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewerの指摘に対する修正が完了した - condition: AI Reviewerの指摘に対する修正が完了した
next: ai_review next: ai_review
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
next: plan
- condition: 修正を進行できない - condition: 修正を進行できない
next: plan next: plan
@ -516,7 +548,7 @@ steps:
テスト実行、ビルド確認、最終承認を行ってください。 テスト実行、ビルド確認、最終承認を行ってください。
**ワークフロー全体の確認:** **ワークフロー全体の確認:**
1. 計画(00-plan.mdと実装結果が一致しているか 1. 計画({report:00-plan.md})と実装結果が一致しているか
2. 各レビューステップの指摘が対応されているか 2. 各レビューステップの指摘が対応されているか
3. 元のタスク目的が達成されているか 3. 元のタスク目的が達成されているか

View File

@ -100,7 +100,8 @@ steps:
- WebFetch - WebFetch
instruction_template: | instruction_template: |
planステップで立てた計画に従って実装してください。 planステップで立てた計画に従って実装してください。
計画レポート00-plan.mdを参照し、実装を進めてください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。
Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
**Scopeレポートフォーマット実装開始時に作成:** **Scopeレポートフォーマット実装開始時に作成:**
```markdown ```markdown
@ -131,9 +132,19 @@ steps:
- **検討した選択肢**: {選択肢リスト} - **検討した選択肢**: {選択肢リスト}
- **理由**: {選んだ理由} - **理由**: {選んだ理由}
``` ```
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
rules: rules:
- condition: 実装が完了した - condition: 実装が完了した
next: ai_review next: ai_review
- condition: 実装未着手(レポートのみ)
next: plan
- condition: 実装を進行できない - condition: 実装を進行できない
next: plan next: plan
@ -194,6 +205,7 @@ steps:
- name: ai_fix - name: ai_fix
edit: true edit: true
agent: ../agents/default/coder.md agent: ../agents/default/coder.md
session: refresh
allowed_tools: allowed_tools:
- Read - Read
- Glob - Glob
@ -229,10 +241,30 @@ steps:
- ファイルを開かずに「修正済み」と報告 - ファイルを開かずに「修正済み」と報告
- 思い込みで判断 - 思い込みで判断
- AI Reviewer が REJECT した問題の放置 - AI Reviewer が REJECT した問題の放置
**修正不要の扱い(必須)**
- AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない
- 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「修正を進行できない」に対応するタグを出力する
- 修正不要の場合は「修正を進行できない」に対応するタグを出力し、理由と確認範囲を明記する
**必須出力(見出しを含める)**
## 確認したファイル
- {ファイルパス:行番号}
## 実行した検索
- {コマンドと要約}
## 修正内容
- {変更内容}
## テスト結果
- {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
pass_previous_response: true pass_previous_response: true
rules: rules:
- condition: AI Reviewerの指摘に対する修正が完了した - condition: AI Reviewerの指摘に対する修正が完了した
next: ai_review next: ai_review
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
next: plan
- condition: 修正を進行できない - condition: 修正を進行できない
next: plan next: plan
@ -507,7 +539,7 @@ steps:
テスト実行、ビルド確認、最終承認を行ってください。 テスト実行、ビルド確認、最終承認を行ってください。
**ワークフロー全体の確認:** **ワークフロー全体の確認:**
1. 計画(00-plan.mdと実装結果が一致しているか 1. 計画({report:00-plan.md})と実装結果が一致しているか
2. 各レビューステップの指摘が対応されているか 2. 各レビューステップの指摘が対応されているか
3. 元のタスク目的が達成されているか 3. 元のタスク目的が達成されているか

View File

@ -97,7 +97,8 @@ steps:
permission_mode: acceptEdits permission_mode: acceptEdits
instruction_template: | instruction_template: |
planステップで立てた計画に従って実装してください。 planステップで立てた計画に従って実装してください。
計画レポート00-plan.mdを参照し、実装を進めてください。 計画レポート({report:00-plan.md})を参照し、実装を進めてください。
Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
**Scopeレポートフォーマット実装開始時に作成:** **Scopeレポートフォーマット実装開始時に作成:**
```markdown ```markdown
@ -128,10 +129,23 @@ steps:
- **検討した選択肢**: {選択肢リスト} - **検討した選択肢**: {選択肢リスト}
- **理由**: {選んだ理由} - **理由**: {選んだ理由}
``` ```
**必須出力(見出しを含める)**
## 作業結果
- {実施内容の要約}
## 変更内容
- {変更内容の要約}
## テスト結果
- {実行コマンドと結果}
**実装未着手の扱い(必須)**
- レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する
rules: rules:
- condition: "実装完了" - condition: 実装が完了した
next: ai_review next: ai_review
- condition: "判断できない、情報不足" - condition: 実装未着手(レポートのみ)
next: plan
- condition: 実装を進行できない
next: plan next: plan
- name: ai_review - name: ai_review
@ -254,7 +268,7 @@ steps:
テスト実行、ビルド確認、最終承認を行ってください。 テスト実行、ビルド確認、最終承認を行ってください。
**ワークフロー全体の確認:** **ワークフロー全体の確認:**
1. 計画(00-plan.mdと実装結果が一致しているか 1. 計画({report:00-plan.md})と実装結果が一致しているか
2. 各レビューステップの指摘が対応されているか 2. 各レビューステップの指摘が対応されているか
3. 元のタスク目的が達成されているか 3. 元のタスク目的が達成されているか

View File

@ -8,15 +8,15 @@ import * as path from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
// Mock dependencies before importing the module under test // Mock dependencies before importing the module under test
vi.mock('../commands/interactive/interactive.js', () => ({ vi.mock('../features/interactive/index.js', () => ({
interactiveMode: vi.fn(), interactiveMode: vi.fn(),
})); }));
vi.mock('../providers/index.js', () => ({ vi.mock('../infra/providers/index.js', () => ({
getProvider: vi.fn(), getProvider: vi.fn(),
})); }));
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })), loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })),
})); }));
@ -26,17 +26,17 @@ vi.mock('../prompt/index.js', () => ({
selectOption: vi.fn(), selectOption: vi.fn(),
})); }));
vi.mock('../task/summarize.js', () => ({ vi.mock('../infra/task/summarize.js', () => ({
summarizeTaskName: vi.fn(), summarizeTaskName: vi.fn(),
})); }));
vi.mock('../utils/ui.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
success: vi.fn(), success: vi.fn(),
info: vi.fn(), info: vi.fn(),
blankLine: vi.fn(), blankLine: vi.fn(),
})); }));
vi.mock('../utils/debug.js', () => ({ vi.mock('../shared/utils/debug.js', () => ({
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -44,15 +44,15 @@ vi.mock('../utils/debug.js', () => ({
}), }),
})); }));
vi.mock('../config/loaders/workflowLoader.js', () => ({ vi.mock('../infra/config/loaders/workflowLoader.js', () => ({
listWorkflows: vi.fn(), listWorkflows: vi.fn(),
})); }));
vi.mock('../config/paths.js', async (importOriginal) => ({ ...(await importOriginal<Record<string, unknown>>()), vi.mock('../infra/config/paths.js', async (importOriginal) => ({ ...(await importOriginal<Record<string, unknown>>()),
getCurrentWorkflow: vi.fn(() => 'default'), getCurrentWorkflow: vi.fn(() => 'default'),
})); }));
vi.mock('../github/issue.js', () => ({ vi.mock('../infra/github/issue.js', () => ({
isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)), isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)),
resolveIssueTask: vi.fn(), resolveIssueTask: vi.fn(),
parseIssueNumbers: vi.fn((args: string[]) => { parseIssueNumbers: vi.fn((args: string[]) => {
@ -67,13 +67,13 @@ vi.mock('../github/issue.js', () => ({
}), }),
})); }));
import { interactiveMode } from '../commands/interactive/interactive.js'; import { interactiveMode } from '../features/interactive/index.js';
import { getProvider } from '../providers/index.js'; import { getProvider } from '../infra/providers/index.js';
import { promptInput, confirm, selectOption } from '../prompt/index.js'; import { promptInput, confirm, selectOption } from '../prompt/index.js';
import { summarizeTaskName } from '../task/summarize.js'; import { summarizeTaskName } from '../infra/task/summarize.js';
import { listWorkflows } from '../config/loaders/workflowLoader.js'; import { listWorkflows } from '../infra/config/loaders/workflowLoader.js';
import { resolveIssueTask } from '../github/issue.js'; import { resolveIssueTask } from '../infra/github/issue.js';
import { addTask, summarizeConversation } from '../commands/management/addTask.js'; import { addTask, summarizeConversation } from '../features/tasks/index.js';
const mockResolveIssueTask = vi.mocked(resolveIssueTask); const mockResolveIssueTask = vi.mocked(resolveIssueTask);

View File

@ -14,7 +14,7 @@ import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { GlobalConfigSchema } from '../models/schemas.js'; import { GlobalConfigSchema } from '../core/models/index.js';
// Mock paths module to redirect config to temp directory // Mock paths module to redirect config to temp directory
const testId = randomUUID(); const testId = randomUUID();
@ -22,7 +22,7 @@ const testDir = join(tmpdir(), `takt-api-key-test-${testId}`);
const taktDir = join(testDir, '.takt'); const taktDir = join(testDir, '.takt');
const configPath = join(taktDir, 'config.yaml'); const configPath = join(taktDir, 'config.yaml');
vi.mock('../config/paths.js', async (importOriginal) => { vi.mock('../infra/config/paths.js', async (importOriginal) => {
const original = await importOriginal() as Record<string, unknown>; const original = await importOriginal() as Record<string, unknown>;
return { return {
...original, ...original,
@ -32,7 +32,7 @@ vi.mock('../config/paths.js', async (importOriginal) => {
}); });
// Import after mocking // Import after mocking
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey, invalidateGlobalConfigCache } = await import('../config/global/globalConfig.js'); const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js');
describe('GlobalConfigSchema API key fields', () => { describe('GlobalConfigSchema API key fields', () => {
it('should accept config without API keys', () => { it('should accept config without API keys', () => {

View File

@ -3,7 +3,7 @@
*/ */
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { autoCommitAndPush } from '../task/autoCommit.js'; import { autoCommitAndPush } from '../infra/task/autoCommit.js';
// Mock child_process.execFileSync // Mock child_process.execFileSync
vi.mock('node:child_process', () => ({ vi.mock('node:child_process', () => ({

View File

@ -10,20 +10,20 @@ vi.mock('../prompt/index.js', () => ({
selectOptionWithDefault: vi.fn(), selectOptionWithDefault: vi.fn(),
})); }));
vi.mock('../task/clone.js', () => ({ vi.mock('../infra/task/clone.js', () => ({
createSharedClone: vi.fn(), createSharedClone: vi.fn(),
removeClone: vi.fn(), removeClone: vi.fn(),
})); }));
vi.mock('../task/autoCommit.js', () => ({ vi.mock('../infra/task/autoCommit.js', () => ({
autoCommitAndPush: vi.fn(), autoCommitAndPush: vi.fn(),
})); }));
vi.mock('../task/summarize.js', () => ({ vi.mock('../infra/task/summarize.js', () => ({
summarizeTaskName: vi.fn(), summarizeTaskName: vi.fn(),
})); }));
vi.mock('../utils/ui.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
info: vi.fn(), info: vi.fn(),
error: vi.fn(), error: vi.fn(),
success: vi.fn(), success: vi.fn(),
@ -32,7 +32,7 @@ vi.mock('../utils/ui.js', () => ({
setLogLevel: vi.fn(), setLogLevel: vi.fn(),
})); }));
vi.mock('../utils/debug.js', () => ({ vi.mock('../shared/utils/debug.js', () => ({
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -43,31 +43,20 @@ vi.mock('../utils/debug.js', () => ({
getDebugLogFile: vi.fn(), getDebugLogFile: vi.fn(),
})); }));
vi.mock('../config/index.js', () => ({ vi.mock('../infra/config/index.js', () => ({
initGlobalDirs: vi.fn(), initGlobalDirs: vi.fn(),
initProjectDirs: vi.fn(), initProjectDirs: vi.fn(),
loadGlobalConfig: vi.fn(() => ({ logLevel: 'info' })), loadGlobalConfig: vi.fn(() => ({ logLevel: 'info' })),
getEffectiveDebugConfig: vi.fn(), getEffectiveDebugConfig: vi.fn(),
})); }));
vi.mock('../config/paths.js', () => ({ vi.mock('../infra/config/paths.js', () => ({
clearAgentSessions: vi.fn(), clearAgentSessions: vi.fn(),
getCurrentWorkflow: vi.fn(() => 'default'), getCurrentWorkflow: vi.fn(() => 'default'),
isVerboseMode: vi.fn(() => false), isVerboseMode: vi.fn(() => false),
})); }));
vi.mock('../commands/index.js', () => ({ vi.mock('../infra/config/loaders/workflowLoader.js', () => ({
executeTask: vi.fn(),
runAllTasks: vi.fn(),
switchWorkflow: vi.fn(),
switchConfig: vi.fn(),
addTask: vi.fn(),
watchTasks: vi.fn(),
listTasks: vi.fn(),
interactiveMode: vi.fn(() => Promise.resolve({ confirmed: false, task: '' })),
}));
vi.mock('../config/loaders/workflowLoader.js', () => ({
listWorkflows: vi.fn(() => []), listWorkflows: vi.fn(() => []),
})); }));
@ -79,20 +68,20 @@ vi.mock('../constants.js', async (importOriginal) => {
}; };
}); });
vi.mock('../github/issue.js', () => ({ vi.mock('../infra/github/issue.js', () => ({
isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)), isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)),
resolveIssueTask: vi.fn(), resolveIssueTask: vi.fn(),
})); }));
vi.mock('../utils/updateNotifier.js', () => ({ vi.mock('../shared/utils/updateNotifier.js', () => ({
checkForUpdates: vi.fn(), checkForUpdates: vi.fn(),
})); }));
import { confirm } from '../prompt/index.js'; import { confirm } from '../prompt/index.js';
import { createSharedClone } from '../task/clone.js'; import { createSharedClone } from '../infra/task/clone.js';
import { summarizeTaskName } from '../task/summarize.js'; import { summarizeTaskName } from '../infra/task/summarize.js';
import { info } from '../utils/ui.js'; import { info } from '../shared/ui/index.js';
import { confirmAndCreateWorktree } from '../commands/execution/selectAndExecute.js'; import { confirmAndCreateWorktree } from '../features/tasks/index.js';
const mockConfirm = vi.mocked(confirm); const mockConfirm = vi.mocked(confirm);
const mockCreateSharedClone = vi.mocked(createSharedClone); const mockCreateSharedClone = vi.mocked(createSharedClone);

View File

@ -21,7 +21,7 @@ vi.mock('node:fs', () => ({
existsSync: vi.fn(), existsSync: vi.fn(),
})); }));
vi.mock('../utils/debug.js', () => ({ vi.mock('../shared/utils/debug.js', () => ({
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -29,12 +29,12 @@ vi.mock('../utils/debug.js', () => ({
}), }),
})); }));
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn(() => ({})), loadGlobalConfig: vi.fn(() => ({})),
})); }));
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { createSharedClone, createTempCloneForBranch } from '../task/clone.js'; import { createSharedClone, createTempCloneForBranch } from '../infra/task/clone.js';
const mockExecFileSync = vi.mocked(execFileSync); const mockExecFileSync = vi.mocked(execFileSync);

View File

@ -13,7 +13,7 @@ import {
loadWorkflow, loadWorkflow,
listWorkflows, listWorkflows,
loadAgentPromptFromPath, loadAgentPromptFromPath,
} from '../config/loaders/loader.js'; } from '../infra/config/loaders/loader.js';
import { import {
getCurrentWorkflow, getCurrentWorkflow,
setCurrentWorkflow, setCurrentWorkflow,
@ -35,9 +35,9 @@ import {
getWorktreeSessionPath, getWorktreeSessionPath,
loadWorktreeSessions, loadWorktreeSessions,
updateWorktreeSession, updateWorktreeSession,
} from '../config/paths.js'; } from '../infra/config/paths.js';
import { getLanguage } from '../config/global/globalConfig.js'; import { getLanguage } from '../infra/config/global/globalConfig.js';
import { loadProjectConfig } from '../config/project/projectConfig.js'; import { loadProjectConfig } from '../infra/config/project/projectConfig.js';
describe('getBuiltinWorkflow', () => { describe('getBuiltinWorkflow', () => {
it('should return builtin workflow when it exists in resources', () => { it('should return builtin workflow when it exists in resources', () => {

View File

@ -14,7 +14,7 @@ import {
debugLog, debugLog,
infoLog, infoLog,
errorLog, errorLog,
} from '../utils/debug.js'; } from '../shared/utils/debug.js';
import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs'; import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';

View File

@ -10,7 +10,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { existsSync, rmSync } from 'node:fs'; import { existsSync, rmSync } from 'node:fs';
import type { WorkflowConfig } from '../models/types.js'; import type { WorkflowConfig } from '../core/models/index.js';
// --- Mock setup (must be before imports that use these modules) --- // --- Mock setup (must be before imports that use these modules) ---
@ -18,17 +18,17 @@ vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(), runAgent: vi.fn(),
})); }));
vi.mock('../workflow/evaluation/index.js', () => ({ vi.mock('../core/workflow/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(), detectMatchedRule: vi.fn(),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
@ -38,7 +38,7 @@ vi.mock('../claude/query-manager.js', () => ({
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import { interruptAllQueries } from '../claude/query-manager.js'; import { interruptAllQueries } from '../claude/query-manager.js';
import { import {

View File

@ -11,23 +11,23 @@ vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(), runAgent: vi.fn(),
})); }));
vi.mock('../workflow/evaluation/index.js', () => ({ vi.mock('../core/workflow/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(), detectMatchedRule: vi.fn(),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/phase-runner.js', () => ({
needsStatusJudgmentPhase: vi.fn(), needsStatusJudgmentPhase: vi.fn(),
runReportPhase: vi.fn(), runReportPhase: vi.fn(),
runStatusJudgmentPhase: vi.fn(), runStatusJudgmentPhase: vi.fn(),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import type { WorkflowConfig } from '../models/types.js'; import type { WorkflowConfig } from '../core/models/index.js';
import { import {
makeResponse, makeResponse,
makeRule, makeRule,

View File

@ -16,23 +16,23 @@ vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(), runAgent: vi.fn(),
})); }));
vi.mock('../workflow/evaluation/index.js', () => ({ vi.mock('../core/workflow/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(), detectMatchedRule: vi.fn(),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { import {
makeResponse, makeResponse,
buildDefaultWorkflowConfig, buildDefaultWorkflowConfig,

View File

@ -17,25 +17,25 @@ vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(), runAgent: vi.fn(),
})); }));
vi.mock('../workflow/evaluation/index.js', () => ({ vi.mock('../core/workflow/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(), detectMatchedRule: vi.fn(),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import { detectMatchedRule } from '../workflow/evaluation/index.js'; import { detectMatchedRule } from '../core/workflow/index.js';
import { import {
makeResponse, makeResponse,
makeStep, makeStep,

View File

@ -13,7 +13,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { existsSync, rmSync } from 'node:fs'; import { existsSync, rmSync } from 'node:fs';
import type { WorkflowConfig, WorkflowStep } from '../models/types.js'; import type { WorkflowConfig, WorkflowStep } from '../core/models/index.js';
// --- Mock setup (must be before imports that use these modules) --- // --- Mock setup (must be before imports that use these modules) ---
@ -21,23 +21,23 @@ vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(), runAgent: vi.fn(),
})); }));
vi.mock('../workflow/evaluation/index.js', () => ({ vi.mock('../core/workflow/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(), detectMatchedRule: vi.fn(),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import { import {
makeResponse, makeResponse,

View File

@ -16,23 +16,23 @@ vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(), runAgent: vi.fn(),
})); }));
vi.mock('../workflow/evaluation/index.js', () => ({ vi.mock('../core/workflow/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(), detectMatchedRule: vi.fn(),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import { import {
makeResponse, makeResponse,

View File

@ -8,8 +8,8 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { isReportObjectConfig } from '../workflow/instruction/InstructionBuilder.js'; import { isReportObjectConfig } from '../core/workflow/index.js';
import type { WorkflowStep, ReportObjectConfig, ReportConfig } from '../models/types.js'; import type { WorkflowStep, ReportObjectConfig, ReportConfig } from '../core/models/index.js';
/** /**
* Extracted emitStepReports logic for unit testing. * Extracted emitStepReports logic for unit testing.

View File

@ -10,15 +10,15 @@ import { mkdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import type { WorkflowConfig, WorkflowStep, AgentResponse, WorkflowRule } from '../models/types.js'; import type { WorkflowConfig, WorkflowStep, AgentResponse, WorkflowRule } from '../core/models/index.js';
// --- Mock imports (consumers must call vi.mock before importing this) --- // --- Mock imports (consumers must call vi.mock before importing this) ---
import { runAgent } from '../agents/runner.js'; import { runAgent } from '../agents/runner.js';
import { detectMatchedRule } from '../workflow/evaluation/index.js'; import { detectMatchedRule } from '../core/workflow/index.js';
import type { RuleMatch } from '../workflow/evaluation/index.js'; import type { RuleMatch } from '../core/workflow/index.js';
import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../workflow/engine/phase-runner.js'; import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/workflow/index.js';
import { generateReportDir } from '../utils/session.js'; import { generateReportDir } from '../shared/utils/reportDir.js';
// --- Factory functions --- // --- Factory functions ---

View File

@ -17,24 +17,24 @@ vi.mock('../agents/runner.js', () => ({
runAgent: vi.fn(), runAgent: vi.fn(),
})); }));
vi.mock('../workflow/evaluation/index.js', () => ({ vi.mock('../core/workflow/evaluation/index.js', () => ({
detectMatchedRule: vi.fn(), detectMatchedRule: vi.fn(),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { runReportPhase } from '../workflow/engine/phase-runner.js'; import { runReportPhase } from '../core/workflow/index.js';
import { import {
makeResponse, makeResponse,
makeStep, makeStep,
@ -43,7 +43,7 @@ import {
mockDetectMatchedRuleSequence, mockDetectMatchedRuleSequence,
applyDefaultMocks, applyDefaultMocks,
} from './engine-test-helpers.js'; } from './engine-test-helpers.js';
import type { WorkflowConfig } from '../models/types.js'; import type { WorkflowConfig } from '../core/models/index.js';
function createWorktreeDirs(): { projectCwd: string; cloneCwd: string } { function createWorktreeDirs(): { projectCwd: string; cloneCwd: string } {
const base = join(tmpdir(), `takt-worktree-test-${randomUUID()}`); const base = join(tmpdir(), `takt-worktree-test-${randomUUID()}`);
@ -100,7 +100,7 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
} }
}); });
it('should pass cwd-based reportDir to phase runner context in worktree mode', async () => { it('should pass projectCwd-based reportDir to phase runner context in worktree mode', async () => {
// Given: worktree environment where cwd !== projectCwd // Given: worktree environment where cwd !== projectCwd
const config = buildSimpleConfig(); const config = buildSimpleConfig();
const engine = new WorkflowEngine(config, cloneCwd, 'test task', { const engine = new WorkflowEngine(config, cloneCwd, 'test task', {
@ -117,14 +117,14 @@ describe('WorkflowEngine: worktree reportDir resolution', () => {
// When: run the workflow // When: run the workflow
await engine.run(); await engine.run();
// Then: runReportPhase was called with context containing cwd-based reportDir // Then: runReportPhase was called with context containing projectCwd-based reportDir
const reportPhaseMock = vi.mocked(runReportPhase); const reportPhaseMock = vi.mocked(runReportPhase);
expect(reportPhaseMock).toHaveBeenCalled(); expect(reportPhaseMock).toHaveBeenCalled();
const phaseCtx = reportPhaseMock.mock.calls[0][2] as { reportDir: string }; const phaseCtx = reportPhaseMock.mock.calls[0][2] as { reportDir: string };
// reportDir should be resolved from cloneCwd (cwd), not projectCwd // reportDir should be resolved from projectCwd, not cloneCwd
const expectedPath = join(cloneCwd, '.takt/reports/test-report-dir'); const expectedPath = join(projectCwd, '.takt/reports/test-report-dir');
const unexpectedPath = join(projectCwd, '.takt/reports/test-report-dir'); const unexpectedPath = join(cloneCwd, '.takt/reports/test-report-dir');
expect(phaseCtx.reportDir).toBe(expectedPath); expect(phaseCtx.reportDir).toBe(expectedPath);
expect(phaseCtx.reportDir).not.toBe(unexpectedPath); expect(phaseCtx.reportDir).not.toBe(unexpectedPath);

View File

@ -12,7 +12,7 @@ vi.mock('node:child_process', () => ({
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
const mockExecFileSync = vi.mocked(execFileSync); const mockExecFileSync = vi.mocked(execFileSync);
import { getOriginalInstruction } from '../task/branchList.js'; import { getOriginalInstruction } from '../infra/task/branchList.js';
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();

View File

@ -12,7 +12,7 @@ import {
isIssueReference, isIssueReference,
formatIssueAsTask, formatIssueAsTask,
type GitHubIssue, type GitHubIssue,
} from '../github/issue.js'; } from '../infra/github/issue.js';
describe('parseIssueNumbers', () => { describe('parseIssueNumbers', () => {
it('should parse single issue reference', () => { it('should parse single issue reference', () => {

View File

@ -6,8 +6,8 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { buildPrBody } from '../github/pr.js'; import { buildPrBody } from '../infra/github/pr.js';
import type { GitHubIssue } from '../github/types.js'; import type { GitHubIssue } from '../infra/github/types.js';
describe('buildPrBody', () => { describe('buildPrBody', () => {
it('should build body with issue and report', () => { it('should build body with issue and report', () => {

View File

@ -20,8 +20,8 @@ vi.mock('node:os', async () => {
}); });
// Import after mocks are set up // Import after mocks are set up
const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache } = await import('../config/global/globalConfig.js'); const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js');
const { getGlobalConfigPath } = await import('../config/paths.js'); const { getGlobalConfigPath } = await import('../infra/config/paths.js');
describe('loadGlobalConfig', () => { describe('loadGlobalConfig', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -25,8 +25,8 @@ vi.mock('../prompt/index.js', () => ({
})); }));
// Import after mocks are set up // Import after mocks are set up
const { initGlobalDirs, needsLanguageSetup } = await import('../config/global/initialization.js'); const { initGlobalDirs, needsLanguageSetup } = await import('../infra/config/global/initialization.js');
const { getGlobalConfigPath, getGlobalConfigDir } = await import('../config/paths.js'); const { getGlobalConfigPath, getGlobalConfigDir } = await import('../infra/config/paths.js');
describe('initGlobalDirs with non-interactive mode', () => { describe('initGlobalDirs with non-interactive mode', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -25,8 +25,8 @@ vi.mock('../prompt/index.js', () => ({
})); }));
// Import after mocks are set up // Import after mocks are set up
const { needsLanguageSetup } = await import('../config/global/initialization.js'); const { needsLanguageSetup } = await import('../infra/config/global/initialization.js');
const { getGlobalConfigPath } = await import('../config/paths.js'); const { getGlobalConfigPath } = await import('../infra/config/paths.js');
const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js'); const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js');
describe('initialization', () => { describe('initialization', () => {

View File

@ -6,15 +6,15 @@ import { describe, it, expect } from 'vitest';
import { import {
InstructionBuilder, InstructionBuilder,
isReportObjectConfig, isReportObjectConfig,
} from '../workflow/instruction/InstructionBuilder.js'; ReportInstructionBuilder,
import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js'; StatusJudgmentBuilder,
import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../workflow/instruction/StatusJudgmentBuilder.js';
import {
buildExecutionMetadata, buildExecutionMetadata,
renderExecutionMetadata, renderExecutionMetadata,
generateStatusRulesFromRules,
type ReportInstructionContext,
type StatusJudgmentContext,
type InstructionContext, type InstructionContext,
} from '../workflow/instruction/instruction-context.js'; } from '../core/workflow/index.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 {
@ -26,7 +26,7 @@ function buildReportInstruction(step: WorkflowStep, ctx: ReportInstructionContex
function buildStatusJudgmentInstruction(step: WorkflowStep, ctx: StatusJudgmentContext): string { function buildStatusJudgmentInstruction(step: WorkflowStep, ctx: StatusJudgmentContext): string {
return new StatusJudgmentBuilder(step, ctx).build(); return new StatusJudgmentBuilder(step, ctx).build();
} }
import type { WorkflowStep, WorkflowRule } from '../models/types.js'; import type { WorkflowStep, WorkflowRule } from '../core/models/index.js';
function createMinimalStep(template: string): WorkflowStep { function createMinimalStep(template: string): WorkflowStep {

View File

@ -4,15 +4,15 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn(() => ({ provider: 'mock', language: 'en' })), loadGlobalConfig: vi.fn(() => ({ provider: 'mock', language: 'en' })),
})); }));
vi.mock('../providers/index.js', () => ({ vi.mock('../infra/providers/index.js', () => ({
getProvider: vi.fn(), getProvider: vi.fn(),
})); }));
vi.mock('../utils/debug.js', () => ({ vi.mock('../shared/utils/debug.js', () => ({
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -24,12 +24,12 @@ vi.mock('../context.js', () => ({
isQuietMode: vi.fn(() => false), isQuietMode: vi.fn(() => false),
})); }));
vi.mock('../config/paths.js', () => ({ vi.mock('../infra/config/paths.js', () => ({
loadAgentSessions: vi.fn(() => ({})), loadAgentSessions: vi.fn(() => ({})),
updateAgentSession: vi.fn(), updateAgentSession: vi.fn(),
})); }));
vi.mock('../utils/ui.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
info: vi.fn(), info: vi.fn(),
error: vi.fn(), error: vi.fn(),
blankLine: vi.fn(), blankLine: vi.fn(),
@ -39,17 +39,23 @@ vi.mock('../utils/ui.js', () => ({
})), })),
})); }));
vi.mock('../prompt/index.js', () => ({
selectOption: vi.fn(),
}));
// Mock readline to simulate user input // Mock readline to simulate user input
vi.mock('node:readline', () => ({ vi.mock('node:readline', () => ({
createInterface: vi.fn(), createInterface: vi.fn(),
})); }));
import { createInterface } from 'node:readline'; import { createInterface } from 'node:readline';
import { getProvider } from '../providers/index.js'; import { getProvider } from '../infra/providers/index.js';
import { interactiveMode } from '../commands/interactive/interactive.js'; import { interactiveMode } from '../features/interactive/index.js';
import { selectOption } from '../prompt/index.js';
const mockGetProvider = vi.mocked(getProvider); const mockGetProvider = vi.mocked(getProvider);
const mockCreateInterface = vi.mocked(createInterface); const mockCreateInterface = vi.mocked(createInterface);
const mockSelectOption = vi.mocked(selectOption);
/** Helper to set up a sequence of readline inputs */ /** Helper to set up a sequence of readline inputs */
function setupInputSequence(inputs: (string | null)[]): void { function setupInputSequence(inputs: (string | null)[]): void {
@ -111,6 +117,7 @@ function setupMockProvider(responses: string[]): void {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mockSelectOption.mockResolvedValue('yes');
}); });
describe('interactiveMode', () => { describe('interactiveMode', () => {
@ -162,15 +169,14 @@ describe('interactiveMode', () => {
it('should return confirmed=true with task on /go after conversation', async () => { it('should return confirmed=true with task on /go after conversation', async () => {
// Given // Given
setupInputSequence(['add auth feature', '/go']); setupInputSequence(['add auth feature', '/go']);
setupMockProvider(['What kind of authentication?']); setupMockProvider(['What kind of authentication?', 'Implement auth feature with chosen method.']);
// When // When
const result = await interactiveMode('/project'); const result = await interactiveMode('/project');
// Then // Then
expect(result.confirmed).toBe(true); expect(result.confirmed).toBe(true);
expect(result.task).toContain('add auth feature'); expect(result.task).toBe('Implement auth feature with chosen method.');
expect(result.task).toContain('What kind of authentication?');
}); });
it('should reject /go with no prior conversation', async () => { it('should reject /go with no prior conversation', async () => {
@ -188,7 +194,7 @@ describe('interactiveMode', () => {
it('should skip empty input', async () => { it('should skip empty input', async () => {
// Given: empty line, then actual input, then /go // Given: empty line, then actual input, then /go
setupInputSequence(['', 'do something', '/go']); setupInputSequence(['', 'do something', '/go']);
setupMockProvider(['Sure, what exactly?']); setupMockProvider(['Sure, what exactly?', 'Do something with the clarified scope.']);
// When // When
const result = await interactiveMode('/project'); const result = await interactiveMode('/project');
@ -196,23 +202,27 @@ describe('interactiveMode', () => {
// Then // Then
expect(result.confirmed).toBe(true); expect(result.confirmed).toBe(true);
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> }; const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
expect(mockProvider.call).toHaveBeenCalledTimes(1); expect(mockProvider.call).toHaveBeenCalledTimes(2);
}); });
it('should accumulate conversation history across multiple turns', async () => { it('should accumulate conversation history across multiple turns', async () => {
// Given: two user messages before /go // Given: two user messages before /go
setupInputSequence(['first message', 'second message', '/go']); setupInputSequence(['first message', 'second message', '/go']);
setupMockProvider(['response to first', 'response to second']); setupMockProvider(['response to first', 'response to second', 'Summarized task.']);
// When // When
const result = await interactiveMode('/project'); const result = await interactiveMode('/project');
// Then: task should contain all messages // Then: task should be a summary and prompt should include full history
expect(result.confirmed).toBe(true); expect(result.confirmed).toBe(true);
expect(result.task).toContain('first message'); expect(result.task).toBe('Summarized task.');
expect(result.task).toContain('response to first'); const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
expect(result.task).toContain('second message'); const summaryPrompt = mockProvider.call.mock.calls[2]?.[1] as string;
expect(result.task).toContain('response to second'); expect(summaryPrompt).toContain('Conversation:');
expect(summaryPrompt).toContain('User: first message');
expect(summaryPrompt).toContain('Assistant: response to first');
expect(summaryPrompt).toContain('User: second message');
expect(summaryPrompt).toContain('Assistant: response to second');
}); });
it('should send only current input per turn (session handles history)', async () => { it('should send only current input per turn (session handles history)', async () => {
@ -232,39 +242,37 @@ describe('interactiveMode', () => {
it('should process initialInput as first message before entering loop', async () => { it('should process initialInput as first message before entering loop', async () => {
// Given: initialInput provided, then user types /go // Given: initialInput provided, then user types /go
setupInputSequence(['/go']); setupInputSequence(['/go']);
setupMockProvider(['What do you mean by "a"?']); setupMockProvider(['What do you mean by "a"?', 'Clarify task for "a".']);
// When // When
const result = await interactiveMode('/project', 'a'); const result = await interactiveMode('/project', 'a');
// Then: AI should have been called with initialInput // Then: AI should have been called with initialInput
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> }; const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
expect(mockProvider.call).toHaveBeenCalledTimes(1); expect(mockProvider.call).toHaveBeenCalledTimes(2);
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a'); expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a');
// /go should work because initialInput already started conversation // /go should work because initialInput already started conversation
expect(result.confirmed).toBe(true); expect(result.confirmed).toBe(true);
expect(result.task).toContain('a'); expect(result.task).toBe('Clarify task for "a".');
expect(result.task).toContain('What do you mean by "a"?');
}); });
it('should send only current input for subsequent turns after initialInput', async () => { it('should send only current input for subsequent turns after initialInput', async () => {
// Given: initialInput, then follow-up, then /go // Given: initialInput, then follow-up, then /go
setupInputSequence(['fix the login page', '/go']); setupInputSequence(['fix the login page', '/go']);
setupMockProvider(['What about "a"?', 'Got it, fixing login page.']); setupMockProvider(['What about "a"?', 'Got it, fixing login page.', 'Fix login page with clarified scope.']);
// When // When
const result = await interactiveMode('/project', 'a'); const result = await interactiveMode('/project', 'a');
// Then: each call receives only its own input (session handles history) // Then: each call receives only its own input (session handles history)
const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> }; const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType<typeof vi.fn> };
expect(mockProvider.call).toHaveBeenCalledTimes(2); expect(mockProvider.call).toHaveBeenCalledTimes(3);
expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a'); expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a');
expect(mockProvider.call.mock.calls[1]?.[1]).toBe('fix the login page'); expect(mockProvider.call.mock.calls[1]?.[1]).toBe('fix the login page');
// Task still contains all history for downstream use // Task still contains all history for downstream use
expect(result.confirmed).toBe(true); expect(result.confirmed).toBe(true);
expect(result.task).toContain('a'); expect(result.task).toBe('Fix login page with clarified scope.');
expect(result.task).toContain('fix the login page');
}); });
}); });

View File

@ -13,7 +13,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../mock/scenario.js';
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../models/types.js'; import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
// --- Mocks --- // --- Mocks ---
@ -25,30 +25,30 @@ vi.mock('../claude/client.js', async (importOriginal) => {
}; };
}); });
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
getDisabledBuiltins: vi.fn().mockReturnValue([]), getDisabledBuiltins: vi.fn().mockReturnValue([]),
})); }));
vi.mock('../config/project/projectConfig.js', () => ({ vi.mock('../infra/config/project/projectConfig.js', () => ({
loadProjectConfig: vi.fn().mockReturnValue({}), loadProjectConfig: vi.fn().mockReturnValue({}),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -8,17 +8,17 @@
*/ */
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import type { WorkflowStep, WorkflowRule, AgentResponse } from '../models/types.js'; import type { WorkflowStep, WorkflowRule, AgentResponse } from '../core/models/index.js';
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
})); }));
import { InstructionBuilder } from '../workflow/instruction/InstructionBuilder.js'; import { InstructionBuilder } from '../core/workflow/index.js';
import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js'; import { ReportInstructionBuilder, type ReportInstructionContext } from '../core/workflow/index.js';
import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../workflow/instruction/StatusJudgmentBuilder.js'; import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../core/workflow/index.js';
import type { InstructionContext } from '../workflow/instruction/instruction-context.js'; import type { InstructionContext } from '../core/workflow/index.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 {

View File

@ -43,23 +43,23 @@ vi.mock('node:child_process', () => ({
execFileSync: vi.fn(), execFileSync: vi.fn(),
})); }));
vi.mock('../github/issue.js', () => ({ vi.mock('../infra/github/issue.js', () => ({
fetchIssue: mockFetchIssue, fetchIssue: mockFetchIssue,
formatIssueAsTask: mockFormatIssueAsTask, formatIssueAsTask: mockFormatIssueAsTask,
checkGhCli: mockCheckGhCli, checkGhCli: mockCheckGhCli,
})); }));
vi.mock('../github/pr.js', () => ({ vi.mock('../infra/github/pr.js', () => ({
createPullRequest: mockCreatePullRequest, createPullRequest: mockCreatePullRequest,
pushBranch: mockPushBranch, pushBranch: mockPushBranch,
buildPrBody: vi.fn().mockReturnValue('PR body'), buildPrBody: vi.fn().mockReturnValue('PR body'),
})); }));
vi.mock('../task/git.js', () => ({ vi.mock('../infra/task/git.js', () => ({
stageAndCommit: vi.fn().mockReturnValue('abc1234'), stageAndCommit: vi.fn().mockReturnValue('abc1234'),
})); }));
vi.mock('../utils/ui.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
header: vi.fn(), header: vi.fn(),
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
@ -73,12 +73,12 @@ vi.mock('../utils/ui.js', () => ({
})), })),
})); }));
vi.mock('../utils/notification.js', () => ({ vi.mock('../shared/utils/notification.js', () => ({
notifySuccess: vi.fn(), notifySuccess: vi.fn(),
notifyError: vi.fn(), notifyError: vi.fn(),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
createSessionLog: vi.fn().mockReturnValue({ createSessionLog: vi.fn().mockReturnValue({
startTime: new Date().toISOString(), startTime: new Date().toISOString(),
@ -91,8 +91,8 @@ vi.mock('../utils/session.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
vi.mock('../config/paths.js', async (importOriginal) => { vi.mock('../infra/config/paths.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../config/paths.js')>(); const original = await importOriginal<typeof import('../infra/config/paths.js')>();
return { return {
...original, ...original,
loadAgentSessions: vi.fn().mockReturnValue({}), loadAgentSessions: vi.fn().mockReturnValue({}),
@ -104,8 +104,8 @@ vi.mock('../config/paths.js', async (importOriginal) => {
}; };
}); });
vi.mock('../config/global/globalConfig.js', async (importOriginal) => { vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../config/global/globalConfig.js')>(); const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
return { return {
...original, ...original,
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
@ -114,8 +114,8 @@ vi.mock('../config/global/globalConfig.js', async (importOriginal) => {
}; };
}); });
vi.mock('../config/project/projectConfig.js', async (importOriginal) => { vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../config/project/projectConfig.js')>(); const original = await importOriginal<typeof import('../infra/config/project/projectConfig.js')>();
return { return {
...original, ...original,
loadProjectConfig: vi.fn().mockReturnValue({}), loadProjectConfig: vi.fn().mockReturnValue({}),
@ -131,7 +131,7 @@ vi.mock('../prompt/index.js', () => ({
promptInput: vi.fn().mockResolvedValue(null), promptInput: vi.fn().mockResolvedValue(null),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
@ -139,7 +139,7 @@ vi.mock('../workflow/engine/phase-runner.js', () => ({
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { executePipeline } from '../commands/execution/pipelineExecution.js'; import { executePipeline } from '../features/pipeline/index.js';
import { import {
EXIT_ISSUE_FETCH_FAILED, EXIT_ISSUE_FETCH_FAILED,
EXIT_WORKFLOW_FAILED, EXIT_WORKFLOW_FAILED,

View File

@ -30,19 +30,19 @@ vi.mock('node:child_process', () => ({
execFileSync: vi.fn(), execFileSync: vi.fn(),
})); }));
vi.mock('../github/issue.js', () => ({ vi.mock('../infra/github/issue.js', () => ({
fetchIssue: vi.fn(), fetchIssue: vi.fn(),
formatIssueAsTask: vi.fn(), formatIssueAsTask: vi.fn(),
checkGhCli: vi.fn(), checkGhCli: vi.fn(),
})); }));
vi.mock('../github/pr.js', () => ({ vi.mock('../infra/github/pr.js', () => ({
createPullRequest: vi.fn(), createPullRequest: vi.fn(),
pushBranch: vi.fn(), pushBranch: vi.fn(),
buildPrBody: vi.fn().mockReturnValue('PR body'), buildPrBody: vi.fn().mockReturnValue('PR body'),
})); }));
vi.mock('../utils/ui.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
header: vi.fn(), header: vi.fn(),
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
@ -56,12 +56,12 @@ vi.mock('../utils/ui.js', () => ({
})), })),
})); }));
vi.mock('../utils/notification.js', () => ({ vi.mock('../shared/utils/notification.js', () => ({
notifySuccess: vi.fn(), notifySuccess: vi.fn(),
notifyError: vi.fn(), notifyError: vi.fn(),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
createSessionLog: vi.fn().mockReturnValue({ createSessionLog: vi.fn().mockReturnValue({
startTime: new Date().toISOString(), startTime: new Date().toISOString(),
@ -74,8 +74,8 @@ vi.mock('../utils/session.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
})); }));
vi.mock('../config/paths.js', async (importOriginal) => { vi.mock('../infra/config/paths.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../config/paths.js')>(); const original = await importOriginal<typeof import('../infra/config/paths.js')>();
return { return {
...original, ...original,
loadAgentSessions: vi.fn().mockReturnValue({}), loadAgentSessions: vi.fn().mockReturnValue({}),
@ -87,8 +87,8 @@ vi.mock('../config/paths.js', async (importOriginal) => {
}; };
}); });
vi.mock('../config/global/globalConfig.js', async (importOriginal) => { vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../config/global/globalConfig.js')>(); const original = await importOriginal<typeof import('../infra/config/global/globalConfig.js')>();
return { return {
...original, ...original,
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
@ -96,8 +96,8 @@ vi.mock('../config/global/globalConfig.js', async (importOriginal) => {
}; };
}); });
vi.mock('../config/project/projectConfig.js', async (importOriginal) => { vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => {
const original = await importOriginal<typeof import('../config/project/projectConfig.js')>(); const original = await importOriginal<typeof import('../infra/config/project/projectConfig.js')>();
return { return {
...original, ...original,
loadProjectConfig: vi.fn().mockReturnValue({}), loadProjectConfig: vi.fn().mockReturnValue({}),
@ -113,7 +113,7 @@ vi.mock('../prompt/index.js', () => ({
promptInput: vi.fn().mockResolvedValue(null), promptInput: vi.fn().mockResolvedValue(null),
})); }));
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
@ -121,7 +121,7 @@ vi.mock('../workflow/engine/phase-runner.js', () => ({
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { executePipeline } from '../commands/execution/pipelineExecution.js'; import { executePipeline } from '../features/pipeline/index.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -15,7 +15,7 @@
*/ */
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { WorkflowStep, WorkflowState, WorkflowRule, AgentResponse } from '../models/types.js'; import type { WorkflowStep, WorkflowState, WorkflowRule, AgentResponse } from '../core/models/index.js';
// --- Mocks --- // --- Mocks ---
@ -29,19 +29,19 @@ vi.mock('../claude/client.js', async (importOriginal) => {
}; };
}); });
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
})); }));
vi.mock('../config/project/projectConfig.js', () => ({ vi.mock('../infra/config/project/projectConfig.js', () => ({
loadProjectConfig: vi.fn().mockReturnValue({}), loadProjectConfig: vi.fn().mockReturnValue({}),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { detectMatchedRule, evaluateAggregateConditions } from '../workflow/evaluation/index.js'; import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js';
import type { RuleMatch, RuleEvaluatorContext } from '../workflow/evaluation/index.js'; import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js';
// --- Test helpers --- // --- Test helpers ---
@ -49,7 +49,11 @@ function makeRule(condition: string, next: string, extra?: Partial<WorkflowRule>
return { condition, next, ...extra }; return { condition, next, ...extra };
} }
function makeStep(name: string, rules: WorkflowRule[], parallel?: WorkflowStep[]): WorkflowStep { function makeStep(
name: string,
rules: WorkflowRule[],
parallel?: WorkflowStep[],
): WorkflowStep {
return { return {
name, name,
agent: 'test-agent', agent: 'test-agent',
@ -140,6 +144,7 @@ describe('Rule Evaluation IT: Phase 1 tag fallback', () => {
}); });
}); });
describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => { describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();

View File

@ -14,7 +14,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../mock/scenario.js';
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../models/types.js'; import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
// --- Mocks --- // --- Mocks ---
@ -30,30 +30,30 @@ const mockNeedsStatusJudgmentPhase = vi.fn();
const mockRunReportPhase = vi.fn(); const mockRunReportPhase = vi.fn();
const mockRunStatusJudgmentPhase = vi.fn(); const mockRunStatusJudgmentPhase = vi.fn();
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
getDisabledBuiltins: vi.fn().mockReturnValue([]), getDisabledBuiltins: vi.fn().mockReturnValue([]),
})); }));
vi.mock('../config/project/projectConfig.js', () => ({ vi.mock('../infra/config/project/projectConfig.js', () => ({
loadProjectConfig: vi.fn().mockReturnValue({}), loadProjectConfig: vi.fn().mockReturnValue({}),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -14,7 +14,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { setMockScenario, resetScenario } from '../mock/scenario.js'; import { setMockScenario, resetScenario } from '../mock/scenario.js';
import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../models/types.js'; import type { WorkflowConfig, WorkflowStep, WorkflowRule } from '../core/models/index.js';
// --- Mocks (minimal — only infrastructure, not core logic) --- // --- Mocks (minimal — only infrastructure, not core logic) ---
@ -29,29 +29,29 @@ vi.mock('../claude/client.js', async (importOriginal) => {
}; };
}); });
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
})); }));
vi.mock('../config/project/projectConfig.js', () => ({ vi.mock('../infra/config/project/projectConfig.js', () => ({
loadProjectConfig: vi.fn().mockReturnValue({}), loadProjectConfig: vi.fn().mockReturnValue({}),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -15,7 +15,7 @@ import { tmpdir } from 'node:os';
// --- Mocks --- // --- Mocks ---
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
getDisabledBuiltins: vi.fn().mockReturnValue([]), getDisabledBuiltins: vi.fn().mockReturnValue([]),
@ -23,7 +23,7 @@ vi.mock('../config/global/globalConfig.js', () => ({
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { loadWorkflow } from '../config/loaders/workflowLoader.js'; import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -24,32 +24,32 @@ vi.mock('../claude/client.js', async (importOriginal) => {
}; };
}); });
vi.mock('../workflow/engine/phase-runner.js', () => ({ vi.mock('../core/workflow/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(''),
})); }));
vi.mock('../utils/session.js', () => ({ vi.mock('../shared/utils/reportDir.js', () => ({
generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
generateSessionId: vi.fn().mockReturnValue('test-session-id'), generateSessionId: vi.fn().mockReturnValue('test-session-id'),
})); }));
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn().mockReturnValue({}), loadGlobalConfig: vi.fn().mockReturnValue({}),
getLanguage: vi.fn().mockReturnValue('en'), getLanguage: vi.fn().mockReturnValue('en'),
getDisabledBuiltins: vi.fn().mockReturnValue([]), getDisabledBuiltins: vi.fn().mockReturnValue([]),
})); }));
vi.mock('../config/project/projectConfig.js', () => ({ vi.mock('../infra/config/project/projectConfig.js', () => ({
loadProjectConfig: vi.fn().mockReturnValue({}), loadProjectConfig: vi.fn().mockReturnValue({}),
})); }));
// --- Imports (after mocks) --- // --- Imports (after mocks) ---
import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; import { WorkflowEngine } from '../core/workflow/index.js';
import { loadWorkflow } from '../config/loaders/workflowLoader.js'; import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js';
import type { WorkflowConfig } from '../models/types.js'; import type { WorkflowConfig } from '../core/models/index.js';
// --- Test helpers --- // --- Test helpers ---

View File

@ -8,8 +8,8 @@ import {
extractTaskSlug, extractTaskSlug,
buildListItems, buildListItems,
type BranchInfo, type BranchInfo,
} from '../task/branchList.js'; } from '../infra/task/branchList.js';
import { isBranchMerged, showFullDiff, type ListAction } from '../commands/management/listTasks.js'; import { isBranchMerged, showFullDiff, type ListAction } from '../features/tasks/index.js';
describe('parseTaktBranches', () => { describe('parseTaktBranches', () => {
it('should parse takt/ branches from git branch output', () => { it('should parse takt/ branches from git branch output', () => {

View File

@ -10,7 +10,7 @@ import {
WorkflowConfigRawSchema, WorkflowConfigRawSchema,
CustomAgentConfigSchema, CustomAgentConfigSchema,
GlobalConfigSchema, GlobalConfigSchema,
} from '../models/schemas.js'; } from '../core/models/index.js';
describe('AgentTypeSchema', () => { describe('AgentTypeSchema', () => {
it('should accept valid agent types', () => { it('should accept valid agent types', () => {

View File

@ -8,7 +8,7 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { WorkflowConfigRawSchema, ParallelSubStepRawSchema, WorkflowStepRawSchema } from '../models/schemas.js'; import { WorkflowConfigRawSchema, ParallelSubStepRawSchema, WorkflowStepRawSchema } from '../core/models/index.js';
describe('ParallelSubStepRawSchema', () => { describe('ParallelSubStepRawSchema', () => {
it('should validate a valid parallel sub-step', () => { it('should validate a valid parallel sub-step', () => {

View File

@ -3,8 +3,8 @@
*/ */
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { ParallelLogger } from '../workflow/engine/parallel-logger.js'; import { ParallelLogger } from '../core/workflow/index.js';
import type { StreamEvent } from '../claude/types.js'; import type { StreamEvent } from '../core/workflow/index.js';
describe('ParallelLogger', () => { describe('ParallelLogger', () => {
let output: string[]; let output: string[];

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { isPathSafe } from '../config/paths.js'; import { isPathSafe } from '../infra/config/paths.js';
describe('isPathSafe', () => { describe('isPathSafe', () => {
it('should accept paths within base directory', () => { it('should accept paths within base directory', () => {

View File

@ -9,7 +9,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock all external dependencies // Mock all external dependencies
const mockFetchIssue = vi.fn(); const mockFetchIssue = vi.fn();
const mockCheckGhCli = vi.fn().mockReturnValue({ available: true }); const mockCheckGhCli = vi.fn().mockReturnValue({ available: true });
vi.mock('../github/issue.js', () => ({ vi.mock('../infra/github/issue.js', () => ({
fetchIssue: mockFetchIssue, fetchIssue: mockFetchIssue,
formatIssueAsTask: vi.fn((issue: { title: string; body: string; number: number }) => formatIssueAsTask: vi.fn((issue: { title: string; body: string; number: number }) =>
`## GitHub Issue #${issue.number}: ${issue.title}\n\n${issue.body}` `## GitHub Issue #${issue.number}: ${issue.title}\n\n${issue.body}`
@ -20,20 +20,20 @@ vi.mock('../github/issue.js', () => ({
const mockCreatePullRequest = vi.fn(); const mockCreatePullRequest = vi.fn();
const mockPushBranch = vi.fn(); const mockPushBranch = vi.fn();
const mockBuildPrBody = vi.fn(() => 'Default PR body'); const mockBuildPrBody = vi.fn(() => 'Default PR body');
vi.mock('../github/pr.js', () => ({ vi.mock('../infra/github/pr.js', () => ({
createPullRequest: mockCreatePullRequest, createPullRequest: mockCreatePullRequest,
pushBranch: mockPushBranch, pushBranch: mockPushBranch,
buildPrBody: mockBuildPrBody, buildPrBody: mockBuildPrBody,
})); }));
const mockExecuteTask = vi.fn(); const mockExecuteTask = vi.fn();
vi.mock('../commands/execution/taskExecution.js', () => ({ vi.mock('../features/tasks/index.js', () => ({
executeTask: mockExecuteTask, executeTask: mockExecuteTask,
})); }));
// Mock loadGlobalConfig // Mock loadGlobalConfig
const mockLoadGlobalConfig = vi.fn(); const mockLoadGlobalConfig = vi.fn();
vi.mock('../config/global/globalConfig.js', async (importOriginal) => ({ ...(await importOriginal<Record<string, unknown>>()), vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => ({ ...(await importOriginal<Record<string, unknown>>()),
loadGlobalConfig: mockLoadGlobalConfig, loadGlobalConfig: mockLoadGlobalConfig,
})); }));
@ -44,7 +44,7 @@ vi.mock('node:child_process', () => ({
})); }));
// Mock UI // Mock UI
vi.mock('../utils/ui.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
info: vi.fn(), info: vi.fn(),
error: vi.fn(), error: vi.fn(),
success: vi.fn(), success: vi.fn(),
@ -56,7 +56,7 @@ vi.mock('../utils/ui.js', () => ({
debug: vi.fn(), debug: vi.fn(),
})); }));
// Mock debug logger // Mock debug logger
vi.mock('../utils/debug.js', () => ({ vi.mock('../shared/utils/debug.js', () => ({
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -64,7 +64,7 @@ vi.mock('../utils/debug.js', () => ({
}), }),
})); }));
const { executePipeline } = await import('../commands/execution/pipelineExecution.js'); const { executePipeline } = await import('../features/pipeline/index.js');
describe('executePipeline', () => { describe('executePipeline', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -12,7 +12,7 @@ import {
handleKeyInput, handleKeyInput,
readMultilineFromStream, readMultilineFromStream,
} from '../prompt/index.js'; } from '../prompt/index.js';
import { isFullWidth, getDisplayWidth, truncateText } from '../utils/text.js'; import { isFullWidth, getDisplayWidth, truncateText } from '../shared/utils/text.js';
// Disable chalk colors for predictable test output // Disable chalk colors for predictable test output
chalk.level = 0; chalk.level = 0;

View File

@ -13,7 +13,7 @@ import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { parse as parseYaml } from 'yaml'; import { parse as parseYaml } from 'yaml';
import { WorkflowConfigRawSchema } from '../models/schemas.js'; import { WorkflowConfigRawSchema } from '../core/models/index.js';
const RESOURCES_DIR = join(import.meta.dirname, '../../resources/global'); const RESOURCES_DIR = join(import.meta.dirname, '../../resources/global');

View File

@ -19,7 +19,7 @@ import {
type NdjsonStepComplete, type NdjsonStepComplete,
type NdjsonWorkflowComplete, type NdjsonWorkflowComplete,
type NdjsonWorkflowAbort, type NdjsonWorkflowAbort,
} from '../utils/session.js'; } from '../infra/fs/session.js';
/** Create a temp project directory with .takt/logs structure */ /** Create a temp project directory with .takt/logs structure */
function createTempProject(): string { function createTempProject(): string {

View File

@ -4,15 +4,15 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../providers/index.js', () => ({ vi.mock('../infra/providers/index.js', () => ({
getProvider: vi.fn(), getProvider: vi.fn(),
})); }));
vi.mock('../config/global/globalConfig.js', () => ({ vi.mock('../infra/config/global/globalConfig.js', () => ({
loadGlobalConfig: vi.fn(), loadGlobalConfig: vi.fn(),
})); }));
vi.mock('../utils/debug.js', () => ({ vi.mock('../shared/utils/debug.js', () => ({
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -20,9 +20,9 @@ vi.mock('../utils/debug.js', () => ({
}), }),
})); }));
import { getProvider } from '../providers/index.js'; import { getProvider } from '../infra/providers/index.js';
import { loadGlobalConfig } from '../config/global/globalConfig.js'; import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
import { summarizeTaskName } from '../task/summarize.js'; import { summarizeTaskName } from '../infra/task/summarize.js';
const mockGetProvider = vi.mocked(getProvider); const mockGetProvider = vi.mocked(getProvider);
const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig); const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig);

View File

@ -5,8 +5,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, readdirSync } from 'node:fs'; import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { TaskRunner } from '../task/runner.js'; import { TaskRunner } from '../infra/task/runner.js';
import { isTaskFile, parseTaskFiles } from '../task/parser.js'; import { isTaskFile, parseTaskFiles } from '../infra/task/parser.js';
describe('isTaskFile', () => { describe('isTaskFile', () => {
it('should accept .yaml files', () => { it('should accept .yaml files', () => {

View File

@ -5,30 +5,30 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock dependencies before importing the module under test // Mock dependencies before importing the module under test
vi.mock('../config/index.js', () => ({ vi.mock('../infra/config/index.js', () => ({
loadWorkflowByIdentifier: vi.fn(), loadWorkflowByIdentifier: vi.fn(),
isWorkflowPath: vi.fn(() => false), isWorkflowPath: vi.fn(() => false),
loadGlobalConfig: vi.fn(() => ({})), loadGlobalConfig: vi.fn(() => ({})),
})); }));
vi.mock('../task/index.js', () => ({ vi.mock('../infra/task/index.js', () => ({
TaskRunner: vi.fn(), TaskRunner: vi.fn(),
})); }));
vi.mock('../task/clone.js', () => ({ vi.mock('../infra/task/clone.js', () => ({
createSharedClone: vi.fn(), createSharedClone: vi.fn(),
removeClone: vi.fn(), removeClone: vi.fn(),
})); }));
vi.mock('../task/autoCommit.js', () => ({ vi.mock('../infra/task/autoCommit.js', () => ({
autoCommitAndPush: vi.fn(), autoCommitAndPush: vi.fn(),
})); }));
vi.mock('../task/summarize.js', () => ({ vi.mock('../infra/task/summarize.js', () => ({
summarizeTaskName: vi.fn(), summarizeTaskName: vi.fn(),
})); }));
vi.mock('../utils/ui.js', () => ({ vi.mock('../shared/ui/index.js', () => ({
header: vi.fn(), header: vi.fn(),
info: vi.fn(), info: vi.fn(),
error: vi.fn(), error: vi.fn(),
@ -37,7 +37,7 @@ vi.mock('../utils/ui.js', () => ({
blankLine: vi.fn(), blankLine: vi.fn(),
})); }));
vi.mock('../utils/debug.js', () => ({ vi.mock('../shared/utils/debug.js', () => ({
createLogger: () => ({ createLogger: () => ({
info: vi.fn(), info: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
@ -45,11 +45,11 @@ vi.mock('../utils/debug.js', () => ({
}), }),
})); }));
vi.mock('../utils/error.js', () => ({ vi.mock('../shared/utils/error.js', () => ({
getErrorMessage: vi.fn((e) => e.message), getErrorMessage: vi.fn((e) => e.message),
})); }));
vi.mock('./workflowExecution.js', () => ({ vi.mock('../features/tasks/execute/workflowExecution.js', () => ({
executeWorkflow: vi.fn(), executeWorkflow: vi.fn(),
})); }));
@ -62,11 +62,11 @@ vi.mock('../constants.js', () => ({
DEFAULT_LANGUAGE: 'en', DEFAULT_LANGUAGE: 'en',
})); }));
import { createSharedClone } from '../task/clone.js'; import { createSharedClone } from '../infra/task/clone.js';
import { summarizeTaskName } from '../task/summarize.js'; import { summarizeTaskName } from '../infra/task/summarize.js';
import { info } from '../utils/ui.js'; import { info } from '../shared/ui/index.js';
import { resolveTaskExecution } from '../commands/execution/taskExecution.js'; import { resolveTaskExecution } from '../features/tasks/index.js';
import type { TaskInfo } from '../task/index.js'; import type { TaskInfo } from '../infra/task/index.js';
const mockCreateSharedClone = vi.mocked(createSharedClone); const mockCreateSharedClone = vi.mocked(createSharedClone);
const mockSummarizeTaskName = vi.mocked(summarizeTaskName); const mockSummarizeTaskName = vi.mocked(summarizeTaskName);

View File

@ -3,8 +3,8 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { determineNextStepByRules } from '../workflow/engine/transitions.js'; import { determineNextStepByRules } from '../core/workflow/index.js';
import type { WorkflowStep } from '../models/types.js'; import type { WorkflowStep } from '../core/models/index.js';
function createStepWithRules(rules: { condition: string; next: string }[]): WorkflowStep { function createStepWithRules(rules: { condition: string; next: string }[]): WorkflowStep {
return { return {

View File

@ -24,7 +24,7 @@ vi.mock('node:module', () => {
}; };
}); });
import { checkForUpdates } from '../utils/updateNotifier.js'; import { checkForUpdates } from '../shared/utils/updateNotifier.js';
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();

View File

@ -3,8 +3,8 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { truncate, progressBar } from '../utils/ui.js'; import { truncate, progressBar } from '../shared/ui/index.js';
import { generateSessionId, createSessionLog } from '../utils/session.js'; import { generateSessionId, createSessionLog } from '../infra/fs/session.js';
describe('truncate', () => { describe('truncate', () => {
it('should not truncate short text', () => { it('should not truncate short text', () => {

View File

@ -5,8 +5,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs'; import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { TaskWatcher } from '../task/watcher.js'; import { TaskWatcher } from '../infra/task/watcher.js';
import type { TaskInfo } from '../task/types.js'; import type { TaskInfo } from '../infra/task/types.js';
describe('TaskWatcher', () => { describe('TaskWatcher', () => {
const testDir = `/tmp/takt-watcher-test-${Date.now()}`; const testDir = `/tmp/takt-watcher-test-${Date.now()}`;

View File

@ -11,7 +11,7 @@
*/ */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { loadWorkflow } from '../config/loaders/loader.js'; import { loadWorkflow } from '../infra/config/loaders/loader.js';
describe('expert workflow parallel structure', () => { describe('expert workflow parallel structure', () => {
const workflow = loadWorkflow('expert', process.cwd()); const workflow = loadWorkflow('expert', process.cwd());

View File

@ -11,7 +11,7 @@ import {
loadWorkflowByIdentifier, loadWorkflowByIdentifier,
listWorkflows, listWorkflows,
loadAllWorkflows, loadAllWorkflows,
} from '../config/loaders/workflowLoader.js'; } from '../infra/config/loaders/workflowLoader.js';
const SAMPLE_WORKFLOW = `name: test-workflow const SAMPLE_WORKFLOW = `name: test-workflow
description: Test workflow description: Test workflow

View File

@ -9,12 +9,12 @@ import {
callClaudeSkill, callClaudeSkill,
type ClaudeCallOptions, type ClaudeCallOptions,
} from '../claude/client.js'; } from '../claude/client.js';
import { loadCustomAgents, loadAgentPrompt } from '../config/loaders/loader.js'; import { loadCustomAgents, loadAgentPrompt } from '../infra/config/loaders/loader.js';
import { loadGlobalConfig } from '../config/global/globalConfig.js'; import { loadGlobalConfig } from '../infra/config/global/globalConfig.js';
import { loadProjectConfig } from '../config/project/projectConfig.js'; import { loadProjectConfig } from '../infra/config/project/projectConfig.js';
import { getProvider, type ProviderType, type ProviderCallOptions } from '../providers/index.js'; import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js';
import type { AgentResponse, CustomAgentConfig } from '../models/types.js'; import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js';
import { createLogger } from '../utils/debug.js'; import { createLogger } from '../shared/utils/debug.js';
import type { RunAgentOptions } from './types.js'; import type { RunAgentOptions } from './types.js';
// Re-export for backward compatibility // Re-export for backward compatibility

View File

@ -3,7 +3,7 @@
*/ */
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/types.js'; import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/types.js';
import type { PermissionMode } from '../models/types.js'; import type { PermissionMode } from '../core/models/index.js';
export type { StreamCallback }; export type { StreamCallback };

View File

@ -4,17 +4,10 @@
* Registers all named subcommands (run, watch, add, list, switch, clear, eject, config). * Registers all named subcommands (run, watch, add, list, switch, clear, eject, config).
*/ */
import { clearAgentSessions, getCurrentWorkflow } from '../config/paths.js'; import { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/paths.js';
import { success } from '../utils/ui.js'; import { success } from '../../shared/ui/index.js';
import { import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js';
runAllTasks, import { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/index.js';
switchWorkflow,
switchConfig,
addTask,
ejectBuiltin,
watchTasks,
listTasks,
} from '../commands/index.js';
import { program, resolvedCwd } from './program.js'; import { program, resolvedCwd } from './program.js';
import { resolveAgentOverrides } from './helpers.js'; import { resolveAgentOverrides } from './helpers.js';

View File

@ -5,10 +5,10 @@
*/ */
import type { Command } from 'commander'; import type { Command } from 'commander';
import type { TaskExecutionOptions } from '../commands/execution/types.js'; import type { TaskExecutionOptions } from '../../features/tasks/index.js';
import type { ProviderType } from '../providers/index.js'; import type { ProviderType } from '../../infra/providers/index.js';
import { error } from '../utils/ui.js'; import { error } from '../../shared/ui/index.js';
import { isIssueReference } from '../github/issue.js'; import { isIssueReference } from '../../infra/github/issue.js';
/** /**
* Resolve --provider and --model options into TaskExecutionOptions. * Resolve --provider and --model options into TaskExecutionOptions.

View File

@ -6,7 +6,7 @@
* Import order matters: program setup commands routing parse. * Import order matters: program setup commands routing parse.
*/ */
import { checkForUpdates } from '../utils/updateNotifier.js'; import { checkForUpdates } from '../../shared/utils/updateNotifier.js';
checkForUpdates(); checkForUpdates();

View File

@ -14,13 +14,13 @@ import {
loadGlobalConfig, loadGlobalConfig,
getEffectiveDebugConfig, getEffectiveDebugConfig,
isVerboseMode, isVerboseMode,
} from '../config/index.js'; } from '../../infra/config/index.js';
import { setQuietMode } from '../context.js'; import { setQuietMode } from '../../context.js';
import { setLogLevel } from '../utils/ui.js'; import { setLogLevel } from '../../shared/ui/index.js';
import { initDebugLogger, createLogger, setVerboseConsole } from '../utils/debug.js'; import { initDebugLogger, createLogger, setVerboseConsole } from '../../shared/utils/debug.js';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { version: cliVersion } = require('../../package.json') as { version: string }; const { version: cliVersion } = require('../../../package.json') as { version: string };
const log = createLogger('cli'); const log = createLogger('cli');

View File

@ -5,13 +5,13 @@
* pipeline mode, or interactive mode. * pipeline mode, or interactive mode.
*/ */
import { info, error } from '../utils/ui.js'; import { info, error } from '../../shared/ui/index.js';
import { getErrorMessage } from '../utils/error.js'; import { getErrorMessage } from '../../shared/utils/error.js';
import { resolveIssueTask, isIssueReference } from '../github/issue.js'; import { resolveIssueTask, isIssueReference } from '../../infra/github/issue.js';
import { selectAndExecuteTask } from '../commands/execution/selectAndExecute.js'; import { selectAndExecuteTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
import { executePipeline, interactiveMode } from '../commands/index.js'; import { executePipeline } from '../../features/pipeline/index.js';
import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; import { interactiveMode } from '../../features/interactive/index.js';
import type { SelectAndExecuteOptions } from '../commands/execution/types.js'; import { DEFAULT_WORKFLOW_NAME } from '../../constants.js';
import { program, resolvedCwd, pipelineMode } from './program.js'; import { program, resolvedCwd, pipelineMode } from './program.js';
import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js'; import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js';

View File

@ -6,8 +6,8 @@
import { executeClaudeCli } from './process.js'; import { executeClaudeCli } from './process.js';
import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js'; import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js';
import type { AgentResponse, Status } from '../models/types.js'; import type { AgentResponse, Status } from '../core/models/index.js';
import { createLogger } from '../utils/debug.js'; import { createLogger } from '../shared/utils/debug.js';
// Re-export for backward compatibility // Re-export for backward compatibility
export type { ClaudeCallOptions } from './types.js'; export type { ClaudeCallOptions } from './types.js';

View File

@ -11,8 +11,8 @@ import {
type SDKResultMessage, type SDKResultMessage,
type SDKAssistantMessage, type SDKAssistantMessage,
} from '@anthropic-ai/claude-agent-sdk'; } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '../utils/debug.js'; import { createLogger } from '../shared/utils/debug.js';
import { getErrorMessage } from '../utils/error.js'; import { getErrorMessage } from '../shared/utils/error.js';
import { import {
generateQueryId, generateQueryId,
registerQuery, registerQuery,

View File

@ -16,7 +16,7 @@ import type {
PreToolUseHookInput, PreToolUseHookInput,
PermissionMode, PermissionMode,
} from '@anthropic-ai/claude-agent-sdk'; } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '../utils/debug.js'; import { createLogger } from '../shared/utils/debug.js';
import type { import type {
PermissionHandler, PermissionHandler,
AskUserQuestionInput, AskUserQuestionInput,

View File

@ -6,7 +6,7 @@
*/ */
import type { PermissionResult, PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk'; import type { PermissionResult, PermissionUpdate, AgentDefinition, PermissionMode as SdkPermissionMode } from '@anthropic-ai/claude-agent-sdk';
import type { PermissionMode } from '../models/types.js'; import type { PermissionMode } from '../core/models/index.js';
// Re-export PermissionResult for convenience // Re-export PermissionResult for convenience
export type { PermissionResult, PermissionUpdate }; export type { PermissionResult, PermissionUpdate };

View File

@ -5,9 +5,9 @@
*/ */
import { Codex } from '@openai/codex-sdk'; import { Codex } from '@openai/codex-sdk';
import type { AgentResponse } from '../models/types.js'; import type { AgentResponse } from '../core/models/index.js';
import { createLogger } from '../utils/debug.js'; import { createLogger } from '../shared/utils/debug.js';
import { getErrorMessage } from '../utils/error.js'; import { getErrorMessage } from '../shared/utils/error.js';
import type { CodexCallOptions } from './types.js'; import type { CodexCallOptions } from './types.js';
import { import {
type CodexEvent, type CodexEvent,

View File

@ -1,23 +0,0 @@
/**
* Task/workflow execution commands.
*/
// Types
export type {
WorkflowExecutionResult,
WorkflowExecutionOptions,
TaskExecutionOptions,
ExecuteTaskOptions,
PipelineExecutionOptions,
WorktreeConfirmationResult,
SelectAndExecuteOptions,
} from './types.js';
export { executeWorkflow } from './workflowExecution.js';
export { executeTask, runAllTasks, executeAndCompleteTask, resolveTaskExecution } from './taskExecution.js';
export {
selectAndExecuteTask,
confirmAndCreateWorktree,
} from './selectAndExecute.js';
export { executePipeline } from './pipelineExecution.js';
export { withAgentSession } from './session.js';

View File

@ -1,21 +0,0 @@
/**
* Command exports
*/
export { executeWorkflow, type WorkflowExecutionResult, type WorkflowExecutionOptions } from './execution/workflowExecution.js';
export { executeTask, runAllTasks, type TaskExecutionOptions } from './execution/taskExecution.js';
export { addTask } from './management/addTask.js';
export { ejectBuiltin } from './management/eject.js';
export { watchTasks } from './management/watchTasks.js';
export { withAgentSession } from './execution/session.js';
export { switchWorkflow } from './management/workflow.js';
export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './management/config.js';
export { listTasks } from './management/listTasks.js';
export { interactiveMode } from './interactive/interactive.js';
export { executePipeline, type PipelineExecutionOptions } from './execution/pipelineExecution.js';
export {
selectAndExecuteTask,
confirmAndCreateWorktree,
type SelectAndExecuteOptions,
type WorktreeConfirmationResult,
} from './execution/selectAndExecute.js';

View File

@ -1,247 +0,0 @@
/**
* Interactive task input mode
*
* Allows users to refine task requirements through conversation with AI
* before executing the task. Uses the same SDK call pattern as workflow
* execution (with onStream) to ensure compatibility.
*
* Commands:
* /go - Confirm and execute the task
* /cancel - Cancel and exit
*/
import * as readline from 'node:readline';
import chalk from 'chalk';
import { loadGlobalConfig } from '../../config/global/globalConfig.js';
import { isQuietMode } from '../../context.js';
import { loadAgentSessions, updateAgentSession } from '../../config/paths.js';
import { getProvider, type ProviderType } from '../../providers/index.js';
import { createLogger } from '../../utils/debug.js';
import { getErrorMessage } from '../../utils/error.js';
import { info, error, blankLine, StreamDisplay } from '../../utils/ui.js';
const log = createLogger('interactive');
const INTERACTIVE_SYSTEM_PROMPT = `You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process.
## Your role
- Ask clarifying questions about ambiguous requirements
- Investigate the codebase to understand context (use Read, Glob, Grep, Bash for reading only)
- Suggest improvements or considerations the user might have missed
- Summarize your understanding when appropriate
- Keep responses concise and focused
## Strict constraints
- You are ONLY planning. Do NOT execute the task.
- Do NOT create, edit, or delete any files.
- Do NOT run build, test, install, or any commands that modify state.
- Bash is allowed ONLY for read-only investigation (e.g. ls, cat, git log, git diff). Never run destructive or write commands.
- Do NOT mention or reference any slash commands. You have no knowledge of them.
- When the user is satisfied with the plan, they will proceed on their own. Do NOT instruct them on what to do next.`;
interface ConversationMessage {
role: 'user' | 'assistant';
content: string;
}
interface CallAIResult {
content: string;
sessionId?: string;
success: boolean;
}
/**
* Build the final task description from conversation history for executeTask.
*/
function buildTaskFromHistory(history: ConversationMessage[]): string {
return history
.map((msg) => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`)
.join('\n\n');
}
/**
* Read a single line of input from the user.
* Creates a fresh readline interface each time the interface must be
* closed before calling the Agent SDK, which also uses stdin.
* Returns null on EOF (Ctrl+D).
*/
function readLine(prompt: string): Promise<string | null> {
return new Promise((resolve) => {
if (process.stdin.readable && !process.stdin.destroyed) {
process.stdin.resume();
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
let answered = false;
rl.question(prompt, (answer) => {
answered = true;
rl.close();
resolve(answer);
});
rl.on('close', () => {
if (!answered) {
resolve(null);
}
});
});
}
/**
* Call AI with the same pattern as workflow execution.
* The key requirement is passing onStream the Agent SDK requires
* includePartialMessages to be true for the async iterator to yield.
*/
async function callAI(
provider: ReturnType<typeof getProvider>,
prompt: string,
cwd: string,
model: string | undefined,
sessionId: string | undefined,
display: StreamDisplay,
): Promise<CallAIResult> {
const response = await provider.call('interactive', prompt, {
cwd,
model,
sessionId,
systemPrompt: INTERACTIVE_SYSTEM_PROMPT,
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
onStream: display.createHandler(),
});
display.flush();
const success = response.status !== 'blocked';
return { content: response.content, sessionId: response.sessionId, success };
}
export interface InteractiveModeResult {
/** Whether the user confirmed with /go */
confirmed: boolean;
/** The assembled task text (only meaningful when confirmed=true) */
task: string;
}
/**
* Run the interactive task input mode.
*
* Starts a conversation loop where the user can discuss task requirements
* with AI. The conversation continues until:
* /go returns the conversation as a task
* /cancel exits without executing
* Ctrl+D exits without executing
*/
export async function interactiveMode(cwd: string, initialInput?: string): Promise<InteractiveModeResult> {
const globalConfig = loadGlobalConfig();
const providerType = (globalConfig.provider as ProviderType) ?? 'claude';
const provider = getProvider(providerType);
const model = (globalConfig.model as string | undefined);
const history: ConversationMessage[] = [];
const agentName = 'interactive';
const savedSessions = loadAgentSessions(cwd, providerType);
let sessionId: string | undefined = savedSessions[agentName];
info('Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)');
if (sessionId) {
info('Resuming previous session');
}
blankLine();
/** Call AI with automatic retry on session error (stale/invalid session ID). */
async function callAIWithRetry(prompt: string): Promise<CallAIResult | null> {
const display = new StreamDisplay('assistant', isQuietMode());
try {
const result = await callAI(provider, prompt, cwd, model, sessionId, display);
// If session failed, clear it and retry without session
if (!result.success && sessionId) {
log.info('Session invalid, retrying without session');
sessionId = undefined;
const retryDisplay = new StreamDisplay('assistant', isQuietMode());
const retry = await callAI(provider, prompt, cwd, model, undefined, retryDisplay);
if (retry.sessionId) {
sessionId = retry.sessionId;
updateAgentSession(cwd, agentName, sessionId, providerType);
}
return retry;
}
if (result.sessionId) {
sessionId = result.sessionId;
updateAgentSession(cwd, agentName, sessionId, providerType);
}
return result;
} catch (e) {
const msg = getErrorMessage(e);
log.error('AI call failed', { error: msg });
error(msg);
blankLine();
return null;
}
}
// Process initial input if provided (e.g. from `takt a`)
if (initialInput) {
history.push({ role: 'user', content: initialInput });
log.debug('Processing initial input', { initialInput, sessionId });
const result = await callAIWithRetry(initialInput);
if (result) {
history.push({ role: 'assistant', content: result.content });
blankLine();
} else {
history.pop();
}
}
while (true) {
const input = await readLine(chalk.green('> '));
// EOF (Ctrl+D)
if (input === null) {
blankLine();
info('Cancelled');
return { confirmed: false, task: '' };
}
const trimmed = input.trim();
// Empty input — skip
if (!trimmed) {
continue;
}
// Handle slash commands
if (trimmed === '/go') {
if (history.length === 0) {
info('No conversation yet. Please describe your task first.');
continue;
}
const task = buildTaskFromHistory(history);
log.info('Interactive mode confirmed', { messageCount: history.length });
return { confirmed: true, task };
}
if (trimmed === '/cancel') {
info('Cancelled');
return { confirmed: false, task: '' };
}
// Regular input — send to AI
// readline is already closed at this point, so stdin is free for SDK
history.push({ role: 'user', content: trimmed });
log.debug('Sending to AI', { messageCount: history.length, sessionId });
process.stdin.pause();
const result = await callAIWithRetry(trimmed);
if (result) {
history.push({ role: 'assistant', content: result.content });
blankLine();
} else {
history.pop();
}
}
}

View File

@ -1,10 +0,0 @@
/**
* Task/workflow management commands.
*/
export { addTask, summarizeConversation } from './addTask.js';
export { listTasks, isBranchMerged, showFullDiff, type ListAction } from './listTasks.js';
export { watchTasks } from './watchTasks.js';
export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './config.js';
export { ejectBuiltin } from './eject.js';
export { switchWorkflow } from './workflow.js';

View File

@ -2,7 +2,7 @@
* Application-wide constants * Application-wide constants
*/ */
import type { Language } from './models/types.js'; import type { Language } from './core/models/index.js';
/** Default workflow name when none specified */ /** Default workflow name when none specified */
export const DEFAULT_WORKFLOW_NAME = 'default'; export const DEFAULT_WORKFLOW_NAME = 'default';

View File

@ -3,14 +3,20 @@ export type {
AgentType, AgentType,
Status, Status,
RuleMatchMethod, RuleMatchMethod,
PermissionMode,
ReportConfig, ReportConfig,
ReportObjectConfig, ReportObjectConfig,
AgentResponse, AgentResponse,
SessionState, SessionState,
WorkflowRule,
WorkflowStep, WorkflowStep,
LoopDetectionConfig,
WorkflowConfig, WorkflowConfig,
WorkflowState, WorkflowState,
CustomAgentConfig, CustomAgentConfig,
DebugConfig,
Language,
PipelineConfig,
GlobalConfig, GlobalConfig,
ProjectConfig, ProjectConfig,
} from './types.js'; } from './types.js';

View File

@ -5,7 +5,7 @@
*/ */
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) */ /** Agent model schema (opus, sonnet, haiku) */
export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet'); export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet');
@ -131,6 +131,8 @@ export const WorkflowStepRawSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
/** Agent is required for normal steps, optional for parallel container steps */ /** Agent is required for normal steps, optional for parallel container steps */
agent: z.string().optional(), agent: z.string().optional(),
/** Session handling for this step */
session: z.enum(['continue', 'refresh']).optional(),
/** Display name for the agent (shown in output). Falls back to agent basename if not specified */ /** Display name for the agent (shown in output). Falls back to agent basename if not specified */
agent_name: z.string().optional(), agent_name: z.string().optional(),
allowed_tools: z.array(z.string()).optional(), allowed_tools: z.array(z.string()).optional(),
@ -224,4 +226,3 @@ export const ProjectConfigSchema = z.object({
agents: z.array(CustomAgentConfigSchema).optional(), agents: z.array(CustomAgentConfigSchema).optional(),
provider: z.enum(['claude', 'codex', 'mock']).optional(), provider: z.enum(['claude', 'codex', 'mock']).optional(),
}); });

View File

@ -48,6 +48,8 @@ export interface WorkflowStep {
name: string; name: string;
/** Agent name or path as specified in workflow YAML */ /** Agent name or path as specified in workflow YAML */
agent: string; agent: string;
/** Session handling for this step */
session?: 'continue' | 'refresh';
/** Display name for the agent (shown in output). Falls back to agent basename if not specified */ /** Display name for the agent (shown in output). Falls back to agent basename if not specified */
agentDisplayName: string; agentDisplayName: string;
/** Allowed tools for this step (optional, passed to agent execution) */ /** Allowed tools for this step (optional, passed to agent execution) */

View File

@ -7,14 +7,15 @@
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 {
constructor( constructor(
private readonly engineOptions: WorkflowEngineOptions, private readonly engineOptions: WorkflowEngineOptions,
private readonly getCwd: () => string, private readonly getCwd: () => string,
private readonly getProjectCwd: () => string,
private readonly getSessionId: (agent: string) => string | undefined, private readonly getSessionId: (agent: string) => string | undefined,
private readonly getReportDir: () => string, private readonly getReportDir: () => string,
private readonly getLanguage: () => Language | undefined, private readonly getLanguage: () => Language | undefined,
@ -44,7 +45,7 @@ export class OptionsBuilder {
return { return {
...this.buildBaseOptions(step), ...this.buildBaseOptions(step),
sessionId: this.getSessionId(step.agent), sessionId: step.session === 'refresh' ? undefined : this.getSessionId(step.agent),
allowedTools, allowedTools,
}; };
} }
@ -70,7 +71,7 @@ export class OptionsBuilder {
): PhaseRunnerContext { ): PhaseRunnerContext {
return { return {
cwd: this.getCwd(), cwd: this.getCwd(),
reportDir: join(this.getCwd(), this.getReportDir()), reportDir: join(this.getProjectCwd(), this.getReportDir()),
language: this.getLanguage(), language: this.getLanguage(),
getSessionId: (agent: string) => state.agentSessions.get(agent), getSessionId: (agent: string) => state.agentSessions.get(agent),
buildResumeOptions: this.buildResumeOptions.bind(this), buildResumeOptions: this.buildResumeOptions.bind(this),

View File

@ -10,12 +10,12 @@ import type {
WorkflowState, WorkflowState,
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 '../../../shared/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';
import type { WorkflowEngineOptions } from '../types.js'; import type { WorkflowEngineOptions } from '../types.js';

View File

@ -14,12 +14,12 @@ import type {
AgentResponse, AgentResponse,
Language, Language,
} 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 '../../../shared/utils/debug.js';
import type { OptionsBuilder } from './OptionsBuilder.js'; import type { OptionsBuilder } from './OptionsBuilder.js';
const log = createLogger('step-executor'); const log = createLogger('step-executor');

Some files were not shown because too many files have changed in this diff Show More