diff --git a/AGENTS.md b/AGENTS.md index 12031e5..c9eed3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`)。 - ビルド成果物は `dist/`、実行スクリプトは `bin/`、静的リソースは `resources/`、ドキュメントは `docs/` で管理します。 - 設定やキャッシュを使う際は `~/.takt/` 以下(実行時)や `.takt/`(プロジェクト固有)を参照します。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a93d2..d938f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Internal - 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()` - Made `projectCwd` required throughout codebase - Removed deprecated `sacrificeMode` @@ -258,4 +258,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed -- Interactive mode removed; CLI simplified \ No newline at end of file +- Interactive mode removed; CLI simplified diff --git a/CLAUDE.md b/CLAUDE.md index b146b5e..1cae284 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 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) @@ -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 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 -**WorkflowEngine** (`src/workflow/engine.ts`) +**WorkflowEngine** (`src/core/workflow/engine/WorkflowEngine.ts`) - State machine that orchestrates agent execution via EventEmitter - 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` @@ -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 - 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): 1. Execution context (working dir, edit permission rules) 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` - `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` - `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 - `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/`) - `watcher.ts` - TaskWatcher class for polling and auto-executing tasks (used by `/watch`) - `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 ### Data Flow @@ -273,7 +273,7 @@ Files: `.takt/logs/{sessionId}.jsonl`, with `latest.json` pointer. Legacy `.json - ESM modules with `.js` extensions in imports - 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 ## Design Principles diff --git a/bin/takt b/bin/takt index 5b920e4..8559dd9 100755 --- a/bin/takt +++ b/bin/takt @@ -16,7 +16,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Import the actual CLI from dist -const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.js'); +const cliPath = join(__dirname, '..', 'dist', 'app', 'cli', 'index.js'); try { await import(cliPath); diff --git a/docs/data-flow.md b/docs/data-flow.md index ec20d03..265b0e5 100644 --- a/docs/data-flow.md +++ b/docs/data-flow.md @@ -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管理 @@ -398,7 +398,7 @@ TAKTのデータフローは以下の7つの主要なレイヤーで構成され ### 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` (成功/失敗) -#### 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.1 Step Execution (`src/workflow/engine/StepExecutor.ts`) +#### 6.1 Step Execution (`src/core/workflow/engine/StepExecutor.ts`) **役割**: 3フェーズモデルによるステップ実行 @@ -604,7 +604,7 @@ const match = await detectMatchedRule(step, response.content, tagContent, {...}) - `InstructionBuilder` を使用してインストラクション文字列を生成 - コンテキスト情報を渡す -#### 6.2 Instruction Building (`src/workflow/instruction/InstructionBuilder.ts`) +#### 6.2 Instruction Building (`src/core/workflow/instruction/InstructionBuilder.ts`) **役割**: Phase 1用のインストラクション文字列生成 @@ -691,7 +691,7 @@ const match = await detectMatchedRule(step, response.content, tagContent, {...}) - `error?: string` - `timestamp: Date` -#### 7.2 Provider (`src/providers/`) +#### 7.2 Provider (`src/infra/providers/`) **役割**: AIプロバイダー(Claude, Codex)とのSDK通信 @@ -881,7 +881,7 @@ new WorkflowEngine(workflowConfig, cwd, task, { ### 1. 会話履歴 → タスク文字列 -**場所**: `src/commands/interactive/interactive.ts` +**場所**: `src/features/interactive/interactive.ts` ```typescript function buildTaskFromHistory(history: ConversationMessage[]): string { @@ -897,7 +897,7 @@ function buildTaskFromHistory(history: ConversationMessage[]): string { ### 2. タスク → ブランチスラグ (AI生成) -**場所**: `src/task/summarize.ts` (呼び出し: `selectAndExecute.ts`, `taskExecution.ts`) +**場所**: `src/infra/task/summarize.ts` (呼び出し: `selectAndExecute.ts`, `taskExecution.ts`) ```typescript await summarizeTaskName(task, { cwd }) @@ -914,7 +914,7 @@ await summarizeTaskName(task, { cwd }) ### 3. ワークフロー設定 → WorkflowState -**場所**: `src/workflow/state-manager.ts` +**場所**: `src/core/workflow/engine/state-manager.ts` ```typescript function createInitialState( @@ -939,7 +939,7 @@ function createInitialState( ### 4. コンテキスト → インストラクション文字列 -**場所**: `src/workflow/instruction/InstructionBuilder.ts` +**場所**: `src/core/workflow/instruction/InstructionBuilder.ts` **入力**: - `step: WorkflowStep` @@ -958,7 +958,7 @@ function createInitialState( ### 5. AgentResponse → ルールマッチ -**場所**: `src/workflow/evaluation/RuleEvaluator.ts` +**場所**: `src/core/workflow/evaluation/RuleEvaluator.ts` **入力**: - `step: WorkflowStep` @@ -979,7 +979,7 @@ function createInitialState( ### 6. ルールマッチ → 次ステップ名 -**場所**: `src/workflow/transitions.ts` +**場所**: `src/core/workflow/engine/transitions.ts` ```typescript function determineNextStepByRules( @@ -997,7 +997,7 @@ function determineNextStepByRules( ### 7. Provider Response → AgentResponse -**場所**: `src/providers/claude.ts`, `src/providers/codex.ts` +**場所**: `src/infra/providers/claude.ts`, `src/infra/providers/codex.ts` **入力**: SDKレスポンス (`ClaudeResult`) @@ -1026,4 +1026,4 @@ TAKTのデータフローは、**7つのレイヤー**を通じて、ユーザ 5. **3-Phase Execution**: メイン実行、レポート出力、ステータス判断の3段階で、明確な責任分離 6. **Rule-Based Routing**: ルール評価の5段階フォールバックで、柔軟かつ予測可能な遷移 -このアーキテクチャにより、TAKTは複雑な多エージェント協調を、ユーザーには透明で、開発者には拡張可能な形で実現しています。 \ No newline at end of file +このアーキテクチャにより、TAKTは複雑な多エージェント協調を、ユーザーには透明で、開発者には拡張可能な形で実現しています。 diff --git a/package.json b/package.json index b0f6a93..ac4bc69 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "bin": { "takt": "./bin/takt", "takt-dev": "./bin/takt", - "takt-cli": "./dist/cli/index.js" + "takt-cli": "./dist/app/cli/index.js" }, "scripts": { "build": "tsc", diff --git a/resources/global/en/prompts/interactive-summary.md b/resources/global/en/prompts/interactive-summary.md new file mode 100644 index 0000000..1156b75 --- /dev/null +++ b/resources/global/en/prompts/interactive-summary.md @@ -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. diff --git a/resources/global/en/prompts/interactive-system.md b/resources/global/en/prompts/interactive-system.md new file mode 100644 index 0000000..2c6ec8a --- /dev/null +++ b/resources/global/en/prompts/interactive-system.md @@ -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. diff --git a/resources/global/en/workflows/default.yaml b/resources/global/en/workflows/default.yaml index ce0b894..85bb0b1 100644 --- a/resources/global/en/workflows/default.yaml +++ b/resources/global/en/workflows/default.yaml @@ -103,11 +103,14 @@ steps: rules: - condition: Implementation complete next: ai_review + - condition: No implementation (report only) + next: plan - condition: Cannot proceed, insufficient info next: plan instruction_template: | 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):** ```markdown @@ -139,6 +142,17 @@ steps: - **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 edit: false agent: ../agents/default/ai-antipattern-reviewer.md @@ -193,6 +207,7 @@ steps: - name: ai_fix edit: true agent: ../agents/default/coder.md + session: refresh allowed_tools: - Read - Glob @@ -206,6 +221,8 @@ steps: rules: - condition: AI issues fixed next: ai_review + - condition: No fix needed (verified target files/spec) + next: plan - condition: Cannot proceed, insufficient info next: plan instruction_template: | @@ -232,6 +249,21 @@ steps: **Absolutely prohibited:** - 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 - Leaving problems that AI Reviewer REJECTED pass_previous_response: true @@ -389,7 +421,7 @@ steps: Run tests, verify the build, and perform final approval. **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? 3. Was the original task objective achieved? diff --git a/resources/global/en/workflows/expert-cqrs.yaml b/resources/global/en/workflows/expert-cqrs.yaml index 825ca1c..2d918ff 100644 --- a/resources/global/en/workflows/expert-cqrs.yaml +++ b/resources/global/en/workflows/expert-cqrs.yaml @@ -100,7 +100,8 @@ steps: - WebFetch instruction_template: | 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):** ```markdown @@ -131,9 +132,19 @@ steps: - **Options Considered**: {List of options} - **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: - condition: Implementation is complete next: ai_review + - condition: No implementation (report only) + next: plan - condition: Cannot proceed with implementation next: plan @@ -194,6 +205,7 @@ steps: - name: ai_fix edit: true agent: ../agents/default/coder.md + session: refresh allowed_tools: - Read - Glob @@ -229,10 +241,30 @@ steps: - Reporting "fixed" without opening files - Judging based on assumptions - 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 rules: - condition: AI Reviewer's issues have been fixed next: ai_review + - condition: No fix needed (verified target files/spec) + next: plan - condition: Unable to proceed with fixes next: plan @@ -507,7 +539,7 @@ steps: Run tests, verify the build, and perform final approval. **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? 3. Was the original task objective achieved? diff --git a/resources/global/en/workflows/expert.yaml b/resources/global/en/workflows/expert.yaml index 26d8eae..6cbb78a 100644 --- a/resources/global/en/workflows/expert.yaml +++ b/resources/global/en/workflows/expert.yaml @@ -112,7 +112,8 @@ steps: - WebFetch instruction_template: | 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):** ```markdown @@ -143,9 +144,19 @@ steps: - **Options Considered**: {List of options} - **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: - condition: Implementation is complete next: ai_review + - condition: No implementation (report only) + next: plan - condition: Cannot proceed with implementation next: plan @@ -206,6 +217,7 @@ steps: - name: ai_fix edit: true agent: ../agents/default/coder.md + session: refresh allowed_tools: - Read - Glob @@ -242,10 +254,30 @@ steps: - Judging based on assumptions - Leaving problems that AI Reviewer REJECTED - 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 rules: - condition: AI Reviewer's issues have been fixed next: ai_review + - condition: No fix needed (verified target files/spec) + next: plan - condition: Unable to proceed with fixes next: plan @@ -520,7 +552,7 @@ steps: Run tests, verify the build, and perform final approval. **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? 3. Was the original task objective achieved? diff --git a/resources/global/en/workflows/simple.yaml b/resources/global/en/workflows/simple.yaml index 0ef12b5..53c1c14 100644 --- a/resources/global/en/workflows/simple.yaml +++ b/resources/global/en/workflows/simple.yaml @@ -99,11 +99,14 @@ steps: rules: - condition: Implementation complete next: ai_review + - condition: No implementation (report only) + next: plan - condition: Cannot proceed, insufficient info next: plan instruction_template: | 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):** ```markdown @@ -135,6 +138,25 @@ steps: - **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 edit: false agent: ../agents/default/ai-antipattern-reviewer.md @@ -260,7 +282,7 @@ steps: Run tests, verify the build, and perform final approval. **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? 3. Was the original task objective achieved? diff --git a/resources/global/ja/prompts/interactive-summary.md b/resources/global/ja/prompts/interactive-summary.md new file mode 100644 index 0000000..a7b7e29 --- /dev/null +++ b/resources/global/ja/prompts/interactive-summary.md @@ -0,0 +1,7 @@ +あなたはタスク要約者です。会話を計画ステップ向けの具体的なタスク指示に変換してください。 + +要件: +- 出力は最終的な指示のみ(前置き不要) +- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く +- 制約や「やらないこと」を保持する +- 情報不足があれば「Open Questions」セクションを短く付ける diff --git a/resources/global/ja/prompts/interactive-system.md b/resources/global/ja/prompts/interactive-system.md new file mode 100644 index 0000000..b687797 --- /dev/null +++ b/resources/global/ja/prompts/interactive-system.md @@ -0,0 +1,16 @@ +あなたはタスク計画のアシスタントです。会話を通じて要件の明確化・整理を手伝います。今は計画フェーズで、実行は別プロセスで行われます。 + +## 役割 +- あいまいな要求に対して確認質問をする +- コードベースの前提を把握する(Read/Glob/Grep/Bash は読み取りのみ) +- 見落としそうな点や改善点を提案する +- 必要に応じて理解した内容を簡潔にまとめる +- 返答は簡潔で要点のみ + +## 厳守事項 +- 計画のみを行い、実装はしない +- ファイルの作成/編集/削除はしない +- build/test/install など状態を変えるコマンドは実行しない +- Bash は読み取り専用(ls/cat/git log/git diff など)に限定 +- スラッシュコマンドに言及しない(存在を知らない前提) +- ユーザーが満足したら次工程に進む。次の指示はしない diff --git a/resources/global/ja/workflows/default.yaml b/resources/global/ja/workflows/default.yaml index 11023ed..9d06ef6 100644 --- a/resources/global/ja/workflows/default.yaml +++ b/resources/global/ja/workflows/default.yaml @@ -94,11 +94,14 @@ steps: rules: - condition: 実装完了 next: ai_review + - condition: 実装未着手(レポートのみ) + next: plan - condition: 判断できない、情報不足 next: plan instruction_template: | planステップで立てた計画に従って実装してください。 - 計画レポート(00-plan.md)を参照し、実装を進めてください。 + 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 + Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **重要**: 実装と同時に単体テストを追加してください。 - 新規作成したクラス・関数には単体テストを追加 @@ -135,6 +138,17 @@ steps: - **理由**: {選んだ理由} ``` + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + + **実装未着手の扱い(必須)** + - レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する + - name: ai_review edit: false agent: ../agents/default/ai-antipattern-reviewer.md @@ -189,6 +203,7 @@ steps: - name: ai_fix edit: true agent: ../agents/default/coder.md + session: refresh allowed_tools: - Read - Glob @@ -199,9 +214,12 @@ steps: - WebSearch - WebFetch permission_mode: acceptEdits + pass_previous_response: true rules: - condition: AI問題の修正完了 next: ai_review + - condition: 修正不要(指摘対象ファイル/仕様の確認済み) + next: plan - condition: 判断できない、情報不足 next: plan instruction_template: | @@ -228,9 +246,23 @@ steps: **絶対に禁止:** - ファイルを開かずに「修正済み」と報告 + + **修正不要の扱い(必須)** + - AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない + - 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「判断できない、情報不足」に対応するタグを出力する + - 修正不要の場合は「判断できない、情報不足」に対応するタグを出力し、理由と確認範囲を明記する + + **必須出力(見出しを含める)** + ## 確認したファイル + - {ファイルパス:行番号} + ## 実行した検索 + - {コマンドと要約} + ## 修正内容 + - {変更内容} + ## テスト結果 + - {実行コマンドと結果} - 思い込みで判断 - AI Reviewer が REJECT した問題の放置 - pass_previous_response: true - name: reviewers parallel: @@ -395,7 +427,7 @@ steps: テスト実行、ビルド確認、最終承認を行ってください。 **ワークフロー全体の確認:** - 1. 計画(00-plan.md)と実装結果が一致しているか + 1. 計画({report:00-plan.md})と実装結果が一致しているか 2. 各レビューステップの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/workflows/expert-cqrs.yaml b/resources/global/ja/workflows/expert-cqrs.yaml index 5bc9e28..451eaf0 100644 --- a/resources/global/ja/workflows/expert-cqrs.yaml +++ b/resources/global/ja/workflows/expert-cqrs.yaml @@ -109,7 +109,8 @@ steps: - WebFetch instruction_template: | planステップで立てた計画に従って実装してください。 - 計画レポート(00-plan.md)を参照し、実装を進めてください。 + 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 + Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **Scopeレポートフォーマット(実装開始時に作成):** ```markdown @@ -140,9 +141,19 @@ steps: - **検討した選択肢**: {選択肢リスト} - **理由**: {選んだ理由} ``` + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} rules: - condition: 実装が完了した next: ai_review + - condition: 実装未着手(レポートのみ) + next: plan - condition: 実装を進行できない next: plan @@ -203,6 +214,7 @@ steps: - name: ai_fix edit: true agent: ../agents/default/coder.md + session: refresh allowed_tools: - Read - Glob @@ -238,10 +250,30 @@ steps: - ファイルを開かずに「修正済み」と報告 - 思い込みで判断 - AI Reviewer が REJECT した問題の放置 + + **修正不要の扱い(必須)** + - AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない + - 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「修正を進行できない」に対応するタグを出力する + - 修正不要の場合は「修正を進行できない」に対応するタグを出力し、理由と確認範囲を明記する + + **必須出力(見出しを含める)** + ## 確認したファイル + - {ファイルパス:行番号} + ## 実行した検索 + - {コマンドと要約} + ## 修正内容 + - {変更内容} + ## テスト結果 + - {実行コマンドと結果} + + **実装未着手の扱い(必須)** + - レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する pass_previous_response: true rules: - condition: AI Reviewerの指摘に対する修正が完了した next: ai_review + - condition: 修正不要(指摘対象ファイル/仕様の確認済み) + next: plan - condition: 修正を進行できない next: plan @@ -516,7 +548,7 @@ steps: テスト実行、ビルド確認、最終承認を行ってください。 **ワークフロー全体の確認:** - 1. 計画(00-plan.md)と実装結果が一致しているか + 1. 計画({report:00-plan.md})と実装結果が一致しているか 2. 各レビューステップの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/workflows/expert.yaml b/resources/global/ja/workflows/expert.yaml index 4118b74..fc251bd 100644 --- a/resources/global/ja/workflows/expert.yaml +++ b/resources/global/ja/workflows/expert.yaml @@ -100,7 +100,8 @@ steps: - WebFetch instruction_template: | planステップで立てた計画に従って実装してください。 - 計画レポート(00-plan.md)を参照し、実装を進めてください。 + 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 + Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **Scopeレポートフォーマット(実装開始時に作成):** ```markdown @@ -131,9 +132,19 @@ steps: - **検討した選択肢**: {選択肢リスト} - **理由**: {選んだ理由} ``` + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} rules: - condition: 実装が完了した next: ai_review + - condition: 実装未着手(レポートのみ) + next: plan - condition: 実装を進行できない next: plan @@ -194,6 +205,7 @@ steps: - name: ai_fix edit: true agent: ../agents/default/coder.md + session: refresh allowed_tools: - Read - Glob @@ -229,10 +241,30 @@ steps: - ファイルを開かずに「修正済み」と報告 - 思い込みで判断 - AI Reviewer が REJECT した問題の放置 + + **修正不要の扱い(必須)** + - AI Reviewの指摘ごとに「対象ファイルの確認結果」を示せない場合は修正不要と判断しない + - 指摘が「生成物」「仕様同期」に関係する場合は、生成元/仕様の確認ができなければ「修正を進行できない」に対応するタグを出力する + - 修正不要の場合は「修正を進行できない」に対応するタグを出力し、理由と確認範囲を明記する + + **必須出力(見出しを含める)** + ## 確認したファイル + - {ファイルパス:行番号} + ## 実行した検索 + - {コマンドと要約} + ## 修正内容 + - {変更内容} + ## テスト結果 + - {実行コマンドと結果} + + **実装未着手の扱い(必須)** + - レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する pass_previous_response: true rules: - condition: AI Reviewerの指摘に対する修正が完了した next: ai_review + - condition: 修正不要(指摘対象ファイル/仕様の確認済み) + next: plan - condition: 修正を進行できない next: plan @@ -507,7 +539,7 @@ steps: テスト実行、ビルド確認、最終承認を行ってください。 **ワークフロー全体の確認:** - 1. 計画(00-plan.md)と実装結果が一致しているか + 1. 計画({report:00-plan.md})と実装結果が一致しているか 2. 各レビューステップの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/resources/global/ja/workflows/simple.yaml b/resources/global/ja/workflows/simple.yaml index 2092252..2324cd5 100644 --- a/resources/global/ja/workflows/simple.yaml +++ b/resources/global/ja/workflows/simple.yaml @@ -97,7 +97,8 @@ steps: permission_mode: acceptEdits instruction_template: | planステップで立てた計画に従って実装してください。 - 計画レポート(00-plan.md)を参照し、実装を進めてください。 + 計画レポート({report:00-plan.md})を参照し、実装を進めてください。 + Workflow Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。 **Scopeレポートフォーマット(実装開始時に作成):** ```markdown @@ -128,10 +129,23 @@ steps: - **検討した選択肢**: {選択肢リスト} - **理由**: {選んだ理由} ``` + + **必須出力(見出しを含める)** + ## 作業結果 + - {実施内容の要約} + ## 変更内容 + - {変更内容の要約} + ## テスト結果 + - {実行コマンドと結果} + + **実装未着手の扱い(必須)** + - レポート出力のみ/実装変更なしの場合は「実装未着手(レポートのみ)」に対応するタグを出力する rules: - - condition: "実装完了" + - condition: 実装が完了した next: ai_review - - condition: "判断できない、情報不足" + - condition: 実装未着手(レポートのみ) + next: plan + - condition: 実装を進行できない next: plan - name: ai_review @@ -254,7 +268,7 @@ steps: テスト実行、ビルド確認、最終承認を行ってください。 **ワークフロー全体の確認:** - 1. 計画(00-plan.md)と実装結果が一致しているか + 1. 計画({report:00-plan.md})と実装結果が一致しているか 2. 各レビューステップの指摘が対応されているか 3. 元のタスク目的が達成されているか diff --git a/src/__tests__/addTask.test.ts b/src/__tests__/addTask.test.ts index 2710a56..26f2e48 100644 --- a/src/__tests__/addTask.test.ts +++ b/src/__tests__/addTask.test.ts @@ -8,15 +8,15 @@ import * as path from 'node:path'; import { tmpdir } from 'node:os'; // Mock dependencies before importing the module under test -vi.mock('../commands/interactive/interactive.js', () => ({ +vi.mock('../features/interactive/index.js', () => ({ interactiveMode: vi.fn(), })); -vi.mock('../providers/index.js', () => ({ +vi.mock('../infra/providers/index.js', () => ({ getProvider: vi.fn(), })); -vi.mock('../config/global/globalConfig.js', () => ({ +vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn(() => ({ provider: 'claude' })), })); @@ -26,17 +26,17 @@ vi.mock('../prompt/index.js', () => ({ selectOption: vi.fn(), })); -vi.mock('../task/summarize.js', () => ({ +vi.mock('../infra/task/summarize.js', () => ({ summarizeTaskName: vi.fn(), })); -vi.mock('../utils/ui.js', () => ({ +vi.mock('../shared/ui/index.js', () => ({ success: vi.fn(), info: vi.fn(), blankLine: vi.fn(), })); -vi.mock('../utils/debug.js', () => ({ +vi.mock('../shared/utils/debug.js', () => ({ createLogger: () => ({ info: 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(), })); -vi.mock('../config/paths.js', async (importOriginal) => ({ ...(await importOriginal>()), +vi.mock('../infra/config/paths.js', async (importOriginal) => ({ ...(await importOriginal>()), getCurrentWorkflow: vi.fn(() => 'default'), })); -vi.mock('../github/issue.js', () => ({ +vi.mock('../infra/github/issue.js', () => ({ isIssueReference: vi.fn((s: string) => /^#\d+$/.test(s)), resolveIssueTask: vi.fn(), parseIssueNumbers: vi.fn((args: string[]) => { @@ -67,13 +67,13 @@ vi.mock('../github/issue.js', () => ({ }), })); -import { interactiveMode } from '../commands/interactive/interactive.js'; -import { getProvider } from '../providers/index.js'; +import { interactiveMode } from '../features/interactive/index.js'; +import { getProvider } from '../infra/providers/index.js'; import { promptInput, confirm, selectOption } from '../prompt/index.js'; -import { summarizeTaskName } from '../task/summarize.js'; -import { listWorkflows } from '../config/loaders/workflowLoader.js'; -import { resolveIssueTask } from '../github/issue.js'; -import { addTask, summarizeConversation } from '../commands/management/addTask.js'; +import { summarizeTaskName } from '../infra/task/summarize.js'; +import { listWorkflows } from '../infra/config/loaders/workflowLoader.js'; +import { resolveIssueTask } from '../infra/github/issue.js'; +import { addTask, summarizeConversation } from '../features/tasks/index.js'; const mockResolveIssueTask = vi.mocked(resolveIssueTask); diff --git a/src/__tests__/apiKeyAuth.test.ts b/src/__tests__/apiKeyAuth.test.ts index 3859481..a80c43e 100644 --- a/src/__tests__/apiKeyAuth.test.ts +++ b/src/__tests__/apiKeyAuth.test.ts @@ -14,7 +14,7 @@ import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; 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 const testId = randomUUID(); @@ -22,7 +22,7 @@ const testDir = join(tmpdir(), `takt-api-key-test-${testId}`); const taktDir = join(testDir, '.takt'); 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; return { ...original, @@ -32,7 +32,7 @@ vi.mock('../config/paths.js', async (importOriginal) => { }); // 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', () => { it('should accept config without API keys', () => { diff --git a/src/__tests__/autoCommit.test.ts b/src/__tests__/autoCommit.test.ts index 0cc40f5..4fab0fb 100644 --- a/src/__tests__/autoCommit.test.ts +++ b/src/__tests__/autoCommit.test.ts @@ -3,7 +3,7 @@ */ 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 vi.mock('node:child_process', () => ({ diff --git a/src/__tests__/cli-worktree.test.ts b/src/__tests__/cli-worktree.test.ts index e14bcb0..ae5ca04 100644 --- a/src/__tests__/cli-worktree.test.ts +++ b/src/__tests__/cli-worktree.test.ts @@ -10,20 +10,20 @@ vi.mock('../prompt/index.js', () => ({ selectOptionWithDefault: vi.fn(), })); -vi.mock('../task/clone.js', () => ({ +vi.mock('../infra/task/clone.js', () => ({ createSharedClone: vi.fn(), removeClone: vi.fn(), })); -vi.mock('../task/autoCommit.js', () => ({ +vi.mock('../infra/task/autoCommit.js', () => ({ autoCommitAndPush: vi.fn(), })); -vi.mock('../task/summarize.js', () => ({ +vi.mock('../infra/task/summarize.js', () => ({ summarizeTaskName: vi.fn(), })); -vi.mock('../utils/ui.js', () => ({ +vi.mock('../shared/ui/index.js', () => ({ info: vi.fn(), error: vi.fn(), success: vi.fn(), @@ -32,7 +32,7 @@ vi.mock('../utils/ui.js', () => ({ setLogLevel: vi.fn(), })); -vi.mock('../utils/debug.js', () => ({ +vi.mock('../shared/utils/debug.js', () => ({ createLogger: () => ({ info: vi.fn(), debug: vi.fn(), @@ -43,31 +43,20 @@ vi.mock('../utils/debug.js', () => ({ getDebugLogFile: vi.fn(), })); -vi.mock('../config/index.js', () => ({ +vi.mock('../infra/config/index.js', () => ({ initGlobalDirs: vi.fn(), initProjectDirs: vi.fn(), loadGlobalConfig: vi.fn(() => ({ logLevel: 'info' })), getEffectiveDebugConfig: vi.fn(), })); -vi.mock('../config/paths.js', () => ({ +vi.mock('../infra/config/paths.js', () => ({ clearAgentSessions: vi.fn(), getCurrentWorkflow: vi.fn(() => 'default'), isVerboseMode: vi.fn(() => false), })); -vi.mock('../commands/index.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', () => ({ +vi.mock('../infra/config/loaders/workflowLoader.js', () => ({ 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)), resolveIssueTask: vi.fn(), })); -vi.mock('../utils/updateNotifier.js', () => ({ +vi.mock('../shared/utils/updateNotifier.js', () => ({ checkForUpdates: vi.fn(), })); import { confirm } from '../prompt/index.js'; -import { createSharedClone } from '../task/clone.js'; -import { summarizeTaskName } from '../task/summarize.js'; -import { info } from '../utils/ui.js'; -import { confirmAndCreateWorktree } from '../commands/execution/selectAndExecute.js'; +import { createSharedClone } from '../infra/task/clone.js'; +import { summarizeTaskName } from '../infra/task/summarize.js'; +import { info } from '../shared/ui/index.js'; +import { confirmAndCreateWorktree } from '../features/tasks/index.js'; const mockConfirm = vi.mocked(confirm); const mockCreateSharedClone = vi.mocked(createSharedClone); diff --git a/src/__tests__/clone.test.ts b/src/__tests__/clone.test.ts index 583346f..eb9d635 100644 --- a/src/__tests__/clone.test.ts +++ b/src/__tests__/clone.test.ts @@ -21,7 +21,7 @@ vi.mock('node:fs', () => ({ existsSync: vi.fn(), })); -vi.mock('../utils/debug.js', () => ({ +vi.mock('../shared/utils/debug.js', () => ({ createLogger: () => ({ info: 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(() => ({})), })); 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); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 68d9e25..6c34d2d 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -13,7 +13,7 @@ import { loadWorkflow, listWorkflows, loadAgentPromptFromPath, -} from '../config/loaders/loader.js'; +} from '../infra/config/loaders/loader.js'; import { getCurrentWorkflow, setCurrentWorkflow, @@ -35,9 +35,9 @@ import { getWorktreeSessionPath, loadWorktreeSessions, updateWorktreeSession, -} from '../config/paths.js'; -import { getLanguage } from '../config/global/globalConfig.js'; -import { loadProjectConfig } from '../config/project/projectConfig.js'; +} from '../infra/config/paths.js'; +import { getLanguage } from '../infra/config/global/globalConfig.js'; +import { loadProjectConfig } from '../infra/config/project/projectConfig.js'; describe('getBuiltinWorkflow', () => { it('should return builtin workflow when it exists in resources', () => { diff --git a/src/__tests__/debug.test.ts b/src/__tests__/debug.test.ts index 888a93a..3ca7281 100644 --- a/src/__tests__/debug.test.ts +++ b/src/__tests__/debug.test.ts @@ -14,7 +14,7 @@ import { debugLog, infoLog, errorLog, -} from '../utils/debug.js'; +} from '../shared/utils/debug.js'; import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; diff --git a/src/__tests__/engine-abort.test.ts b/src/__tests__/engine-abort.test.ts index aad3a42..99d74d8 100644 --- a/src/__tests__/engine-abort.test.ts +++ b/src/__tests__/engine-abort.test.ts @@ -10,7 +10,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 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) --- @@ -18,17 +18,17 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../workflow/evaluation/index.js', () => ({ +vi.mock('../core/workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); @@ -38,7 +38,7 @@ vi.mock('../claude/query-manager.js', () => ({ // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; import { runAgent } from '../agents/runner.js'; import { interruptAllQueries } from '../claude/query-manager.js'; import { diff --git a/src/__tests__/engine-agent-overrides.test.ts b/src/__tests__/engine-agent-overrides.test.ts index 48913b1..0a76d75 100644 --- a/src/__tests__/engine-agent-overrides.test.ts +++ b/src/__tests__/engine-agent-overrides.test.ts @@ -11,23 +11,23 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../workflow/evaluation/index.js', () => ({ +vi.mock('../core/workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn(), runReportPhase: vi.fn(), runStatusJudgmentPhase: vi.fn(), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ 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 type { WorkflowConfig } from '../models/types.js'; +import type { WorkflowConfig } from '../core/models/index.js'; import { makeResponse, makeRule, diff --git a/src/__tests__/engine-blocked.test.ts b/src/__tests__/engine-blocked.test.ts index 1e084e5..49ae0dc 100644 --- a/src/__tests__/engine-blocked.test.ts +++ b/src/__tests__/engine-blocked.test.ts @@ -16,23 +16,23 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../workflow/evaluation/index.js', () => ({ +vi.mock('../core/workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; import { makeResponse, buildDefaultWorkflowConfig, diff --git a/src/__tests__/engine-error.test.ts b/src/__tests__/engine-error.test.ts index 632e679..2c28aa5 100644 --- a/src/__tests__/engine-error.test.ts +++ b/src/__tests__/engine-error.test.ts @@ -17,25 +17,25 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../workflow/evaluation/index.js', () => ({ +vi.mock('../core/workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; import { runAgent } from '../agents/runner.js'; -import { detectMatchedRule } from '../workflow/evaluation/index.js'; +import { detectMatchedRule } from '../core/workflow/index.js'; import { makeResponse, makeStep, diff --git a/src/__tests__/engine-happy-path.test.ts b/src/__tests__/engine-happy-path.test.ts index 51cc97c..2357a70 100644 --- a/src/__tests__/engine-happy-path.test.ts +++ b/src/__tests__/engine-happy-path.test.ts @@ -13,7 +13,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 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) --- @@ -21,23 +21,23 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../workflow/evaluation/index.js', () => ({ +vi.mock('../core/workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; import { runAgent } from '../agents/runner.js'; import { makeResponse, diff --git a/src/__tests__/engine-parallel.test.ts b/src/__tests__/engine-parallel.test.ts index c2b3446..c9132fb 100644 --- a/src/__tests__/engine-parallel.test.ts +++ b/src/__tests__/engine-parallel.test.ts @@ -16,23 +16,23 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../workflow/evaluation/index.js', () => ({ +vi.mock('../core/workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; import { runAgent } from '../agents/runner.js'; import { makeResponse, diff --git a/src/__tests__/engine-report.test.ts b/src/__tests__/engine-report.test.ts index 707b5b0..ab73825 100644 --- a/src/__tests__/engine-report.test.ts +++ b/src/__tests__/engine-report.test.ts @@ -8,8 +8,8 @@ import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { EventEmitter } from 'node:events'; import { existsSync } from 'node:fs'; -import { isReportObjectConfig } from '../workflow/instruction/InstructionBuilder.js'; -import type { WorkflowStep, ReportObjectConfig, ReportConfig } from '../models/types.js'; +import { isReportObjectConfig } from '../core/workflow/index.js'; +import type { WorkflowStep, ReportObjectConfig, ReportConfig } from '../core/models/index.js'; /** * Extracted emitStepReports logic for unit testing. diff --git a/src/__tests__/engine-test-helpers.ts b/src/__tests__/engine-test-helpers.ts index 1d1ab7d..55e148c 100644 --- a/src/__tests__/engine-test-helpers.ts +++ b/src/__tests__/engine-test-helpers.ts @@ -10,15 +10,15 @@ import { mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; 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) --- import { runAgent } from '../agents/runner.js'; -import { detectMatchedRule } from '../workflow/evaluation/index.js'; -import type { RuleMatch } from '../workflow/evaluation/index.js'; -import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../workflow/engine/phase-runner.js'; -import { generateReportDir } from '../utils/session.js'; +import { detectMatchedRule } from '../core/workflow/index.js'; +import type { RuleMatch } from '../core/workflow/index.js'; +import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../core/workflow/index.js'; +import { generateReportDir } from '../shared/utils/reportDir.js'; // --- Factory functions --- diff --git a/src/__tests__/engine-worktree-report.test.ts b/src/__tests__/engine-worktree-report.test.ts index bd6f28f..2ddc982 100644 --- a/src/__tests__/engine-worktree-report.test.ts +++ b/src/__tests__/engine-worktree-report.test.ts @@ -17,24 +17,24 @@ vi.mock('../agents/runner.js', () => ({ runAgent: vi.fn(), })); -vi.mock('../workflow/evaluation/index.js', () => ({ +vi.mock('../core/workflow/evaluation/index.js', () => ({ detectMatchedRule: vi.fn(), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; -import { runReportPhase } from '../workflow/engine/phase-runner.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; +import { runReportPhase } from '../core/workflow/index.js'; import { makeResponse, makeStep, @@ -43,7 +43,7 @@ import { mockDetectMatchedRuleSequence, applyDefaultMocks, } 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 } { 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 const config = buildSimpleConfig(); const engine = new WorkflowEngine(config, cloneCwd, 'test task', { @@ -117,14 +117,14 @@ describe('WorkflowEngine: worktree reportDir resolution', () => { // When: run the workflow 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); expect(reportPhaseMock).toHaveBeenCalled(); const phaseCtx = reportPhaseMock.mock.calls[0][2] as { reportDir: string }; - // reportDir should be resolved from cloneCwd (cwd), not projectCwd - const expectedPath = join(cloneCwd, '.takt/reports/test-report-dir'); - const unexpectedPath = join(projectCwd, '.takt/reports/test-report-dir'); + // reportDir should be resolved from projectCwd, not cloneCwd + const expectedPath = join(projectCwd, '.takt/reports/test-report-dir'); + const unexpectedPath = join(cloneCwd, '.takt/reports/test-report-dir'); expect(phaseCtx.reportDir).toBe(expectedPath); expect(phaseCtx.reportDir).not.toBe(unexpectedPath); diff --git a/src/__tests__/getOriginalInstruction.test.ts b/src/__tests__/getOriginalInstruction.test.ts index 56f3c43..7a01221 100644 --- a/src/__tests__/getOriginalInstruction.test.ts +++ b/src/__tests__/getOriginalInstruction.test.ts @@ -12,7 +12,7 @@ vi.mock('node:child_process', () => ({ import { execFileSync } from 'node:child_process'; const mockExecFileSync = vi.mocked(execFileSync); -import { getOriginalInstruction } from '../task/branchList.js'; +import { getOriginalInstruction } from '../infra/task/branchList.js'; beforeEach(() => { vi.clearAllMocks(); diff --git a/src/__tests__/github-issue.test.ts b/src/__tests__/github-issue.test.ts index 671736d..e8f1801 100644 --- a/src/__tests__/github-issue.test.ts +++ b/src/__tests__/github-issue.test.ts @@ -12,7 +12,7 @@ import { isIssueReference, formatIssueAsTask, type GitHubIssue, -} from '../github/issue.js'; +} from '../infra/github/issue.js'; describe('parseIssueNumbers', () => { it('should parse single issue reference', () => { diff --git a/src/__tests__/github-pr.test.ts b/src/__tests__/github-pr.test.ts index bc6c245..ccd0380 100644 --- a/src/__tests__/github-pr.test.ts +++ b/src/__tests__/github-pr.test.ts @@ -6,8 +6,8 @@ */ import { describe, it, expect } from 'vitest'; -import { buildPrBody } from '../github/pr.js'; -import type { GitHubIssue } from '../github/types.js'; +import { buildPrBody } from '../infra/github/pr.js'; +import type { GitHubIssue } from '../infra/github/types.js'; describe('buildPrBody', () => { it('should build body with issue and report', () => { diff --git a/src/__tests__/globalConfig-defaults.test.ts b/src/__tests__/globalConfig-defaults.test.ts index b48922a..d9b47f0 100644 --- a/src/__tests__/globalConfig-defaults.test.ts +++ b/src/__tests__/globalConfig-defaults.test.ts @@ -20,8 +20,8 @@ vi.mock('node:os', async () => { }); // Import after mocks are set up -const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache } = await import('../config/global/globalConfig.js'); -const { getGlobalConfigPath } = await import('../config/paths.js'); +const { loadGlobalConfig, saveGlobalConfig, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js'); +const { getGlobalConfigPath } = await import('../infra/config/paths.js'); describe('loadGlobalConfig', () => { beforeEach(() => { diff --git a/src/__tests__/initialization-noninteractive.test.ts b/src/__tests__/initialization-noninteractive.test.ts index eb45e75..fee6594 100644 --- a/src/__tests__/initialization-noninteractive.test.ts +++ b/src/__tests__/initialization-noninteractive.test.ts @@ -25,8 +25,8 @@ vi.mock('../prompt/index.js', () => ({ })); // Import after mocks are set up -const { initGlobalDirs, needsLanguageSetup } = await import('../config/global/initialization.js'); -const { getGlobalConfigPath, getGlobalConfigDir } = await import('../config/paths.js'); +const { initGlobalDirs, needsLanguageSetup } = await import('../infra/config/global/initialization.js'); +const { getGlobalConfigPath, getGlobalConfigDir } = await import('../infra/config/paths.js'); describe('initGlobalDirs with non-interactive mode', () => { beforeEach(() => { diff --git a/src/__tests__/initialization.test.ts b/src/__tests__/initialization.test.ts index ecb088c..6adff90 100644 --- a/src/__tests__/initialization.test.ts +++ b/src/__tests__/initialization.test.ts @@ -25,8 +25,8 @@ vi.mock('../prompt/index.js', () => ({ })); // Import after mocks are set up -const { needsLanguageSetup } = await import('../config/global/initialization.js'); -const { getGlobalConfigPath } = await import('../config/paths.js'); +const { needsLanguageSetup } = await import('../infra/config/global/initialization.js'); +const { getGlobalConfigPath } = await import('../infra/config/paths.js'); const { copyProjectResourcesToDir, getLanguageResourcesDir, getProjectResourcesDir } = await import('../resources/index.js'); describe('initialization', () => { diff --git a/src/__tests__/instructionBuilder.test.ts b/src/__tests__/instructionBuilder.test.ts index 55e5875..9f65fa1 100644 --- a/src/__tests__/instructionBuilder.test.ts +++ b/src/__tests__/instructionBuilder.test.ts @@ -6,15 +6,15 @@ import { describe, it, expect } from 'vitest'; import { InstructionBuilder, isReportObjectConfig, -} from '../workflow/instruction/InstructionBuilder.js'; -import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js'; -import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../workflow/instruction/StatusJudgmentBuilder.js'; -import { + ReportInstructionBuilder, + StatusJudgmentBuilder, buildExecutionMetadata, renderExecutionMetadata, + generateStatusRulesFromRules, + type ReportInstructionContext, + type StatusJudgmentContext, type InstructionContext, -} from '../workflow/instruction/instruction-context.js'; -import { generateStatusRulesFromRules } from '../workflow/instruction/status-rules.js'; +} from '../core/workflow/index.js'; // Backward-compatible function wrappers for test readability function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string { @@ -26,7 +26,7 @@ function buildReportInstruction(step: WorkflowStep, ctx: ReportInstructionContex function buildStatusJudgmentInstruction(step: WorkflowStep, ctx: StatusJudgmentContext): string { 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 { diff --git a/src/__tests__/interactive.test.ts b/src/__tests__/interactive.test.ts index 867d37e..bac06a3 100644 --- a/src/__tests__/interactive.test.ts +++ b/src/__tests__/interactive.test.ts @@ -4,15 +4,15 @@ 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' })), })); -vi.mock('../providers/index.js', () => ({ +vi.mock('../infra/providers/index.js', () => ({ getProvider: vi.fn(), })); -vi.mock('../utils/debug.js', () => ({ +vi.mock('../shared/utils/debug.js', () => ({ createLogger: () => ({ info: vi.fn(), debug: vi.fn(), @@ -24,12 +24,12 @@ vi.mock('../context.js', () => ({ isQuietMode: vi.fn(() => false), })); -vi.mock('../config/paths.js', () => ({ +vi.mock('../infra/config/paths.js', () => ({ loadAgentSessions: vi.fn(() => ({})), updateAgentSession: vi.fn(), })); -vi.mock('../utils/ui.js', () => ({ +vi.mock('../shared/ui/index.js', () => ({ info: vi.fn(), error: 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 vi.mock('node:readline', () => ({ createInterface: vi.fn(), })); import { createInterface } from 'node:readline'; -import { getProvider } from '../providers/index.js'; -import { interactiveMode } from '../commands/interactive/interactive.js'; +import { getProvider } from '../infra/providers/index.js'; +import { interactiveMode } from '../features/interactive/index.js'; +import { selectOption } from '../prompt/index.js'; const mockGetProvider = vi.mocked(getProvider); const mockCreateInterface = vi.mocked(createInterface); +const mockSelectOption = vi.mocked(selectOption); /** Helper to set up a sequence of readline inputs */ function setupInputSequence(inputs: (string | null)[]): void { @@ -111,6 +117,7 @@ function setupMockProvider(responses: string[]): void { beforeEach(() => { vi.clearAllMocks(); + mockSelectOption.mockResolvedValue('yes'); }); describe('interactiveMode', () => { @@ -162,15 +169,14 @@ describe('interactiveMode', () => { it('should return confirmed=true with task on /go after conversation', async () => { // Given setupInputSequence(['add auth feature', '/go']); - setupMockProvider(['What kind of authentication?']); + setupMockProvider(['What kind of authentication?', 'Implement auth feature with chosen method.']); // When const result = await interactiveMode('/project'); // Then expect(result.confirmed).toBe(true); - expect(result.task).toContain('add auth feature'); - expect(result.task).toContain('What kind of authentication?'); + expect(result.task).toBe('Implement auth feature with chosen method.'); }); it('should reject /go with no prior conversation', async () => { @@ -188,7 +194,7 @@ describe('interactiveMode', () => { it('should skip empty input', async () => { // Given: empty line, then actual input, then /go setupInputSequence(['', 'do something', '/go']); - setupMockProvider(['Sure, what exactly?']); + setupMockProvider(['Sure, what exactly?', 'Do something with the clarified scope.']); // When const result = await interactiveMode('/project'); @@ -196,23 +202,27 @@ describe('interactiveMode', () => { // Then expect(result.confirmed).toBe(true); const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType }; - expect(mockProvider.call).toHaveBeenCalledTimes(1); + expect(mockProvider.call).toHaveBeenCalledTimes(2); }); it('should accumulate conversation history across multiple turns', async () => { // Given: two user messages before /go setupInputSequence(['first message', 'second message', '/go']); - setupMockProvider(['response to first', 'response to second']); + setupMockProvider(['response to first', 'response to second', 'Summarized task.']); // When 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.task).toContain('first message'); - expect(result.task).toContain('response to first'); - expect(result.task).toContain('second message'); - expect(result.task).toContain('response to second'); + expect(result.task).toBe('Summarized task.'); + const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType }; + const summaryPrompt = mockProvider.call.mock.calls[2]?.[1] as string; + 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 () => { @@ -232,39 +242,37 @@ describe('interactiveMode', () => { it('should process initialInput as first message before entering loop', async () => { // Given: initialInput provided, then user types /go setupInputSequence(['/go']); - setupMockProvider(['What do you mean by "a"?']); + setupMockProvider(['What do you mean by "a"?', 'Clarify task for "a".']); // When const result = await interactiveMode('/project', 'a'); // Then: AI should have been called with initialInput const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType }; - expect(mockProvider.call).toHaveBeenCalledTimes(1); + expect(mockProvider.call).toHaveBeenCalledTimes(2); expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a'); // /go should work because initialInput already started conversation expect(result.confirmed).toBe(true); - expect(result.task).toContain('a'); - expect(result.task).toContain('What do you mean by "a"?'); + expect(result.task).toBe('Clarify task for "a".'); }); it('should send only current input for subsequent turns after initialInput', async () => { // Given: initialInput, then follow-up, then /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 const result = await interactiveMode('/project', 'a'); // Then: each call receives only its own input (session handles history) const mockProvider = mockGetProvider.mock.results[0]!.value as { call: ReturnType }; - expect(mockProvider.call).toHaveBeenCalledTimes(2); + expect(mockProvider.call).toHaveBeenCalledTimes(3); expect(mockProvider.call.mock.calls[0]?.[1]).toBe('a'); expect(mockProvider.call.mock.calls[1]?.[1]).toBe('fix the login page'); // Task still contains all history for downstream use expect(result.confirmed).toBe(true); - expect(result.task).toContain('a'); - expect(result.task).toContain('fix the login page'); + expect(result.task).toBe('Fix login page with clarified scope.'); }); }); diff --git a/src/__tests__/it-error-recovery.test.ts b/src/__tests__/it-error-recovery.test.ts index 6374f47..420df51 100644 --- a/src/__tests__/it-error-recovery.test.ts +++ b/src/__tests__/it-error-recovery.test.ts @@ -13,7 +13,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; 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 --- @@ -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), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateSessionId: vi.fn().mockReturnValue('test-session-id'), })); -vi.mock('../config/global/globalConfig.js', () => ({ +vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), })); -vi.mock('../config/project/projectConfig.js', () => ({ +vi.mock('../infra/config/project/projectConfig.js', () => ({ loadProjectConfig: vi.fn().mockReturnValue({}), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; // --- Test helpers --- diff --git a/src/__tests__/it-instruction-builder.test.ts b/src/__tests__/it-instruction-builder.test.ts index 49b1cd2..a5a7fe0 100644 --- a/src/__tests__/it-instruction-builder.test.ts +++ b/src/__tests__/it-instruction-builder.test.ts @@ -8,17 +8,17 @@ */ 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({}), getLanguage: vi.fn().mockReturnValue('en'), })); -import { InstructionBuilder } from '../workflow/instruction/InstructionBuilder.js'; -import { ReportInstructionBuilder, type ReportInstructionContext } from '../workflow/instruction/ReportInstructionBuilder.js'; -import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../workflow/instruction/StatusJudgmentBuilder.js'; -import type { InstructionContext } from '../workflow/instruction/instruction-context.js'; +import { InstructionBuilder } from '../core/workflow/index.js'; +import { ReportInstructionBuilder, type ReportInstructionContext } from '../core/workflow/index.js'; +import { StatusJudgmentBuilder, type StatusJudgmentContext } from '../core/workflow/index.js'; +import type { InstructionContext } from '../core/workflow/index.js'; // Function wrappers for test readability function buildInstruction(step: WorkflowStep, ctx: InstructionContext): string { diff --git a/src/__tests__/it-pipeline-modes.test.ts b/src/__tests__/it-pipeline-modes.test.ts index 638c093..f93d48c 100644 --- a/src/__tests__/it-pipeline-modes.test.ts +++ b/src/__tests__/it-pipeline-modes.test.ts @@ -43,23 +43,23 @@ vi.mock('node:child_process', () => ({ execFileSync: vi.fn(), })); -vi.mock('../github/issue.js', () => ({ +vi.mock('../infra/github/issue.js', () => ({ fetchIssue: mockFetchIssue, formatIssueAsTask: mockFormatIssueAsTask, checkGhCli: mockCheckGhCli, })); -vi.mock('../github/pr.js', () => ({ +vi.mock('../infra/github/pr.js', () => ({ createPullRequest: mockCreatePullRequest, pushBranch: mockPushBranch, buildPrBody: vi.fn().mockReturnValue('PR body'), })); -vi.mock('../task/git.js', () => ({ +vi.mock('../infra/task/git.js', () => ({ stageAndCommit: vi.fn().mockReturnValue('abc1234'), })); -vi.mock('../utils/ui.js', () => ({ +vi.mock('../shared/ui/index.js', () => ({ header: vi.fn(), info: 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(), notifyError: vi.fn(), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateSessionId: vi.fn().mockReturnValue('test-session-id'), createSessionLog: vi.fn().mockReturnValue({ startTime: new Date().toISOString(), @@ -91,8 +91,8 @@ vi.mock('../utils/session.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); -vi.mock('../config/paths.js', async (importOriginal) => { - const original = await importOriginal(); +vi.mock('../infra/config/paths.js', async (importOriginal) => { + const original = await importOriginal(); return { ...original, loadAgentSessions: vi.fn().mockReturnValue({}), @@ -104,8 +104,8 @@ vi.mock('../config/paths.js', async (importOriginal) => { }; }); -vi.mock('../config/global/globalConfig.js', async (importOriginal) => { - const original = await importOriginal(); +vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => { + const original = await importOriginal(); return { ...original, loadGlobalConfig: vi.fn().mockReturnValue({}), @@ -114,8 +114,8 @@ vi.mock('../config/global/globalConfig.js', async (importOriginal) => { }; }); -vi.mock('../config/project/projectConfig.js', async (importOriginal) => { - const original = await importOriginal(); +vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => { + const original = await importOriginal(); return { ...original, loadProjectConfig: vi.fn().mockReturnValue({}), @@ -131,7 +131,7 @@ vi.mock('../prompt/index.js', () => ({ promptInput: vi.fn().mockResolvedValue(null), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -139,7 +139,7 @@ vi.mock('../workflow/engine/phase-runner.js', () => ({ // --- Imports (after mocks) --- -import { executePipeline } from '../commands/execution/pipelineExecution.js'; +import { executePipeline } from '../features/pipeline/index.js'; import { EXIT_ISSUE_FETCH_FAILED, EXIT_WORKFLOW_FAILED, diff --git a/src/__tests__/it-pipeline.test.ts b/src/__tests__/it-pipeline.test.ts index ebf7ab4..6fd950b 100644 --- a/src/__tests__/it-pipeline.test.ts +++ b/src/__tests__/it-pipeline.test.ts @@ -30,19 +30,19 @@ vi.mock('node:child_process', () => ({ execFileSync: vi.fn(), })); -vi.mock('../github/issue.js', () => ({ +vi.mock('../infra/github/issue.js', () => ({ fetchIssue: vi.fn(), formatIssueAsTask: vi.fn(), checkGhCli: vi.fn(), })); -vi.mock('../github/pr.js', () => ({ +vi.mock('../infra/github/pr.js', () => ({ createPullRequest: vi.fn(), pushBranch: vi.fn(), buildPrBody: vi.fn().mockReturnValue('PR body'), })); -vi.mock('../utils/ui.js', () => ({ +vi.mock('../shared/ui/index.js', () => ({ header: vi.fn(), info: 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(), notifyError: vi.fn(), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateSessionId: vi.fn().mockReturnValue('test-session-id'), createSessionLog: vi.fn().mockReturnValue({ startTime: new Date().toISOString(), @@ -74,8 +74,8 @@ vi.mock('../utils/session.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), })); -vi.mock('../config/paths.js', async (importOriginal) => { - const original = await importOriginal(); +vi.mock('../infra/config/paths.js', async (importOriginal) => { + const original = await importOriginal(); return { ...original, loadAgentSessions: vi.fn().mockReturnValue({}), @@ -87,8 +87,8 @@ vi.mock('../config/paths.js', async (importOriginal) => { }; }); -vi.mock('../config/global/globalConfig.js', async (importOriginal) => { - const original = await importOriginal(); +vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => { + const original = await importOriginal(); return { ...original, loadGlobalConfig: vi.fn().mockReturnValue({}), @@ -96,8 +96,8 @@ vi.mock('../config/global/globalConfig.js', async (importOriginal) => { }; }); -vi.mock('../config/project/projectConfig.js', async (importOriginal) => { - const original = await importOriginal(); +vi.mock('../infra/config/project/projectConfig.js', async (importOriginal) => { + const original = await importOriginal(); return { ...original, loadProjectConfig: vi.fn().mockReturnValue({}), @@ -113,7 +113,7 @@ vi.mock('../prompt/index.js', () => ({ promptInput: vi.fn().mockResolvedValue(null), })); -vi.mock('../workflow/engine/phase-runner.js', () => ({ +vi.mock('../core/workflow/phase-runner.js', () => ({ needsStatusJudgmentPhase: vi.fn().mockReturnValue(false), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), @@ -121,7 +121,7 @@ vi.mock('../workflow/engine/phase-runner.js', () => ({ // --- Imports (after mocks) --- -import { executePipeline } from '../commands/execution/pipelineExecution.js'; +import { executePipeline } from '../features/pipeline/index.js'; // --- Test helpers --- diff --git a/src/__tests__/it-rule-evaluation.test.ts b/src/__tests__/it-rule-evaluation.test.ts index 409e238..75a248b 100644 --- a/src/__tests__/it-rule-evaluation.test.ts +++ b/src/__tests__/it-rule-evaluation.test.ts @@ -15,7 +15,7 @@ */ 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 --- @@ -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({}), getLanguage: vi.fn().mockReturnValue('en'), })); -vi.mock('../config/project/projectConfig.js', () => ({ +vi.mock('../infra/config/project/projectConfig.js', () => ({ loadProjectConfig: vi.fn().mockReturnValue({}), })); // --- Imports (after mocks) --- -import { detectMatchedRule, evaluateAggregateConditions } from '../workflow/evaluation/index.js'; -import type { RuleMatch, RuleEvaluatorContext } from '../workflow/evaluation/index.js'; +import { detectMatchedRule, evaluateAggregateConditions } from '../core/workflow/index.js'; +import type { RuleMatch, RuleEvaluatorContext } from '../core/workflow/index.js'; // --- Test helpers --- @@ -49,7 +49,11 @@ function makeRule(condition: string, next: string, extra?: Partial return { condition, next, ...extra }; } -function makeStep(name: string, rules: WorkflowRule[], parallel?: WorkflowStep[]): WorkflowStep { +function makeStep( + name: string, + rules: WorkflowRule[], + parallel?: WorkflowStep[], +): WorkflowStep { return { name, agent: 'test-agent', @@ -140,6 +144,7 @@ describe('Rule Evaluation IT: Phase 1 tag fallback', () => { }); }); + describe('Rule Evaluation IT: Aggregate conditions (all/any)', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/__tests__/it-three-phase-execution.test.ts b/src/__tests__/it-three-phase-execution.test.ts index cca0ed4..4d332b4 100644 --- a/src/__tests__/it-three-phase-execution.test.ts +++ b/src/__tests__/it-three-phase-execution.test.ts @@ -14,7 +14,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; 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 --- @@ -30,30 +30,30 @@ const mockNeedsStatusJudgmentPhase = vi.fn(); const mockRunReportPhase = 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), runReportPhase: (...args: unknown[]) => mockRunReportPhase(...args), runStatusJudgmentPhase: (...args: unknown[]) => mockRunStatusJudgmentPhase(...args), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateSessionId: vi.fn().mockReturnValue('test-session-id'), })); -vi.mock('../config/global/globalConfig.js', () => ({ +vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), })); -vi.mock('../config/project/projectConfig.js', () => ({ +vi.mock('../infra/config/project/projectConfig.js', () => ({ loadProjectConfig: vi.fn().mockReturnValue({}), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; // --- Test helpers --- diff --git a/src/__tests__/it-workflow-execution.test.ts b/src/__tests__/it-workflow-execution.test.ts index 67bf1c1..6ca3a64 100644 --- a/src/__tests__/it-workflow-execution.test.ts +++ b/src/__tests__/it-workflow-execution.test.ts @@ -14,7 +14,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; 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) --- @@ -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), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateSessionId: vi.fn().mockReturnValue('test-session-id'), })); -vi.mock('../config/global/globalConfig.js', () => ({ +vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), })); -vi.mock('../config/project/projectConfig.js', () => ({ +vi.mock('../infra/config/project/projectConfig.js', () => ({ loadProjectConfig: vi.fn().mockReturnValue({}), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; // --- Test helpers --- diff --git a/src/__tests__/it-workflow-loader.test.ts b/src/__tests__/it-workflow-loader.test.ts index a224fe5..33d6380 100644 --- a/src/__tests__/it-workflow-loader.test.ts +++ b/src/__tests__/it-workflow-loader.test.ts @@ -15,7 +15,7 @@ import { tmpdir } from 'node:os'; // --- Mocks --- -vi.mock('../config/global/globalConfig.js', () => ({ +vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), @@ -23,7 +23,7 @@ vi.mock('../config/global/globalConfig.js', () => ({ // --- Imports (after mocks) --- -import { loadWorkflow } from '../config/loaders/workflowLoader.js'; +import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js'; // --- Test helpers --- diff --git a/src/__tests__/it-workflow-patterns.test.ts b/src/__tests__/it-workflow-patterns.test.ts index 58f2ee6..d7f462e 100644 --- a/src/__tests__/it-workflow-patterns.test.ts +++ b/src/__tests__/it-workflow-patterns.test.ts @@ -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), runReportPhase: vi.fn().mockResolvedValue(undefined), runStatusJudgmentPhase: vi.fn().mockResolvedValue(''), })); -vi.mock('../utils/session.js', () => ({ +vi.mock('../shared/utils/reportDir.js', () => ({ generateReportDir: vi.fn().mockReturnValue('test-report-dir'), generateSessionId: vi.fn().mockReturnValue('test-session-id'), })); -vi.mock('../config/global/globalConfig.js', () => ({ +vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn().mockReturnValue({}), getLanguage: vi.fn().mockReturnValue('en'), getDisabledBuiltins: vi.fn().mockReturnValue([]), })); -vi.mock('../config/project/projectConfig.js', () => ({ +vi.mock('../infra/config/project/projectConfig.js', () => ({ loadProjectConfig: vi.fn().mockReturnValue({}), })); // --- Imports (after mocks) --- -import { WorkflowEngine } from '../workflow/engine/WorkflowEngine.js'; -import { loadWorkflow } from '../config/loaders/workflowLoader.js'; -import type { WorkflowConfig } from '../models/types.js'; +import { WorkflowEngine } from '../core/workflow/index.js'; +import { loadWorkflow } from '../infra/config/loaders/workflowLoader.js'; +import type { WorkflowConfig } from '../core/models/index.js'; // --- Test helpers --- diff --git a/src/__tests__/listTasks.test.ts b/src/__tests__/listTasks.test.ts index 7810b10..ecc1729 100644 --- a/src/__tests__/listTasks.test.ts +++ b/src/__tests__/listTasks.test.ts @@ -8,8 +8,8 @@ import { extractTaskSlug, buildListItems, type BranchInfo, -} from '../task/branchList.js'; -import { isBranchMerged, showFullDiff, type ListAction } from '../commands/management/listTasks.js'; +} from '../infra/task/branchList.js'; +import { isBranchMerged, showFullDiff, type ListAction } from '../features/tasks/index.js'; describe('parseTaktBranches', () => { it('should parse takt/ branches from git branch output', () => { diff --git a/src/__tests__/models.test.ts b/src/__tests__/models.test.ts index dcd9ef5..6a29e06 100644 --- a/src/__tests__/models.test.ts +++ b/src/__tests__/models.test.ts @@ -10,7 +10,7 @@ import { WorkflowConfigRawSchema, CustomAgentConfigSchema, GlobalConfigSchema, -} from '../models/schemas.js'; +} from '../core/models/index.js'; describe('AgentTypeSchema', () => { it('should accept valid agent types', () => { diff --git a/src/__tests__/parallel-and-loader.test.ts b/src/__tests__/parallel-and-loader.test.ts index b8a53e3..bf3e244 100644 --- a/src/__tests__/parallel-and-loader.test.ts +++ b/src/__tests__/parallel-and-loader.test.ts @@ -8,7 +8,7 @@ */ 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', () => { it('should validate a valid parallel sub-step', () => { diff --git a/src/__tests__/parallel-logger.test.ts b/src/__tests__/parallel-logger.test.ts index 9f454ac..04fb359 100644 --- a/src/__tests__/parallel-logger.test.ts +++ b/src/__tests__/parallel-logger.test.ts @@ -3,8 +3,8 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { ParallelLogger } from '../workflow/engine/parallel-logger.js'; -import type { StreamEvent } from '../claude/types.js'; +import { ParallelLogger } from '../core/workflow/index.js'; +import type { StreamEvent } from '../core/workflow/index.js'; describe('ParallelLogger', () => { let output: string[]; diff --git a/src/__tests__/paths.test.ts b/src/__tests__/paths.test.ts index 91c132a..bc209f9 100644 --- a/src/__tests__/paths.test.ts +++ b/src/__tests__/paths.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { isPathSafe } from '../config/paths.js'; +import { isPathSafe } from '../infra/config/paths.js'; describe('isPathSafe', () => { it('should accept paths within base directory', () => { diff --git a/src/__tests__/pipelineExecution.test.ts b/src/__tests__/pipelineExecution.test.ts index 2668a8f..e12f63b 100644 --- a/src/__tests__/pipelineExecution.test.ts +++ b/src/__tests__/pipelineExecution.test.ts @@ -9,7 +9,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock all external dependencies const mockFetchIssue = vi.fn(); const mockCheckGhCli = vi.fn().mockReturnValue({ available: true }); -vi.mock('../github/issue.js', () => ({ +vi.mock('../infra/github/issue.js', () => ({ fetchIssue: mockFetchIssue, formatIssueAsTask: vi.fn((issue: { title: string; body: string; number: number }) => `## GitHub Issue #${issue.number}: ${issue.title}\n\n${issue.body}` @@ -20,20 +20,20 @@ vi.mock('../github/issue.js', () => ({ const mockCreatePullRequest = vi.fn(); const mockPushBranch = vi.fn(); const mockBuildPrBody = vi.fn(() => 'Default PR body'); -vi.mock('../github/pr.js', () => ({ +vi.mock('../infra/github/pr.js', () => ({ createPullRequest: mockCreatePullRequest, pushBranch: mockPushBranch, buildPrBody: mockBuildPrBody, })); const mockExecuteTask = vi.fn(); -vi.mock('../commands/execution/taskExecution.js', () => ({ +vi.mock('../features/tasks/index.js', () => ({ executeTask: mockExecuteTask, })); // Mock loadGlobalConfig const mockLoadGlobalConfig = vi.fn(); -vi.mock('../config/global/globalConfig.js', async (importOriginal) => ({ ...(await importOriginal>()), +vi.mock('../infra/config/global/globalConfig.js', async (importOriginal) => ({ ...(await importOriginal>()), loadGlobalConfig: mockLoadGlobalConfig, })); @@ -44,7 +44,7 @@ vi.mock('node:child_process', () => ({ })); // Mock UI -vi.mock('../utils/ui.js', () => ({ +vi.mock('../shared/ui/index.js', () => ({ info: vi.fn(), error: vi.fn(), success: vi.fn(), @@ -56,7 +56,7 @@ vi.mock('../utils/ui.js', () => ({ debug: vi.fn(), })); // Mock debug logger -vi.mock('../utils/debug.js', () => ({ +vi.mock('../shared/utils/debug.js', () => ({ createLogger: () => ({ info: 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', () => { beforeEach(() => { diff --git a/src/__tests__/prompt.test.ts b/src/__tests__/prompt.test.ts index 67fd065..7efaa77 100644 --- a/src/__tests__/prompt.test.ts +++ b/src/__tests__/prompt.test.ts @@ -12,7 +12,7 @@ import { handleKeyInput, readMultilineFromStream, } 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 chalk.level = 0; diff --git a/src/__tests__/review-only-workflow.test.ts b/src/__tests__/review-only-workflow.test.ts index 617c790..9a65074 100644 --- a/src/__tests__/review-only-workflow.test.ts +++ b/src/__tests__/review-only-workflow.test.ts @@ -13,7 +13,7 @@ import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; 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'); diff --git a/src/__tests__/session.test.ts b/src/__tests__/session.test.ts index d4b07c1..183087e 100644 --- a/src/__tests__/session.test.ts +++ b/src/__tests__/session.test.ts @@ -19,7 +19,7 @@ import { type NdjsonStepComplete, type NdjsonWorkflowComplete, type NdjsonWorkflowAbort, -} from '../utils/session.js'; +} from '../infra/fs/session.js'; /** Create a temp project directory with .takt/logs structure */ function createTempProject(): string { diff --git a/src/__tests__/summarize.test.ts b/src/__tests__/summarize.test.ts index ef0c8d6..319891d 100644 --- a/src/__tests__/summarize.test.ts +++ b/src/__tests__/summarize.test.ts @@ -4,15 +4,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -vi.mock('../providers/index.js', () => ({ +vi.mock('../infra/providers/index.js', () => ({ getProvider: vi.fn(), })); -vi.mock('../config/global/globalConfig.js', () => ({ +vi.mock('../infra/config/global/globalConfig.js', () => ({ loadGlobalConfig: vi.fn(), })); -vi.mock('../utils/debug.js', () => ({ +vi.mock('../shared/utils/debug.js', () => ({ createLogger: () => ({ info: vi.fn(), debug: vi.fn(), @@ -20,9 +20,9 @@ vi.mock('../utils/debug.js', () => ({ }), })); -import { getProvider } from '../providers/index.js'; -import { loadGlobalConfig } from '../config/global/globalConfig.js'; -import { summarizeTaskName } from '../task/summarize.js'; +import { getProvider } from '../infra/providers/index.js'; +import { loadGlobalConfig } from '../infra/config/global/globalConfig.js'; +import { summarizeTaskName } from '../infra/task/summarize.js'; const mockGetProvider = vi.mocked(getProvider); const mockLoadGlobalConfig = vi.mocked(loadGlobalConfig); diff --git a/src/__tests__/task.test.ts b/src/__tests__/task.test.ts index dac5eae..8ec3ba7 100644 --- a/src/__tests__/task.test.ts +++ b/src/__tests__/task.test.ts @@ -5,8 +5,8 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; -import { TaskRunner } from '../task/runner.js'; -import { isTaskFile, parseTaskFiles } from '../task/parser.js'; +import { TaskRunner } from '../infra/task/runner.js'; +import { isTaskFile, parseTaskFiles } from '../infra/task/parser.js'; describe('isTaskFile', () => { it('should accept .yaml files', () => { diff --git a/src/__tests__/taskExecution.test.ts b/src/__tests__/taskExecution.test.ts index 173fab5..b9ef1d7 100644 --- a/src/__tests__/taskExecution.test.ts +++ b/src/__tests__/taskExecution.test.ts @@ -5,30 +5,30 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock dependencies before importing the module under test -vi.mock('../config/index.js', () => ({ +vi.mock('../infra/config/index.js', () => ({ loadWorkflowByIdentifier: vi.fn(), isWorkflowPath: vi.fn(() => false), loadGlobalConfig: vi.fn(() => ({})), })); -vi.mock('../task/index.js', () => ({ +vi.mock('../infra/task/index.js', () => ({ TaskRunner: vi.fn(), })); -vi.mock('../task/clone.js', () => ({ +vi.mock('../infra/task/clone.js', () => ({ createSharedClone: vi.fn(), removeClone: vi.fn(), })); -vi.mock('../task/autoCommit.js', () => ({ +vi.mock('../infra/task/autoCommit.js', () => ({ autoCommitAndPush: vi.fn(), })); -vi.mock('../task/summarize.js', () => ({ +vi.mock('../infra/task/summarize.js', () => ({ summarizeTaskName: vi.fn(), })); -vi.mock('../utils/ui.js', () => ({ +vi.mock('../shared/ui/index.js', () => ({ header: vi.fn(), info: vi.fn(), error: vi.fn(), @@ -37,7 +37,7 @@ vi.mock('../utils/ui.js', () => ({ blankLine: vi.fn(), })); -vi.mock('../utils/debug.js', () => ({ +vi.mock('../shared/utils/debug.js', () => ({ createLogger: () => ({ info: 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), })); -vi.mock('./workflowExecution.js', () => ({ +vi.mock('../features/tasks/execute/workflowExecution.js', () => ({ executeWorkflow: vi.fn(), })); @@ -62,11 +62,11 @@ vi.mock('../constants.js', () => ({ DEFAULT_LANGUAGE: 'en', })); -import { createSharedClone } from '../task/clone.js'; -import { summarizeTaskName } from '../task/summarize.js'; -import { info } from '../utils/ui.js'; -import { resolveTaskExecution } from '../commands/execution/taskExecution.js'; -import type { TaskInfo } from '../task/index.js'; +import { createSharedClone } from '../infra/task/clone.js'; +import { summarizeTaskName } from '../infra/task/summarize.js'; +import { info } from '../shared/ui/index.js'; +import { resolveTaskExecution } from '../features/tasks/index.js'; +import type { TaskInfo } from '../infra/task/index.js'; const mockCreateSharedClone = vi.mocked(createSharedClone); const mockSummarizeTaskName = vi.mocked(summarizeTaskName); diff --git a/src/__tests__/transitions.test.ts b/src/__tests__/transitions.test.ts index d81351f..4a0ba2c 100644 --- a/src/__tests__/transitions.test.ts +++ b/src/__tests__/transitions.test.ts @@ -3,8 +3,8 @@ */ import { describe, it, expect } from 'vitest'; -import { determineNextStepByRules } from '../workflow/engine/transitions.js'; -import type { WorkflowStep } from '../models/types.js'; +import { determineNextStepByRules } from '../core/workflow/index.js'; +import type { WorkflowStep } from '../core/models/index.js'; function createStepWithRules(rules: { condition: string; next: string }[]): WorkflowStep { return { diff --git a/src/__tests__/updateNotifier.test.ts b/src/__tests__/updateNotifier.test.ts index ee4e166..24f2846 100644 --- a/src/__tests__/updateNotifier.test.ts +++ b/src/__tests__/updateNotifier.test.ts @@ -24,7 +24,7 @@ vi.mock('node:module', () => { }; }); -import { checkForUpdates } from '../utils/updateNotifier.js'; +import { checkForUpdates } from '../shared/utils/updateNotifier.js'; beforeEach(() => { vi.clearAllMocks(); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index cb0077d..65080b9 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -3,8 +3,8 @@ */ import { describe, it, expect } from 'vitest'; -import { truncate, progressBar } from '../utils/ui.js'; -import { generateSessionId, createSessionLog } from '../utils/session.js'; +import { truncate, progressBar } from '../shared/ui/index.js'; +import { generateSessionId, createSessionLog } from '../infra/fs/session.js'; describe('truncate', () => { it('should not truncate short text', () => { diff --git a/src/__tests__/watcher.test.ts b/src/__tests__/watcher.test.ts index 228a6b6..4b48e19 100644 --- a/src/__tests__/watcher.test.ts +++ b/src/__tests__/watcher.test.ts @@ -5,8 +5,8 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs'; import { join } from 'node:path'; -import { TaskWatcher } from '../task/watcher.js'; -import type { TaskInfo } from '../task/types.js'; +import { TaskWatcher } from '../infra/task/watcher.js'; +import type { TaskInfo } from '../infra/task/types.js'; describe('TaskWatcher', () => { const testDir = `/tmp/takt-watcher-test-${Date.now()}`; diff --git a/src/__tests__/workflow-expert-parallel.test.ts b/src/__tests__/workflow-expert-parallel.test.ts index 2ddf36f..64e0245 100644 --- a/src/__tests__/workflow-expert-parallel.test.ts +++ b/src/__tests__/workflow-expert-parallel.test.ts @@ -11,7 +11,7 @@ */ 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', () => { const workflow = loadWorkflow('expert', process.cwd()); diff --git a/src/__tests__/workflowLoader.test.ts b/src/__tests__/workflowLoader.test.ts index 847a52a..71c87a2 100644 --- a/src/__tests__/workflowLoader.test.ts +++ b/src/__tests__/workflowLoader.test.ts @@ -11,7 +11,7 @@ import { loadWorkflowByIdentifier, listWorkflows, loadAllWorkflows, -} from '../config/loaders/workflowLoader.js'; +} from '../infra/config/loaders/workflowLoader.js'; const SAMPLE_WORKFLOW = `name: test-workflow description: Test workflow diff --git a/src/agents/runner.ts b/src/agents/runner.ts index a7945b1..cfc38e8 100644 --- a/src/agents/runner.ts +++ b/src/agents/runner.ts @@ -9,12 +9,12 @@ import { callClaudeSkill, type ClaudeCallOptions, } from '../claude/client.js'; -import { loadCustomAgents, loadAgentPrompt } from '../config/loaders/loader.js'; -import { loadGlobalConfig } from '../config/global/globalConfig.js'; -import { loadProjectConfig } from '../config/project/projectConfig.js'; -import { getProvider, type ProviderType, type ProviderCallOptions } from '../providers/index.js'; -import type { AgentResponse, CustomAgentConfig } from '../models/types.js'; -import { createLogger } from '../utils/debug.js'; +import { loadCustomAgents, loadAgentPrompt } from '../infra/config/loaders/loader.js'; +import { loadGlobalConfig } from '../infra/config/global/globalConfig.js'; +import { loadProjectConfig } from '../infra/config/project/projectConfig.js'; +import { getProvider, type ProviderType, type ProviderCallOptions } from '../infra/providers/index.js'; +import type { AgentResponse, CustomAgentConfig } from '../core/models/index.js'; +import { createLogger } from '../shared/utils/debug.js'; import type { RunAgentOptions } from './types.js'; // Re-export for backward compatibility diff --git a/src/agents/types.ts b/src/agents/types.ts index 6f33588..ea2a3d3 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -3,7 +3,7 @@ */ 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 }; diff --git a/src/cli/commands.ts b/src/app/cli/commands.ts similarity index 85% rename from src/cli/commands.ts rename to src/app/cli/commands.ts index 1078f98..720222a 100644 --- a/src/cli/commands.ts +++ b/src/app/cli/commands.ts @@ -4,17 +4,10 @@ * Registers all named subcommands (run, watch, add, list, switch, clear, eject, config). */ -import { clearAgentSessions, getCurrentWorkflow } from '../config/paths.js'; -import { success } from '../utils/ui.js'; -import { - runAllTasks, - switchWorkflow, - switchConfig, - addTask, - ejectBuiltin, - watchTasks, - listTasks, -} from '../commands/index.js'; +import { clearAgentSessions, getCurrentWorkflow } from '../../infra/config/paths.js'; +import { success } from '../../shared/ui/index.js'; +import { runAllTasks, addTask, watchTasks, listTasks } from '../../features/tasks/index.js'; +import { switchWorkflow, switchConfig, ejectBuiltin } from '../../features/config/index.js'; import { program, resolvedCwd } from './program.js'; import { resolveAgentOverrides } from './helpers.js'; diff --git a/src/cli/helpers.ts b/src/app/cli/helpers.ts similarity index 86% rename from src/cli/helpers.ts rename to src/app/cli/helpers.ts index 94602d9..6be2454 100644 --- a/src/cli/helpers.ts +++ b/src/app/cli/helpers.ts @@ -5,10 +5,10 @@ */ import type { Command } from 'commander'; -import type { TaskExecutionOptions } from '../commands/execution/types.js'; -import type { ProviderType } from '../providers/index.js'; -import { error } from '../utils/ui.js'; -import { isIssueReference } from '../github/issue.js'; +import type { TaskExecutionOptions } from '../../features/tasks/index.js'; +import type { ProviderType } from '../../infra/providers/index.js'; +import { error } from '../../shared/ui/index.js'; +import { isIssueReference } from '../../infra/github/issue.js'; /** * Resolve --provider and --model options into TaskExecutionOptions. diff --git a/src/cli/index.ts b/src/app/cli/index.ts similarity index 80% rename from src/cli/index.ts rename to src/app/cli/index.ts index b4a903f..cf7a473 100644 --- a/src/cli/index.ts +++ b/src/app/cli/index.ts @@ -6,7 +6,7 @@ * Import order matters: program setup → commands → routing → parse. */ -import { checkForUpdates } from '../utils/updateNotifier.js'; +import { checkForUpdates } from '../../shared/utils/updateNotifier.js'; checkForUpdates(); diff --git a/src/cli/program.ts b/src/app/cli/program.ts similarity index 91% rename from src/cli/program.ts rename to src/app/cli/program.ts index 1fc7240..417b167 100644 --- a/src/cli/program.ts +++ b/src/app/cli/program.ts @@ -14,13 +14,13 @@ import { loadGlobalConfig, getEffectiveDebugConfig, isVerboseMode, -} from '../config/index.js'; -import { setQuietMode } from '../context.js'; -import { setLogLevel } from '../utils/ui.js'; -import { initDebugLogger, createLogger, setVerboseConsole } from '../utils/debug.js'; +} from '../../infra/config/index.js'; +import { setQuietMode } from '../../context.js'; +import { setLogLevel } from '../../shared/ui/index.js'; +import { initDebugLogger, createLogger, setVerboseConsole } from '../../shared/utils/debug.js'; 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'); diff --git a/src/cli/routing.ts b/src/app/cli/routing.ts similarity index 86% rename from src/cli/routing.ts rename to src/app/cli/routing.ts index aa9abee..d2b15fa 100644 --- a/src/cli/routing.ts +++ b/src/app/cli/routing.ts @@ -5,13 +5,13 @@ * pipeline mode, or interactive mode. */ -import { info, error } from '../utils/ui.js'; -import { getErrorMessage } from '../utils/error.js'; -import { resolveIssueTask, isIssueReference } from '../github/issue.js'; -import { selectAndExecuteTask } from '../commands/execution/selectAndExecute.js'; -import { executePipeline, interactiveMode } from '../commands/index.js'; -import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; -import type { SelectAndExecuteOptions } from '../commands/execution/types.js'; +import { info, error } from '../../shared/ui/index.js'; +import { getErrorMessage } from '../../shared/utils/error.js'; +import { resolveIssueTask, isIssueReference } from '../../infra/github/issue.js'; +import { selectAndExecuteTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js'; +import { executePipeline } from '../../features/pipeline/index.js'; +import { interactiveMode } from '../../features/interactive/index.js'; +import { DEFAULT_WORKFLOW_NAME } from '../../constants.js'; import { program, resolvedCwd, pipelineMode } from './program.js'; import { resolveAgentOverrides, parseCreateWorktreeOption, isDirectTask } from './helpers.js'; diff --git a/src/claude/client.ts b/src/claude/client.ts index 6e6cd40..ad01cb4 100644 --- a/src/claude/client.ts +++ b/src/claude/client.ts @@ -6,8 +6,8 @@ import { executeClaudeCli } from './process.js'; import type { ClaudeSpawnOptions, ClaudeCallOptions } from './types.js'; -import type { AgentResponse, Status } from '../models/types.js'; -import { createLogger } from '../utils/debug.js'; +import type { AgentResponse, Status } from '../core/models/index.js'; +import { createLogger } from '../shared/utils/debug.js'; // Re-export for backward compatibility export type { ClaudeCallOptions } from './types.js'; diff --git a/src/claude/executor.ts b/src/claude/executor.ts index f94416a..b2251ce 100644 --- a/src/claude/executor.ts +++ b/src/claude/executor.ts @@ -11,8 +11,8 @@ import { type SDKResultMessage, type SDKAssistantMessage, } from '@anthropic-ai/claude-agent-sdk'; -import { createLogger } from '../utils/debug.js'; -import { getErrorMessage } from '../utils/error.js'; +import { createLogger } from '../shared/utils/debug.js'; +import { getErrorMessage } from '../shared/utils/error.js'; import { generateQueryId, registerQuery, diff --git a/src/claude/options-builder.ts b/src/claude/options-builder.ts index f5fa699..d9aa6da 100644 --- a/src/claude/options-builder.ts +++ b/src/claude/options-builder.ts @@ -16,7 +16,7 @@ import type { PreToolUseHookInput, PermissionMode, } from '@anthropic-ai/claude-agent-sdk'; -import { createLogger } from '../utils/debug.js'; +import { createLogger } from '../shared/utils/debug.js'; import type { PermissionHandler, AskUserQuestionInput, diff --git a/src/claude/types.ts b/src/claude/types.ts index 754f434..c186985 100644 --- a/src/claude/types.ts +++ b/src/claude/types.ts @@ -6,7 +6,7 @@ */ 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 export type { PermissionResult, PermissionUpdate }; diff --git a/src/codex/client.ts b/src/codex/client.ts index 76859bd..be0008d 100644 --- a/src/codex/client.ts +++ b/src/codex/client.ts @@ -5,9 +5,9 @@ */ import { Codex } from '@openai/codex-sdk'; -import type { AgentResponse } from '../models/types.js'; -import { createLogger } from '../utils/debug.js'; -import { getErrorMessage } from '../utils/error.js'; +import type { AgentResponse } from '../core/models/index.js'; +import { createLogger } from '../shared/utils/debug.js'; +import { getErrorMessage } from '../shared/utils/error.js'; import type { CodexCallOptions } from './types.js'; import { type CodexEvent, diff --git a/src/commands/execution/index.ts b/src/commands/execution/index.ts deleted file mode 100644 index 1baecc4..0000000 --- a/src/commands/execution/index.ts +++ /dev/null @@ -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'; diff --git a/src/commands/index.ts b/src/commands/index.ts deleted file mode 100644 index bb41bc9..0000000 --- a/src/commands/index.ts +++ /dev/null @@ -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'; diff --git a/src/commands/interactive/interactive.ts b/src/commands/interactive/interactive.ts deleted file mode 100644 index 10c362b..0000000 --- a/src/commands/interactive/interactive.ts +++ /dev/null @@ -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 { - 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, - prompt: string, - cwd: string, - model: string | undefined, - sessionId: string | undefined, - display: StreamDisplay, -): Promise { - 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 { - 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 { - 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(); - } - } -} diff --git a/src/commands/management/index.ts b/src/commands/management/index.ts deleted file mode 100644 index 662cae9..0000000 --- a/src/commands/management/index.ts +++ /dev/null @@ -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'; diff --git a/src/constants.ts b/src/constants.ts index 8ce34cc..52faf18 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,7 +2,7 @@ * Application-wide constants */ -import type { Language } from './models/types.js'; +import type { Language } from './core/models/index.js'; /** Default workflow name when none specified */ export const DEFAULT_WORKFLOW_NAME = 'default'; diff --git a/src/models/agent.ts b/src/core/models/agent.ts similarity index 100% rename from src/models/agent.ts rename to src/core/models/agent.ts diff --git a/src/models/config.ts b/src/core/models/config.ts similarity index 100% rename from src/models/config.ts rename to src/core/models/config.ts diff --git a/src/models/global-config.ts b/src/core/models/global-config.ts similarity index 100% rename from src/models/global-config.ts rename to src/core/models/global-config.ts diff --git a/src/models/index.ts b/src/core/models/index.ts similarity index 87% rename from src/models/index.ts rename to src/core/models/index.ts index 915df0d..24b2ecb 100644 --- a/src/models/index.ts +++ b/src/core/models/index.ts @@ -3,14 +3,20 @@ export type { AgentType, Status, RuleMatchMethod, + PermissionMode, ReportConfig, ReportObjectConfig, AgentResponse, SessionState, + WorkflowRule, WorkflowStep, + LoopDetectionConfig, WorkflowConfig, WorkflowState, CustomAgentConfig, + DebugConfig, + Language, + PipelineConfig, GlobalConfig, ProjectConfig, } from './types.js'; diff --git a/src/models/response.ts b/src/core/models/response.ts similarity index 100% rename from src/models/response.ts rename to src/core/models/response.ts diff --git a/src/models/schemas.ts b/src/core/models/schemas.ts similarity index 98% rename from src/models/schemas.ts rename to src/core/models/schemas.ts index 3f1d7d9..ee9ef83 100644 --- a/src/models/schemas.ts +++ b/src/core/models/schemas.ts @@ -5,7 +5,7 @@ */ import { z } from 'zod/v4'; -import { DEFAULT_LANGUAGE } from '../constants.js'; +import { DEFAULT_LANGUAGE } from '../../constants.js'; /** Agent model schema (opus, sonnet, haiku) */ export const AgentModelSchema = z.enum(['opus', 'sonnet', 'haiku']).default('sonnet'); @@ -131,6 +131,8 @@ export const WorkflowStepRawSchema = z.object({ name: z.string().min(1), /** Agent is required for normal steps, optional for parallel container steps */ 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 */ agent_name: z.string().optional(), allowed_tools: z.array(z.string()).optional(), @@ -224,4 +226,3 @@ export const ProjectConfigSchema = z.object({ agents: z.array(CustomAgentConfigSchema).optional(), provider: z.enum(['claude', 'codex', 'mock']).optional(), }); - diff --git a/src/models/session.ts b/src/core/models/session.ts similarity index 100% rename from src/models/session.ts rename to src/core/models/session.ts diff --git a/src/models/status.ts b/src/core/models/status.ts similarity index 100% rename from src/models/status.ts rename to src/core/models/status.ts diff --git a/src/models/types.ts b/src/core/models/types.ts similarity index 100% rename from src/models/types.ts rename to src/core/models/types.ts diff --git a/src/models/workflow-types.ts b/src/core/models/workflow-types.ts similarity index 98% rename from src/models/workflow-types.ts rename to src/core/models/workflow-types.ts index 8e94b8c..dd8dbb9 100644 --- a/src/models/workflow-types.ts +++ b/src/core/models/workflow-types.ts @@ -48,6 +48,8 @@ export interface WorkflowStep { name: string; /** Agent name or path as specified in workflow YAML */ 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 */ agentDisplayName: string; /** Allowed tools for this step (optional, passed to agent execution) */ diff --git a/src/workflow/constants.ts b/src/core/workflow/constants.ts similarity index 100% rename from src/workflow/constants.ts rename to src/core/workflow/constants.ts diff --git a/src/workflow/engine/OptionsBuilder.ts b/src/core/workflow/engine/OptionsBuilder.ts similarity index 88% rename from src/workflow/engine/OptionsBuilder.ts rename to src/core/workflow/engine/OptionsBuilder.ts index 4bcb543..c2024da 100644 --- a/src/workflow/engine/OptionsBuilder.ts +++ b/src/core/workflow/engine/OptionsBuilder.ts @@ -7,14 +7,15 @@ import { join } from 'node:path'; import type { WorkflowStep, WorkflowState, Language } from '../../models/types.js'; -import type { RunAgentOptions } from '../../agents/runner.js'; -import type { PhaseRunnerContext } from './phase-runner.js'; +import type { RunAgentOptions } from '../../../agents/runner.js'; +import type { PhaseRunnerContext } from '../phase-runner.js'; import type { WorkflowEngineOptions } from '../types.js'; export class OptionsBuilder { constructor( private readonly engineOptions: WorkflowEngineOptions, private readonly getCwd: () => string, + private readonly getProjectCwd: () => string, private readonly getSessionId: (agent: string) => string | undefined, private readonly getReportDir: () => string, private readonly getLanguage: () => Language | undefined, @@ -44,7 +45,7 @@ export class OptionsBuilder { return { ...this.buildBaseOptions(step), - sessionId: this.getSessionId(step.agent), + sessionId: step.session === 'refresh' ? undefined : this.getSessionId(step.agent), allowedTools, }; } @@ -70,7 +71,7 @@ export class OptionsBuilder { ): PhaseRunnerContext { return { cwd: this.getCwd(), - reportDir: join(this.getCwd(), this.getReportDir()), + reportDir: join(this.getProjectCwd(), this.getReportDir()), language: this.getLanguage(), getSessionId: (agent: string) => state.agentSessions.get(agent), buildResumeOptions: this.buildResumeOptions.bind(this), diff --git a/src/workflow/engine/ParallelRunner.ts b/src/core/workflow/engine/ParallelRunner.ts similarity index 97% rename from src/workflow/engine/ParallelRunner.ts rename to src/core/workflow/engine/ParallelRunner.ts index 1e2f84e..5e2d3ae 100644 --- a/src/workflow/engine/ParallelRunner.ts +++ b/src/core/workflow/engine/ParallelRunner.ts @@ -10,12 +10,12 @@ import type { WorkflowState, AgentResponse, } from '../../models/types.js'; -import { runAgent } from '../../agents/runner.js'; +import { runAgent } from '../../../agents/runner.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 { 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 { StepExecutor } from './StepExecutor.js'; import type { WorkflowEngineOptions } from '../types.js'; diff --git a/src/workflow/engine/StepExecutor.ts b/src/core/workflow/engine/StepExecutor.ts similarity index 97% rename from src/workflow/engine/StepExecutor.ts rename to src/core/workflow/engine/StepExecutor.ts index 987c7ac..fb9ba74 100644 --- a/src/workflow/engine/StepExecutor.ts +++ b/src/core/workflow/engine/StepExecutor.ts @@ -14,12 +14,12 @@ import type { AgentResponse, Language, } from '../../models/types.js'; -import { runAgent } from '../../agents/runner.js'; +import { runAgent } from '../../../agents/runner.js'; import { InstructionBuilder, isReportObjectConfig } from '../instruction/InstructionBuilder.js'; -import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from './phase-runner.js'; +import { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from '../phase-runner.js'; import { detectMatchedRule } from '../evaluation/index.js'; import { incrementStepIteration, getPreviousOutput } from './state-manager.js'; -import { createLogger } from '../../utils/debug.js'; +import { createLogger } from '../../../shared/utils/debug.js'; import type { OptionsBuilder } from './OptionsBuilder.js'; const log = createLogger('step-executor'); diff --git a/src/workflow/engine/WorkflowEngine.ts b/src/core/workflow/engine/WorkflowEngine.ts similarity index 97% rename from src/workflow/engine/WorkflowEngine.ts rename to src/core/workflow/engine/WorkflowEngine.ts index 7409e36..2cccf91 100644 --- a/src/workflow/engine/WorkflowEngine.ts +++ b/src/core/workflow/engine/WorkflowEngine.ts @@ -25,10 +25,10 @@ import { addUserInput as addUserInputToState, incrementStepIteration, } from './state-manager.js'; -import { generateReportDir } from '../../utils/session.js'; -import { getErrorMessage } from '../../utils/error.js'; -import { createLogger } from '../../utils/debug.js'; -import { interruptAllQueries } from '../../claude/query-manager.js'; +import { generateReportDir } from '../../../shared/utils/reportDir.js'; +import { getErrorMessage } from '../../../shared/utils/error.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import { interruptAllQueries } from '../../../claude/query-manager.js'; import { OptionsBuilder } from './OptionsBuilder.js'; import { StepExecutor } from './StepExecutor.js'; import { ParallelRunner } from './ParallelRunner.js'; @@ -79,6 +79,7 @@ export class WorkflowEngine extends EventEmitter { this.optionsBuilder = new OptionsBuilder( options, () => this.cwd, + () => this.projectCwd, (agent) => this.state.agentSessions.get(agent), () => this.reportDir, () => this.options.language, diff --git a/src/workflow/engine/blocked-handler.ts b/src/core/workflow/engine/blocked-handler.ts similarity index 100% rename from src/workflow/engine/blocked-handler.ts rename to src/core/workflow/engine/blocked-handler.ts diff --git a/src/workflow/engine/index.ts b/src/core/workflow/engine/index.ts similarity index 100% rename from src/workflow/engine/index.ts rename to src/core/workflow/engine/index.ts diff --git a/src/workflow/engine/loop-detector.ts b/src/core/workflow/engine/loop-detector.ts similarity index 100% rename from src/workflow/engine/loop-detector.ts rename to src/core/workflow/engine/loop-detector.ts diff --git a/src/workflow/engine/parallel-logger.ts b/src/core/workflow/engine/parallel-logger.ts similarity index 98% rename from src/workflow/engine/parallel-logger.ts rename to src/core/workflow/engine/parallel-logger.ts index 5160868..f7b28be 100644 --- a/src/workflow/engine/parallel-logger.ts +++ b/src/core/workflow/engine/parallel-logger.ts @@ -6,7 +6,7 @@ * aligned to the longest sub-step name. */ -import type { StreamCallback, StreamEvent } from '../../claude/types.js'; +import type { StreamCallback, StreamEvent } from '../types.js'; /** ANSI color codes for sub-step prefixes (cycled in order) */ const COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const; // cyan, yellow, magenta, green diff --git a/src/workflow/engine/state-manager.ts b/src/core/workflow/engine/state-manager.ts similarity index 100% rename from src/workflow/engine/state-manager.ts rename to src/core/workflow/engine/state-manager.ts diff --git a/src/workflow/engine/transitions.ts b/src/core/workflow/engine/transitions.ts similarity index 100% rename from src/workflow/engine/transitions.ts rename to src/core/workflow/engine/transitions.ts diff --git a/src/workflow/evaluation/AggregateEvaluator.ts b/src/core/workflow/evaluation/AggregateEvaluator.ts similarity index 97% rename from src/workflow/evaluation/AggregateEvaluator.ts rename to src/core/workflow/evaluation/AggregateEvaluator.ts index d0b1b26..008d52b 100644 --- a/src/workflow/evaluation/AggregateEvaluator.ts +++ b/src/core/workflow/evaluation/AggregateEvaluator.ts @@ -5,7 +5,7 @@ */ import type { WorkflowStep, WorkflowState } from '../../models/types.js'; -import { createLogger } from '../../utils/debug.js'; +import { createLogger } from '../../../shared/utils/debug.js'; const log = createLogger('aggregate-evaluator'); diff --git a/src/workflow/evaluation/RuleEvaluator.ts b/src/core/workflow/evaluation/RuleEvaluator.ts similarity index 97% rename from src/workflow/evaluation/RuleEvaluator.ts rename to src/core/workflow/evaluation/RuleEvaluator.ts index 0b3c0b7..a944913 100644 --- a/src/workflow/evaluation/RuleEvaluator.ts +++ b/src/core/workflow/evaluation/RuleEvaluator.ts @@ -11,8 +11,8 @@ import type { WorkflowState, RuleMatchMethod, } from '../../models/types.js'; -import { detectRuleIndex, callAiJudge } from '../../claude/client.js'; -import { createLogger } from '../../utils/debug.js'; +import { detectRuleIndex, callAiJudge } from '../../../claude/client.js'; +import { createLogger } from '../../../shared/utils/debug.js'; import { AggregateEvaluator } from './AggregateEvaluator.js'; const log = createLogger('rule-evaluator'); @@ -157,4 +157,5 @@ export class RuleEvaluator { log.debug('AI judge (fallback) did not match any condition', { step: this.step.name }); return -1; } + } diff --git a/src/workflow/evaluation/index.ts b/src/core/workflow/evaluation/index.ts similarity index 100% rename from src/workflow/evaluation/index.ts rename to src/core/workflow/evaluation/index.ts diff --git a/src/workflow/evaluation/rule-utils.ts b/src/core/workflow/evaluation/rule-utils.ts similarity index 100% rename from src/workflow/evaluation/rule-utils.ts rename to src/core/workflow/evaluation/rule-utils.ts diff --git a/src/workflow/index.ts b/src/core/workflow/index.ts similarity index 81% rename from src/workflow/index.ts rename to src/core/workflow/index.ts index 85d99e0..b23987e 100644 --- a/src/workflow/index.ts +++ b/src/core/workflow/index.ts @@ -20,6 +20,11 @@ export type { IterationLimitCallback, WorkflowEngineOptions, LoopCheckResult, + StreamEvent, + StreamCallback, + PermissionHandler, + AskUserQuestionHandler, + ProviderType, } from './types.js'; // Transitions (engine/) @@ -38,12 +43,19 @@ export { // Blocked handling (engine/) export { handleBlocked, type BlockedHandlerResult } from './engine/blocked-handler.js'; +// Parallel logger (engine/) +export { ParallelLogger } from './engine/parallel-logger.js'; + // Instruction building export { InstructionBuilder, isReportObjectConfig } from './instruction/InstructionBuilder.js'; export { ReportInstructionBuilder, type ReportInstructionContext } from './instruction/ReportInstructionBuilder.js'; export { StatusJudgmentBuilder, type StatusJudgmentContext } from './instruction/StatusJudgmentBuilder.js'; export { buildExecutionMetadata, renderExecutionMetadata, type InstructionContext, type ExecutionMetadata } from './instruction/instruction-context.js'; +export { generateStatusRulesFromRules } from './instruction/status-rules.js'; // Rule evaluation export { RuleEvaluator, type RuleMatch, type RuleEvaluatorContext, detectMatchedRule, evaluateAggregateConditions } from './evaluation/index.js'; export { AggregateEvaluator } from './evaluation/AggregateEvaluator.js'; + +// Phase runner +export { needsStatusJudgmentPhase, runReportPhase, runStatusJudgmentPhase } from './phase-runner.js'; diff --git a/src/workflow/instruction/InstructionBuilder.ts b/src/core/workflow/instruction/InstructionBuilder.ts similarity index 100% rename from src/workflow/instruction/InstructionBuilder.ts rename to src/core/workflow/instruction/InstructionBuilder.ts diff --git a/src/workflow/instruction/ReportInstructionBuilder.ts b/src/core/workflow/instruction/ReportInstructionBuilder.ts similarity index 88% rename from src/workflow/instruction/ReportInstructionBuilder.ts rename to src/core/workflow/instruction/ReportInstructionBuilder.ts index 0960102..4c5588e 100644 --- a/src/workflow/instruction/ReportInstructionBuilder.ts +++ b/src/core/workflow/instruction/ReportInstructionBuilder.ts @@ -20,11 +20,15 @@ import { isReportObjectConfig, renderReportContext, renderReportOutputInstructio const REPORT_PHASE_STRINGS = { en: { noSourceEdit: '**Do NOT modify project source files.** Only output report files.', + reportDirOnly: '**Use only the Report Directory files shown above.** Do not search or open reports outside that directory.', instructionBody: 'Output the results of your previous work as a report.', + reportJsonFormat: 'Output a JSON object mapping each report file name to its content.', }, ja: { noSourceEdit: '**プロジェクトのソースファイルを変更しないでください。** レポートファイルのみ出力してください。', + reportDirOnly: '**上記のReport Directory内のファイルのみ使用してください。** 他のレポートディレクトリは検索/参照しないでください。', instructionBody: '前のステップの作業結果をレポートとして出力してください。', + reportJsonFormat: 'レポートファイル名→内容のJSONオブジェクトで出力してください。', }, } as const; @@ -77,6 +81,7 @@ export class ReportInstructionBuilder { `- ${m.noCommit}`, `- ${m.noCd}`, `- ${r.noSourceEdit}`, + `- ${r.reportDirOnly}`, ]; if (m.note) { execLines.push(''); @@ -96,6 +101,7 @@ export class ReportInstructionBuilder { const instrParts: string[] = [ s.instructions, r.instructionBody, + r.reportJsonFormat, ]; // Report output instruction (auto-generated or explicit order) diff --git a/src/workflow/instruction/StatusJudgmentBuilder.ts b/src/core/workflow/instruction/StatusJudgmentBuilder.ts similarity index 100% rename from src/workflow/instruction/StatusJudgmentBuilder.ts rename to src/core/workflow/instruction/StatusJudgmentBuilder.ts diff --git a/src/workflow/instruction/escape.ts b/src/core/workflow/instruction/escape.ts similarity index 100% rename from src/workflow/instruction/escape.ts rename to src/core/workflow/instruction/escape.ts diff --git a/src/workflow/instruction/index.ts b/src/core/workflow/instruction/index.ts similarity index 100% rename from src/workflow/instruction/index.ts rename to src/core/workflow/instruction/index.ts diff --git a/src/workflow/instruction/instruction-context.ts b/src/core/workflow/instruction/instruction-context.ts similarity index 100% rename from src/workflow/instruction/instruction-context.ts rename to src/core/workflow/instruction/instruction-context.ts diff --git a/src/workflow/instruction/status-rules.ts b/src/core/workflow/instruction/status-rules.ts similarity index 100% rename from src/workflow/instruction/status-rules.ts rename to src/core/workflow/instruction/status-rules.ts diff --git a/src/core/workflow/parallel-logger.ts b/src/core/workflow/parallel-logger.ts new file mode 100644 index 0000000..e6fed9e --- /dev/null +++ b/src/core/workflow/parallel-logger.ts @@ -0,0 +1,5 @@ +/** + * Public re-export for ParallelLogger. + */ + +export { ParallelLogger } from './engine/parallel-logger.js'; diff --git a/src/workflow/engine/phase-runner.ts b/src/core/workflow/phase-runner.ts similarity index 51% rename from src/workflow/engine/phase-runner.ts rename to src/core/workflow/phase-runner.ts index cbfb518..6ab0ec5 100644 --- a/src/workflow/engine/phase-runner.ts +++ b/src/core/workflow/phase-runner.ts @@ -5,12 +5,15 @@ * as session-resume operations. */ -import type { WorkflowStep, Language } from '../../models/types.js'; +import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, resolve, sep } from 'node:path'; +import type { WorkflowStep, Language } from '../models/types.js'; import { runAgent, type RunAgentOptions } from '../../agents/runner.js'; -import { ReportInstructionBuilder } from '../instruction/ReportInstructionBuilder.js'; -import { StatusJudgmentBuilder } from '../instruction/StatusJudgmentBuilder.js'; -import { hasTagBasedRules } from '../evaluation/rule-utils.js'; -import { createLogger } from '../../utils/debug.js'; +import { ReportInstructionBuilder } from './instruction/ReportInstructionBuilder.js'; +import { StatusJudgmentBuilder } from './instruction/StatusJudgmentBuilder.js'; +import { hasTagBasedRules } from './evaluation/rule-utils.js'; +import { isReportObjectConfig } from './instruction/InstructionBuilder.js'; +import { createLogger } from '../../shared/utils/debug.js'; const log = createLogger('phase-runner'); @@ -37,10 +40,82 @@ export function needsStatusJudgmentPhase(step: WorkflowStep): boolean { return hasTagBasedRules(step); } +function extractJsonPayload(content: string): string | null { + const trimmed = content.trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + return trimmed; + } + const match = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i); + return match ? match[1]!.trim() : null; +} + +function parseReportJson(content: string): Record | null { + const payload = extractJsonPayload(content); + if (!payload) return null; + try { + const parsed = JSON.parse(payload); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + const obj = parsed as Record; + for (const value of Object.values(obj)) { + if (typeof value !== 'string') return null; + } + return obj as Record; + } + return null; + } catch { + return null; + } +} + +function getReportFiles(report: WorkflowStep['report']): string[] { + if (!report) return []; + if (typeof report === 'string') return [report]; + if (isReportObjectConfig(report)) return [report.name]; + return report.map((rc) => rc.path); +} + +function resolveReportOutputs( + report: WorkflowStep['report'], + content: string, +): Map { + if (!report) return new Map(); + + const files = getReportFiles(report); + const json = parseReportJson(content); + if (!json) { + throw new Error('Report output must be a JSON object mapping report file names to content.'); + } + + const outputs = new Map(); + for (const file of files) { + const value = json[file]; + if (typeof value !== 'string') { + throw new Error(`Report output missing content for file: ${file}`); + } + outputs.set(file, value); + } + return outputs; +} + +function writeReportFile(reportDir: string, fileName: string, content: string): void { + const baseDir = resolve(reportDir); + const targetPath = resolve(reportDir, fileName); + const basePrefix = baseDir.endsWith(sep) ? baseDir : baseDir + sep; + if (!targetPath.startsWith(basePrefix)) { + throw new Error(`Report file path escapes report directory: ${fileName}`); + } + mkdirSync(dirname(targetPath), { recursive: true }); + if (existsSync(targetPath)) { + appendFileSync(targetPath, `\n\n${content}`); + } else { + writeFileSync(targetPath, content); + } +} + /** * Phase 2: Report output. - * Resumes the agent session with Write-only tools to output reports. - * The response is discarded — only sessionId is updated. + * Resumes the agent session with no tools to request report content. + * The engine writes the report files to the Report Directory. */ export async function runReportPhase( step: WorkflowStep, @@ -62,11 +137,15 @@ export async function runReportPhase( }).build(); const reportOptions = ctx.buildResumeOptions(step, sessionId, { - allowedTools: ['Write'], + allowedTools: [], maxTurns: 3, }); const reportResponse = await runAgent(step.agent, reportInstruction, reportOptions); + const outputs = resolveReportOutputs(step.report, reportResponse.content); + for (const [fileName, content] of outputs.entries()) { + writeReportFile(ctx.reportDir, fileName, content); + } // Update session (phase 2 may update it) ctx.updateAgentSession(step.agent, reportResponse.sessionId); diff --git a/src/workflow/types.ts b/src/core/workflow/types.ts similarity index 63% rename from src/workflow/types.ts rename to src/core/workflow/types.ts index 8076d93..dbf87f0 100644 --- a/src/workflow/types.ts +++ b/src/core/workflow/types.ts @@ -6,9 +6,88 @@ */ import type { WorkflowStep, AgentResponse, WorkflowState, Language } from '../models/types.js'; -import type { StreamCallback } from '../agents/runner.js'; -import type { PermissionHandler, AskUserQuestionHandler } from '../claude/process.js'; -import type { ProviderType } from '../providers/index.js'; +import type { PermissionResult } from '../../claude/types.js'; + +export type ProviderType = 'claude' | 'codex' | 'mock'; + +export interface StreamInitEventData { + model: string; + sessionId: string; +} + +export interface StreamToolUseEventData { + tool: string; + input: Record; + id: string; +} + +export interface StreamToolResultEventData { + content: string; + isError: boolean; +} + +export interface StreamToolOutputEventData { + tool: string; + output: string; +} + +export interface StreamTextEventData { + text: string; +} + +export interface StreamThinkingEventData { + thinking: string; +} + +export interface StreamResultEventData { + result: string; + sessionId: string; + success: boolean; + error?: string; +} + +export interface StreamErrorEventData { + message: string; + raw?: string; +} + +export type StreamEvent = + | { type: 'init'; data: StreamInitEventData } + | { type: 'tool_use'; data: StreamToolUseEventData } + | { type: 'tool_result'; data: StreamToolResultEventData } + | { type: 'tool_output'; data: StreamToolOutputEventData } + | { type: 'text'; data: StreamTextEventData } + | { type: 'thinking'; data: StreamThinkingEventData } + | { type: 'result'; data: StreamResultEventData } + | { type: 'error'; data: StreamErrorEventData }; + +export type StreamCallback = (event: StreamEvent) => void; + +export interface PermissionRequest { + toolName: string; + input: Record; + suggestions?: Array>; + blockedPath?: string; + decisionReason?: string; +} + +export type PermissionHandler = (request: PermissionRequest) => Promise; + +export interface AskUserQuestionInput { + questions: Array<{ + question: string; + header?: string; + options?: Array<{ + label: string; + description?: string; + }>; + multiSelect?: boolean; + }>; +} + +export type AskUserQuestionHandler = ( + input: AskUserQuestionInput +) => Promise>; /** Events emitted by workflow engine */ export interface WorkflowEvents { diff --git a/src/commands/management/eject.ts b/src/features/config/ejectBuiltin.ts similarity index 95% rename from src/commands/management/eject.ts rename to src/features/config/ejectBuiltin.ts index 0511882..9b46c01 100644 --- a/src/commands/management/eject.ts +++ b/src/features/config/ejectBuiltin.ts @@ -7,9 +7,9 @@ import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; -import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../../config/paths.js'; -import { getLanguage } from '../../config/global/globalConfig.js'; -import { header, success, info, warn, error, blankLine } from '../../utils/ui.js'; +import { getGlobalWorkflowsDir, getGlobalAgentsDir, getBuiltinWorkflowsDir, getBuiltinAgentsDir } from '../../infra/config/paths.js'; +import { getLanguage } from '../../infra/config/global/globalConfig.js'; +import { header, success, info, warn, error, blankLine } from '../../shared/ui/index.js'; /** * Eject a builtin workflow to user space for customization. diff --git a/src/features/config/index.ts b/src/features/config/index.ts new file mode 100644 index 0000000..32676d9 --- /dev/null +++ b/src/features/config/index.ts @@ -0,0 +1,7 @@ +/** + * Config feature exports + */ + +export { switchWorkflow } from './switchWorkflow.js'; +export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './switchConfig.js'; +export { ejectBuiltin } from './ejectBuiltin.js'; diff --git a/src/commands/management/config.ts b/src/features/config/switchConfig.ts similarity index 95% rename from src/commands/management/config.ts rename to src/features/config/switchConfig.ts index 99dfd72..8c74847 100644 --- a/src/commands/management/config.ts +++ b/src/features/config/switchConfig.ts @@ -6,16 +6,16 @@ */ import chalk from 'chalk'; -import { info, success } from '../../utils/ui.js'; +import { info, success } from '../../shared/ui/index.js'; import { selectOption } from '../../prompt/index.js'; import { loadProjectConfig, updateProjectConfig, type PermissionMode, -} from '../../config/project/projectConfig.js'; +} from '../../infra/config/project/projectConfig.js'; // Re-export for convenience -export type { PermissionMode } from '../../config/project/projectConfig.js'; +export type { PermissionMode } from '../../infra/config/project/projectConfig.js'; /** * Get permission mode options for selection diff --git a/src/commands/management/workflow.ts b/src/features/config/switchWorkflow.ts similarity index 85% rename from src/commands/management/workflow.ts rename to src/features/config/switchWorkflow.ts index c75d1d8..1d2091c 100644 --- a/src/commands/management/workflow.ts +++ b/src/features/config/switchWorkflow.ts @@ -2,9 +2,9 @@ * Workflow switching command */ -import { listWorkflows, loadWorkflow } from '../../config/index.js'; -import { getCurrentWorkflow, setCurrentWorkflow } from '../../config/paths.js'; -import { info, success, error } from '../../utils/ui.js'; +import { listWorkflows, loadWorkflow } from '../../infra/config/loaders/workflowLoader.js'; +import { getCurrentWorkflow, setCurrentWorkflow } from '../../infra/config/paths.js'; +import { info, success, error } from '../../shared/ui/index.js'; import { selectOption } from '../../prompt/index.js'; /** diff --git a/src/commands/interactive/index.ts b/src/features/interactive/index.ts similarity index 100% rename from src/commands/interactive/index.ts rename to src/features/interactive/index.ts diff --git a/src/features/interactive/interactive.ts b/src/features/interactive/interactive.ts new file mode 100644 index 0000000..4b5ff81 --- /dev/null +++ b/src/features/interactive/interactive.ts @@ -0,0 +1,419 @@ +/** + * 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 { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import chalk from 'chalk'; +import type { Language } from '../../core/models/index.js'; +import { loadGlobalConfig } from '../../infra/config/global/globalConfig.js'; +import { isQuietMode } from '../../context.js'; +import { loadAgentSessions, updateAgentSession } from '../../infra/config/paths.js'; +import { getProvider, type ProviderType } from '../../infra/providers/index.js'; +import { selectOption } from '../../prompt/index.js'; +import { getLanguageResourcesDir } from '../../resources/index.js'; +import { createLogger } from '../../shared/utils/debug.js'; +import { getErrorMessage } from '../../shared/utils/error.js'; +import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js'; +const log = createLogger('interactive'); + +const INTERACTIVE_SYSTEM_PROMPT_EN = `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.`; + +const INTERACTIVE_SYSTEM_PROMPT_JA = `あなたはタスク計画のアシスタントです。会話を通じて要件の明確化・整理を手伝います。今は計画フェーズで、実行は別プロセスで行われます。 + +## 役割 +- あいまいな要求に対して確認質問をする +- コードベースの前提を把握する(Read/Glob/Grep/Bash は読み取りのみ) +- 見落としそうな点や改善点を提案する +- 必要に応じて理解した内容を簡潔にまとめる +- 返答は簡潔で要点のみ + +## 厳守事項 +- 計画のみを行い、実装はしない +- ファイルの作成/編集/削除はしない +- build/test/install など状態を変えるコマンドは実行しない +- Bash は読み取り専用(ls/cat/git log/git diff など)に限定 +- スラッシュコマンドに言及しない(存在を知らない前提) +- ユーザーが満足したら次工程に進む。次の指示はしない`; + +const INTERACTIVE_SUMMARY_PROMPT_EN = `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.`; + +const INTERACTIVE_SUMMARY_PROMPT_JA = `あなたはタスク要約者です。会話を計画ステップ向けの具体的なタスク指示に変換してください。 + +要件: +- 出力は最終的な指示のみ(前置き不要) +- スコープや対象(ファイル/モジュール)が出ている場合は明確に書く +- 制約や「やらないこと」を保持する +- 情報不足があれば「Open Questions」セクションを短く付ける`; + +const UI_TEXT = { + en: { + intro: 'Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)', + resume: 'Resuming previous session', + noConversation: 'No conversation yet. Please describe your task first.', + summarizeFailed: 'Failed to summarize conversation. Please try again.', + continuePrompt: 'Okay, continue describing your task.', + proposed: 'Proposed task instruction:', + confirm: 'Use this task instruction?', + cancelled: 'Cancelled', + }, + ja: { + intro: '対話モード - タスク内容を入力してください。コマンド: /go(実行), /cancel(終了)', + resume: '前回のセッションを再開します', + noConversation: 'まだ会話がありません。まずタスク内容を入力してください。', + summarizeFailed: '会話の要約に失敗しました。再度お試しください。', + continuePrompt: '続けてタスク内容を入力してください。', + proposed: '提案されたタスク指示:', + confirm: 'このタスク指示で進めますか?', + cancelled: 'キャンセルしました', + }, +} as const; + +function resolveLanguage(lang?: Language): 'en' | 'ja' { + return lang === 'ja' ? 'ja' : 'en'; +} + +function readPromptFile(lang: 'en' | 'ja', fileName: string, fallback: string): string { + const filePath = join(getLanguageResourcesDir(lang), 'prompts', fileName); + if (existsSync(filePath)) { + return readFileSync(filePath, 'utf-8').trim(); + } + if (lang !== 'en') { + const enPath = join(getLanguageResourcesDir('en'), 'prompts', fileName); + if (existsSync(enPath)) { + return readFileSync(enPath, 'utf-8').trim(); + } + } + return fallback.trim(); +} + +function getInteractivePrompts(lang: 'en' | 'ja') { + return { + systemPrompt: readPromptFile( + lang, + 'interactive-system.md', + lang === 'ja' ? INTERACTIVE_SYSTEM_PROMPT_JA : INTERACTIVE_SYSTEM_PROMPT_EN, + ), + summaryPrompt: readPromptFile( + lang, + 'interactive-summary.md', + lang === 'ja' ? INTERACTIVE_SUMMARY_PROMPT_JA : INTERACTIVE_SUMMARY_PROMPT_EN, + ), + conversationLabel: lang === 'ja' ? '会話:' : 'Conversation:', + noTranscript: lang === 'ja' + ? '(ローカル履歴なし。現在のセッション文脈を要約してください。)' + : '(No local transcript. Summarize the current session context.)', + ui: UI_TEXT[lang], + }; +} + +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'); +} + +function buildSummaryPrompt( + history: ConversationMessage[], + hasSession: boolean, + summaryPrompt: string, + noTranscriptNote: string, + conversationLabel: string, +): string { + if (history.length > 0) { + const historyText = buildTaskFromHistory(history); + return `${summaryPrompt}\n\n${conversationLabel}\n${historyText}`; + } + if (hasSession) { + return `${summaryPrompt}\n\n${conversationLabel}\n${noTranscriptNote}`; + } + return ''; +} + +async function confirmTask(task: string, message: string, confirmLabel: string, yesLabel: string, noLabel: string): Promise { + blankLine(); + info(message); + console.log(task); + const decision = await selectOption(confirmLabel, [ + { label: yesLabel, value: 'yes' }, + { label: noLabel, value: 'no' }, + ]); + return decision === 'yes'; +} + +/** + * 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 { + 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, + prompt: string, + cwd: string, + model: string | undefined, + sessionId: string | undefined, + display: StreamDisplay, + systemPrompt: string, +): Promise { + const response = await provider.call('interactive', prompt, { + cwd, + model, + sessionId, + systemPrompt, + 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 { + const globalConfig = loadGlobalConfig(); + const lang = resolveLanguage(globalConfig.language); + const prompts = getInteractivePrompts(lang); + if (!globalConfig.provider) { + throw new Error('Provider is not configured.'); + } + const providerType = globalConfig.provider as ProviderType; + 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(prompts.ui.intro); + if (sessionId) { + info(prompts.ui.resume); + } + blankLine(); + + /** Call AI with automatic retry on session error (stale/invalid session ID). */ + async function callAIWithRetry(prompt: string, systemPrompt: string): Promise { + const display = new StreamDisplay('assistant', isQuietMode()); + try { + const result = await callAI( + provider, + prompt, + cwd, + model, + sessionId, + display, + systemPrompt, + ); + // 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, + systemPrompt, + ); + 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, prompts.systemPrompt); + 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') { + const summaryPrompt = buildSummaryPrompt( + history, + !!sessionId, + prompts.summaryPrompt, + prompts.noTranscript, + prompts.conversationLabel, + ); + if (!summaryPrompt) { + info(prompts.ui.noConversation); + continue; + } + const summaryResult = await callAIWithRetry(summaryPrompt, prompts.summaryPrompt); + if (!summaryResult) { + info(prompts.ui.summarizeFailed); + continue; + } + const task = summaryResult.content.trim(); + const confirmed = await confirmTask( + task, + prompts.ui.proposed, + prompts.ui.confirm, + lang === 'ja' ? 'はい' : 'Yes', + lang === 'ja' ? 'いいえ' : 'No', + ); + if (!confirmed) { + info(prompts.ui.continuePrompt); + continue; + } + log.info('Interactive mode confirmed', { messageCount: history.length }); + return { confirmed: true, task }; + } + + if (trimmed === '/cancel') { + info(prompts.ui.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, prompts.systemPrompt); + if (result) { + history.push({ role: 'assistant', content: result.content }); + blankLine(); + } else { + history.pop(); + } + } +} diff --git a/src/commands/execution/pipelineExecution.ts b/src/features/pipeline/execute.ts similarity index 91% rename from src/commands/execution/pipelineExecution.ts rename to src/features/pipeline/execute.ts index 9268a1a..33482b6 100644 --- a/src/commands/execution/pipelineExecution.ts +++ b/src/features/pipeline/execute.ts @@ -10,17 +10,16 @@ */ import { execFileSync } from 'node:child_process'; -import { fetchIssue, formatIssueAsTask, checkGhCli } from '../../github/issue.js'; -import type { GitHubIssue } from '../../github/types.js'; -import { createPullRequest, pushBranch, buildPrBody } from '../../github/pr.js'; -import { stageAndCommit } from '../../task/git.js'; -import { executeTask } from './taskExecution.js'; -import type { TaskExecutionOptions, PipelineExecutionOptions } from './types.js'; -import { loadGlobalConfig } from '../../config/global/globalConfig.js'; -import { info, error, success, status, blankLine } from '../../utils/ui.js'; -import { createLogger } from '../../utils/debug.js'; -import { getErrorMessage } from '../../utils/error.js'; -import type { PipelineConfig } from '../../models/types.js'; +import { fetchIssue, formatIssueAsTask, checkGhCli } from '../../infra/github/issue.js'; +import type { GitHubIssue } from '../../infra/github/types.js'; +import { createPullRequest, pushBranch, buildPrBody } from '../../infra/github/pr.js'; +import { stageAndCommit } from '../../infra/task/git.js'; +import { executeTask, type TaskExecutionOptions, type PipelineExecutionOptions } from '../tasks/index.js'; +import { loadGlobalConfig } from '../../infra/config/global/globalConfig.js'; +import { info, error, success, status, blankLine } from '../../shared/ui/index.js'; +import { createLogger } from '../../shared/utils/debug.js'; +import { getErrorMessage } from '../../shared/utils/error.js'; +import type { PipelineConfig } from '../../core/models/index.js'; import { EXIT_ISSUE_FETCH_FAILED, EXIT_WORKFLOW_FAILED, diff --git a/src/features/pipeline/index.ts b/src/features/pipeline/index.ts new file mode 100644 index 0000000..05116a6 --- /dev/null +++ b/src/features/pipeline/index.ts @@ -0,0 +1,5 @@ +/** + * Pipeline feature exports + */ + +export { executePipeline, type PipelineExecutionOptions } from './execute.js'; diff --git a/src/commands/management/addTask.ts b/src/features/tasks/add/index.ts similarity index 86% rename from src/commands/management/addTask.ts rename to src/features/tasks/add/index.ts index cc422d5..9e5c168 100644 --- a/src/commands/management/addTask.ts +++ b/src/features/tasks/add/index.ts @@ -8,18 +8,18 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { stringify as stringifyYaml } from 'yaml'; -import { promptInput, confirm, selectOption } from '../../prompt/index.js'; -import { success, info } from '../../utils/ui.js'; -import { summarizeTaskName } from '../../task/summarize.js'; -import { loadGlobalConfig } from '../../config/global/globalConfig.js'; -import { getProvider, type ProviderType } from '../../providers/index.js'; -import { createLogger } from '../../utils/debug.js'; -import { getErrorMessage } from '../../utils/error.js'; -import { listWorkflows } from '../../config/loaders/workflowLoader.js'; -import { getCurrentWorkflow } from '../../config/paths.js'; -import { interactiveMode } from '../interactive/interactive.js'; -import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../github/issue.js'; -import type { TaskFileData } from '../../task/schema.js'; +import { promptInput, confirm, selectOption } from '../../../prompt/index.js'; +import { success, info } from '../../../shared/ui/index.js'; +import { summarizeTaskName } from '../../../infra/task/summarize.js'; +import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js'; +import { getProvider, type ProviderType } from '../../../infra/providers/index.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import { getErrorMessage } from '../../../shared/utils/error.js'; +import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js'; +import { getCurrentWorkflow } from '../../../infra/config/paths.js'; +import { interactiveMode } from '../../interactive/index.js'; +import { isIssueReference, resolveIssueTask, parseIssueNumbers } from '../../../infra/github/issue.js'; +import type { TaskFileData } from '../../../infra/task/schema.js'; const log = createLogger('add-task'); diff --git a/src/commands/execution/selectAndExecute.ts b/src/features/tasks/execute/selectAndExecute.ts similarity index 87% rename from src/commands/execution/selectAndExecute.ts rename to src/features/tasks/execute/selectAndExecute.ts index ea7f9ed..811d5bc 100644 --- a/src/commands/execution/selectAndExecute.ts +++ b/src/features/tasks/execute/selectAndExecute.ts @@ -6,16 +6,16 @@ * mixing CLI parsing with business logic. */ -import { getCurrentWorkflow } from '../../config/paths.js'; -import { listWorkflows, isWorkflowPath } from '../../config/loaders/workflowLoader.js'; -import { selectOptionWithDefault, confirm } from '../../prompt/index.js'; -import { createSharedClone } from '../../task/clone.js'; -import { autoCommitAndPush } from '../../task/autoCommit.js'; -import { summarizeTaskName } from '../../task/summarize.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../constants.js'; -import { info, error, success } from '../../utils/ui.js'; -import { createLogger } from '../../utils/debug.js'; -import { createPullRequest, buildPrBody } from '../../github/pr.js'; +import { getCurrentWorkflow } from '../../../infra/config/paths.js'; +import { listWorkflows, isWorkflowPath } from '../../../infra/config/loaders/workflowLoader.js'; +import { selectOptionWithDefault, confirm } from '../../../prompt/index.js'; +import { createSharedClone } from '../../../infra/task/clone.js'; +import { autoCommitAndPush } from '../../../infra/task/autoCommit.js'; +import { summarizeTaskName } from '../../../infra/task/summarize.js'; +import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js'; +import { info, error, success } from '../../../shared/ui/index.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import { createPullRequest, buildPrBody } from '../../../infra/github/pr.js'; import { executeTask } from './taskExecution.js'; import type { TaskExecutionOptions, WorktreeConfirmationResult, SelectAndExecuteOptions } from './types.js'; diff --git a/src/commands/execution/session.ts b/src/features/tasks/execute/session.ts similarity index 75% rename from src/commands/execution/session.ts rename to src/features/tasks/execute/session.ts index 3719ee4..13b9899 100644 --- a/src/commands/execution/session.ts +++ b/src/features/tasks/execute/session.ts @@ -2,9 +2,9 @@ * Session management helpers for agent execution */ -import { loadAgentSessions, updateAgentSession } from '../../config/paths.js'; -import { loadGlobalConfig } from '../../config/global/globalConfig.js'; -import type { AgentResponse } from '../../models/types.js'; +import { loadAgentSessions, updateAgentSession } from '../../../infra/config/paths.js'; +import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js'; +import type { AgentResponse } from '../../../core/models/index.js'; /** * Execute a function with agent session management. diff --git a/src/commands/execution/taskExecution.ts b/src/features/tasks/execute/taskExecution.ts similarity index 92% rename from src/commands/execution/taskExecution.ts rename to src/features/tasks/execute/taskExecution.ts index 0593c0b..9012656 100644 --- a/src/commands/execution/taskExecution.ts +++ b/src/features/tasks/execute/taskExecution.ts @@ -2,11 +2,11 @@ * Task execution logic */ -import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../config/index.js'; -import { TaskRunner, type TaskInfo } from '../../task/index.js'; -import { createSharedClone } from '../../task/clone.js'; -import { autoCommitAndPush } from '../../task/autoCommit.js'; -import { summarizeTaskName } from '../../task/summarize.js'; +import { loadWorkflowByIdentifier, isWorkflowPath, loadGlobalConfig } from '../../../infra/config/index.js'; +import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js'; +import { createSharedClone } from '../../../infra/task/clone.js'; +import { autoCommitAndPush } from '../../../infra/task/autoCommit.js'; +import { summarizeTaskName } from '../../../infra/task/summarize.js'; import { header, info, @@ -14,11 +14,11 @@ import { success, status, blankLine, -} from '../../utils/ui.js'; -import { createLogger } from '../../utils/debug.js'; -import { getErrorMessage } from '../../utils/error.js'; +} from '../../../shared/ui/index.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import { getErrorMessage } from '../../../shared/utils/error.js'; import { executeWorkflow } from './workflowExecution.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../constants.js'; +import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js'; import type { TaskExecutionOptions, ExecuteTaskOptions } from './types.js'; export type { TaskExecutionOptions, ExecuteTaskOptions }; diff --git a/src/commands/execution/types.ts b/src/features/tasks/execute/types.ts similarity index 93% rename from src/commands/execution/types.ts rename to src/features/tasks/execute/types.ts index f600063..eca493e 100644 --- a/src/commands/execution/types.ts +++ b/src/features/tasks/execute/types.ts @@ -2,8 +2,8 @@ * Execution module type definitions */ -import type { Language } from '../../models/types.js'; -import type { ProviderType } from '../../providers/index.js'; +import type { Language } from '../../../core/models/index.js'; +import type { ProviderType } from '../../../infra/providers/index.js'; /** Result of workflow execution */ export interface WorkflowExecutionResult { diff --git a/src/commands/execution/workflowExecution.ts b/src/features/tasks/execute/workflowExecution.ts similarity index 94% rename from src/commands/execution/workflowExecution.ts rename to src/features/tasks/execute/workflowExecution.ts index ed47739..d0a2a20 100644 --- a/src/commands/execution/workflowExecution.ts +++ b/src/features/tasks/execute/workflowExecution.ts @@ -3,9 +3,8 @@ */ import { readFileSync } from 'node:fs'; -import { WorkflowEngine } from '../../workflow/engine/WorkflowEngine.js'; -import type { WorkflowConfig } from '../../models/types.js'; -import type { IterationLimitRequest } from '../../workflow/types.js'; +import { WorkflowEngine, type IterationLimitRequest } from '../../../core/workflow/index.js'; +import type { WorkflowConfig } from '../../../core/models/index.js'; import type { WorkflowExecutionResult, WorkflowExecutionOptions } from './types.js'; export type { WorkflowExecutionResult, WorkflowExecutionOptions }; @@ -15,9 +14,9 @@ import { updateAgentSession, loadWorktreeSessions, updateWorktreeSession, -} from '../../config/paths.js'; -import { loadGlobalConfig } from '../../config/global/globalConfig.js'; -import { isQuietMode } from '../../context.js'; +} from '../../../infra/config/paths.js'; +import { loadGlobalConfig } from '../../../infra/config/global/globalConfig.js'; +import { isQuietMode } from '../../../context.js'; import { header, info, @@ -27,7 +26,7 @@ import { status, blankLine, StreamDisplay, -} from '../../utils/ui.js'; +} from '../../../shared/ui/index.js'; import { generateSessionId, createSessionLog, @@ -39,11 +38,11 @@ import { type NdjsonStepComplete, type NdjsonWorkflowComplete, type NdjsonWorkflowAbort, -} from '../../utils/session.js'; -import { createLogger } from '../../utils/debug.js'; -import { notifySuccess, notifyError } from '../../utils/notification.js'; -import { selectOption, promptInput } from '../../prompt/index.js'; -import { EXIT_SIGINT } from '../../exitCodes.js'; +} from '../../../infra/fs/session.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import { notifySuccess, notifyError } from '../../../shared/utils/notification.js'; +import { selectOption, promptInput } from '../../../prompt/index.js'; +import { EXIT_SIGINT } from '../../../exitCodes.js'; const log = createLogger('workflow'); @@ -200,6 +199,7 @@ export async function executeWorkflow( ...(instruction ? { instruction } : {}), }; appendNdjsonLine(ndjsonLogPath, record); + }); engine.on('step:complete', (step, response, instruction) => { @@ -252,6 +252,7 @@ export async function executeWorkflow( }; appendNdjsonLine(ndjsonLogPath, record); + // Update in-memory log for pointer metadata (immutable) sessionLog = { ...sessionLog, iterations: sessionLog.iterations + 1 }; updateLatestPointer(sessionLog, workflowSessionId, projectCwd); diff --git a/src/features/tasks/index.ts b/src/features/tasks/index.ts new file mode 100644 index 0000000..211fedb --- /dev/null +++ b/src/features/tasks/index.ts @@ -0,0 +1,27 @@ +/** + * Task feature exports + */ + +export { executeWorkflow, type WorkflowExecutionResult, type WorkflowExecutionOptions } from './execute/workflowExecution.js'; +export { executeTask, runAllTasks, type TaskExecutionOptions } from './execute/taskExecution.js'; +export { executeAndCompleteTask, resolveTaskExecution } from './execute/taskExecution.js'; +export { withAgentSession } from './execute/session.js'; +export type { PipelineExecutionOptions } from './execute/types.js'; +export { + selectAndExecuteTask, + confirmAndCreateWorktree, + type SelectAndExecuteOptions, + type WorktreeConfirmationResult, +} from './execute/selectAndExecute.js'; +export { addTask, summarizeConversation } from './add/index.js'; +export { watchTasks } from './watch/index.js'; +export { + listTasks, + type ListAction, + isBranchMerged, + showFullDiff, + tryMergeBranch, + mergeBranch, + deleteBranch, + instructBranch, +} from './list/index.js'; diff --git a/src/commands/management/listTasks.ts b/src/features/tasks/list/index.ts similarity index 90% rename from src/commands/management/listTasks.ts rename to src/features/tasks/list/index.ts index 5a8a436..3e9d4ef 100644 --- a/src/commands/management/listTasks.ts +++ b/src/features/tasks/list/index.ts @@ -9,11 +9,11 @@ import { detectDefaultBranch, listTaktBranches, buildListItems, -} from '../../task/branchList.js'; -import { selectOption, confirm } from '../../prompt/index.js'; -import { info } from '../../utils/ui.js'; -import { createLogger } from '../../utils/debug.js'; -import type { TaskExecutionOptions } from '../execution/types.js'; +} from '../../../infra/task/branchList.js'; +import { selectOption, confirm } from '../../../prompt/index.js'; +import { info } from '../../../shared/ui/index.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import type { TaskExecutionOptions } from '../execute/types.js'; import { type ListAction, showFullDiff, diff --git a/src/commands/management/taskActions.ts b/src/features/tasks/list/taskActions.ts similarity index 92% rename from src/commands/management/taskActions.ts rename to src/features/tasks/list/taskActions.ts index cb164b5..9058258 100644 --- a/src/commands/management/taskActions.ts +++ b/src/features/tasks/list/taskActions.ts @@ -12,21 +12,21 @@ import { removeClone, removeCloneMeta, cleanupOrphanedClone, -} from '../../task/clone.js'; +} from '../../../infra/task/clone.js'; import { detectDefaultBranch, type BranchListItem, -} from '../../task/branchList.js'; -import { autoCommitAndPush } from '../../task/autoCommit.js'; -import { selectOption, promptInput } from '../../prompt/index.js'; -import { info, success, error as logError, warn, header, blankLine } from '../../utils/ui.js'; -import { createLogger } from '../../utils/debug.js'; -import { getErrorMessage } from '../../utils/error.js'; -import { executeTask } from '../execution/taskExecution.js'; -import type { TaskExecutionOptions } from '../execution/types.js'; -import { listWorkflows } from '../../config/loaders/workflowLoader.js'; -import { getCurrentWorkflow } from '../../config/paths.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../constants.js'; +} from '../../../infra/task/branchList.js'; +import { autoCommitAndPush } from '../../../infra/task/autoCommit.js'; +import { selectOption, promptInput } from '../../../prompt/index.js'; +import { info, success, error as logError, warn, header, blankLine } from '../../../shared/ui/index.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import { getErrorMessage } from '../../../shared/utils/error.js'; +import { executeTask } from '../execute/taskExecution.js'; +import type { TaskExecutionOptions } from '../execute/types.js'; +import { listWorkflows } from '../../../infra/config/loaders/workflowLoader.js'; +import { getCurrentWorkflow } from '../../../infra/config/paths.js'; +import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js'; const log = createLogger('list-tasks'); diff --git a/src/commands/management/watchTasks.ts b/src/features/tasks/watch/index.ts similarity index 80% rename from src/commands/management/watchTasks.ts rename to src/features/tasks/watch/index.ts index 3d63a5c..caffcc3 100644 --- a/src/commands/management/watchTasks.ts +++ b/src/features/tasks/watch/index.ts @@ -5,19 +5,19 @@ * Stays resident until Ctrl+C (SIGINT). */ -import { TaskRunner, type TaskInfo } from '../../task/index.js'; -import { TaskWatcher } from '../../task/watcher.js'; -import { getCurrentWorkflow } from '../../config/paths.js'; +import { TaskRunner, type TaskInfo } from '../../../infra/task/index.js'; +import { TaskWatcher } from '../../../infra/task/watcher.js'; +import { getCurrentWorkflow } from '../../../infra/config/paths.js'; import { header, info, success, status, blankLine, -} from '../../utils/ui.js'; -import { executeAndCompleteTask } from '../execution/taskExecution.js'; -import { DEFAULT_WORKFLOW_NAME } from '../../constants.js'; -import type { TaskExecutionOptions } from '../execution/types.js'; +} from '../../../shared/ui/index.js'; +import { executeAndCompleteTask } from '../execute/taskExecution.js'; +import { DEFAULT_WORKFLOW_NAME } from '../../../constants.js'; +import type { TaskExecutionOptions } from '../execute/types.js'; /** * Watch for tasks and execute them as they appear. diff --git a/src/index.ts b/src/index.ts index d06f67e..1caca1f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,13 +5,62 @@ */ // Models -export * from './models/index.js'; +export * from './core/models/index.js'; // Configuration -export * from './config/index.js'; +export * from './infra/config/index.js'; // Claude integration -export * from './claude/index.js'; +export { + ClaudeClient, + ClaudeProcess, + QueryExecutor, + QueryRegistry, + executeClaudeCli, + executeClaudeQuery, + generateQueryId, + hasActiveProcess, + isQueryActive, + getActiveQueryCount, + registerQuery, + unregisterQuery, + interruptQuery, + interruptAllQueries, + interruptCurrentProcess, + sdkMessageToStreamEvent, + createCanUseToolCallback, + createAskUserQuestionHooks, + buildSdkOptions, + callClaude, + callClaudeCustom, + callClaudeAgent, + callClaudeSkill, + callAiJudge, + detectRuleIndex, + detectJudgeIndex, + buildJudgePrompt, + isRegexSafe, +} from './claude/index.js'; +export type { + StreamEvent, + StreamCallback, + PermissionRequest, + PermissionHandler, + AskUserQuestionInput, + AskUserQuestionHandler, + ClaudeResult, + ClaudeResultWithQueryId, + ClaudeCallOptions, + ClaudeSpawnOptions, + InitEventData, + ToolUseEventData, + ToolResultEventData, + ToolOutputEventData, + TextEventData, + ThinkingEventData, + ResultEventData, + ErrorEventData, +} from './claude/index.js'; // Codex integration export * from './codex/index.js'; @@ -20,10 +69,54 @@ export * from './codex/index.js'; export * from './agents/index.js'; // Workflow engine -export * from './workflow/index.js'; +export { + WorkflowEngine, + COMPLETE_STEP, + ABORT_STEP, + ERROR_MESSAGES, + determineNextStepByRules, + extractBlockedPrompt, + LoopDetector, + createInitialState, + addUserInput, + getPreviousOutput, + handleBlocked, + ParallelLogger, + InstructionBuilder, + isReportObjectConfig, + ReportInstructionBuilder, + StatusJudgmentBuilder, + buildExecutionMetadata, + renderExecutionMetadata, + RuleEvaluator, + detectMatchedRule, + evaluateAggregateConditions, + AggregateEvaluator, + needsStatusJudgmentPhase, + runReportPhase, + runStatusJudgmentPhase, +} from './core/workflow/index.js'; +export type { + WorkflowEvents, + UserInputRequest, + IterationLimitRequest, + SessionUpdateCallback, + IterationLimitCallback, + WorkflowEngineOptions, + LoopCheckResult, + ProviderType, + RuleMatch, + RuleEvaluatorContext, + ReportInstructionContext, + StatusJudgmentContext, + InstructionContext, + ExecutionMetadata, + BlockedHandlerResult, +} from './core/workflow/index.js'; // Utilities -export * from './utils/index.js'; +export * from './shared/utils/index.js'; +export * from './shared/ui/index.js'; // Resources (embedded prompts and templates) export * from './resources/index.js'; diff --git a/src/config/global/globalConfig.ts b/src/infra/config/global/globalConfig.ts similarity index 98% rename from src/config/global/globalConfig.ts rename to src/infra/config/global/globalConfig.ts index 2b59403..b89ed59 100644 --- a/src/config/global/globalConfig.ts +++ b/src/infra/config/global/globalConfig.ts @@ -8,10 +8,10 @@ import { readFileSync, existsSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; -import { GlobalConfigSchema } from '../../models/schemas.js'; -import type { GlobalConfig, DebugConfig, Language } from '../../models/types.js'; +import { GlobalConfigSchema } from '../../../core/models/index.js'; +import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js'; import { getGlobalConfigPath, getProjectConfigPath } from '../paths.js'; -import { DEFAULT_LANGUAGE } from '../../constants.js'; +import { DEFAULT_LANGUAGE } from '../../../constants.js'; /** Create default global configuration (fresh instance each call) */ function createDefaultGlobalConfig(): GlobalConfig { diff --git a/src/config/global/index.ts b/src/infra/config/global/index.ts similarity index 100% rename from src/config/global/index.ts rename to src/infra/config/global/index.ts diff --git a/src/config/global/initialization.ts b/src/infra/config/global/initialization.ts similarity index 94% rename from src/config/global/initialization.ts rename to src/infra/config/global/initialization.ts index d250305..399cd10 100644 --- a/src/config/global/initialization.ts +++ b/src/infra/config/global/initialization.ts @@ -8,16 +8,16 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import type { Language } from '../../models/types.js'; -import { DEFAULT_LANGUAGE } from '../../constants.js'; -import { selectOptionWithDefault } from '../../prompt/index.js'; +import type { Language } from '../../../core/models/index.js'; +import { DEFAULT_LANGUAGE } from '../../../constants.js'; +import { selectOptionWithDefault } from '../../../prompt/index.js'; import { getGlobalConfigDir, getGlobalConfigPath, getProjectConfigDir, ensureDir, } from '../paths.js'; -import { copyProjectResourcesToDir, getLanguageResourcesDir } from '../../resources/index.js'; +import { copyProjectResourcesToDir, getLanguageResourcesDir } from '../../../resources/index.js'; import { setLanguage, setProvider } from './globalConfig.js'; /** diff --git a/src/config/index.ts b/src/infra/config/index.ts similarity index 100% rename from src/config/index.ts rename to src/infra/config/index.ts diff --git a/src/config/loaders/agentLoader.ts b/src/infra/config/loaders/agentLoader.ts similarity index 97% rename from src/config/loaders/agentLoader.ts rename to src/infra/config/loaders/agentLoader.ts index 726a1a9..a2ef729 100644 --- a/src/config/loaders/agentLoader.ts +++ b/src/infra/config/loaders/agentLoader.ts @@ -8,7 +8,7 @@ import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { join, basename } from 'node:path'; -import type { CustomAgentConfig } from '../../models/types.js'; +import type { CustomAgentConfig } from '../../../core/models/index.js'; import { getGlobalAgentsDir, getGlobalWorkflowsDir, diff --git a/src/config/loaders/index.ts b/src/infra/config/loaders/index.ts similarity index 100% rename from src/config/loaders/index.ts rename to src/infra/config/loaders/index.ts diff --git a/src/config/loaders/loader.ts b/src/infra/config/loaders/loader.ts similarity index 100% rename from src/config/loaders/loader.ts rename to src/infra/config/loaders/loader.ts diff --git a/src/config/loaders/workflowLoader.ts b/src/infra/config/loaders/workflowLoader.ts similarity index 100% rename from src/config/loaders/workflowLoader.ts rename to src/infra/config/loaders/workflowLoader.ts diff --git a/src/config/loaders/workflowParser.ts b/src/infra/config/loaders/workflowParser.ts similarity index 98% rename from src/config/loaders/workflowParser.ts rename to src/infra/config/loaders/workflowParser.ts index ed8a3d6..2dacb9a 100644 --- a/src/config/loaders/workflowParser.ts +++ b/src/infra/config/loaders/workflowParser.ts @@ -9,8 +9,8 @@ import { readFileSync, existsSync } from 'node:fs'; import { join, dirname, basename } from 'node:path'; import { parse as parseYaml } from 'yaml'; import type { z } from 'zod'; -import { WorkflowConfigRawSchema, WorkflowStepRawSchema } from '../../models/schemas.js'; -import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig, ReportObjectConfig } from '../../models/types.js'; +import { WorkflowConfigRawSchema, WorkflowStepRawSchema } from '../../../core/models/index.js'; +import type { WorkflowConfig, WorkflowStep, WorkflowRule, ReportConfig, ReportObjectConfig } from '../../../core/models/index.js'; /** Parsed step type from Zod schema (replaces `any`) */ type RawStep = z.output; @@ -141,6 +141,7 @@ function normalizeStepFromRaw(step: RawStep, workflowDir: string): WorkflowStep const result: WorkflowStep = { name: step.name, agent: agentSpec, + session: step.session, agentDisplayName: step.agent_name || (agentSpec ? extractAgentDisplayName(agentSpec) : step.name), agentPath: agentSpec ? resolveAgentPathForWorkflow(agentSpec, workflowDir) : undefined, allowedTools: step.allowed_tools, diff --git a/src/config/loaders/workflowResolver.ts b/src/infra/config/loaders/workflowResolver.ts similarity index 96% rename from src/config/loaders/workflowResolver.ts rename to src/infra/config/loaders/workflowResolver.ts index 53a159c..a133d4e 100644 --- a/src/config/loaders/workflowResolver.ts +++ b/src/infra/config/loaders/workflowResolver.ts @@ -8,11 +8,11 @@ import { existsSync, readdirSync, statSync } from 'node:fs'; import { join, resolve, isAbsolute } from 'node:path'; import { homedir } from 'node:os'; -import type { WorkflowConfig } from '../../models/types.js'; +import type { WorkflowConfig } from '../../../core/models/index.js'; import { getGlobalWorkflowsDir, getBuiltinWorkflowsDir, getProjectConfigDir } from '../paths.js'; import { getLanguage, getDisabledBuiltins } from '../global/globalConfig.js'; -import { createLogger } from '../../utils/debug.js'; -import { getErrorMessage } from '../../utils/error.js'; +import { createLogger } from '../../../shared/utils/debug.js'; +import { getErrorMessage } from '../../../shared/utils/error.js'; import { loadWorkflowFromFile } from './workflowParser.js'; const log = createLogger('workflow-resolver'); diff --git a/src/config/paths.ts b/src/infra/config/paths.ts similarity index 96% rename from src/config/paths.ts rename to src/infra/config/paths.ts index 22fea78..26897bd 100644 --- a/src/config/paths.ts +++ b/src/infra/config/paths.ts @@ -8,8 +8,8 @@ import { homedir } from 'node:os'; import { join, resolve } from 'node:path'; import { existsSync, mkdirSync } from 'node:fs'; -import type { Language } from '../models/types.js'; -import { getLanguageResourcesDir } from '../resources/index.js'; +import type { Language } from '../../core/models/index.js'; +import { getLanguageResourcesDir } from '../../resources/index.js'; /** Get takt global config directory (~/.takt) */ export function getGlobalConfigDir(): string { diff --git a/src/config/project/index.ts b/src/infra/config/project/index.ts similarity index 100% rename from src/config/project/index.ts rename to src/infra/config/project/index.ts diff --git a/src/config/project/projectConfig.ts b/src/infra/config/project/projectConfig.ts similarity index 97% rename from src/config/project/projectConfig.ts rename to src/infra/config/project/projectConfig.ts index ef88c56..d447c5d 100644 --- a/src/config/project/projectConfig.ts +++ b/src/infra/config/project/projectConfig.ts @@ -7,7 +7,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { parse, stringify } from 'yaml'; -import { copyProjectResourcesToDir } from '../../resources/index.js'; +import { copyProjectResourcesToDir } from '../../../resources/index.js'; import type { PermissionMode, ProjectPermissionMode, ProjectLocalConfig } from '../types.js'; export type { PermissionMode, ProjectPermissionMode, ProjectLocalConfig }; diff --git a/src/config/project/sessionStore.ts b/src/infra/config/project/sessionStore.ts similarity index 100% rename from src/config/project/sessionStore.ts rename to src/infra/config/project/sessionStore.ts diff --git a/src/config/types.ts b/src/infra/config/types.ts similarity index 100% rename from src/config/types.ts rename to src/infra/config/types.ts diff --git a/src/utils/session.ts b/src/infra/fs/session.ts similarity index 94% rename from src/utils/session.ts rename to src/infra/fs/session.ts index 5c97be0..98479a2 100644 --- a/src/utils/session.ts +++ b/src/infra/fs/session.ts @@ -5,12 +5,13 @@ import { existsSync, readFileSync, copyFileSync, appendFileSync } from 'node:fs'; import { join } from 'node:path'; import { getProjectLogsDir, getGlobalLogsDir, ensureDir, writeFileAtomic } from '../config/paths.js'; +import { generateReportDir as buildReportDir } from '../../shared/utils/reportDir.js'; import type { SessionLog, NdjsonRecord, NdjsonWorkflowStart, LatestLogPointer, -} from './types.js'; +} from '../../shared/utils/types.js'; // Re-export types for backward compatibility export type { @@ -22,7 +23,7 @@ export type { NdjsonWorkflowAbort, NdjsonRecord, LatestLogPointer, -} from './types.js'; +} from '../../shared/utils/types.js'; /** * Manages session lifecycle: ID generation, NDJSON logging, @@ -34,6 +35,7 @@ export class SessionManager { appendFileSync(filepath, JSON.stringify(record) + '\n', 'utf-8'); } + /** Initialize an NDJSON log file with the workflow_start record */ initNdjsonLog( sessionId: string, @@ -57,6 +59,7 @@ export class SessionManager { return filepath; } + /** Load an NDJSON log file and convert it to a SessionLog */ loadNdjsonLog(filepath: string): SessionLog | null { if (!existsSync(filepath)) { @@ -136,20 +139,7 @@ export class SessionManager { /** Generate report directory name from task and timestamp */ generateReportDir(task: string): string { - const now = new Date(); - const timestamp = now.toISOString() - .replace(/[-:T]/g, '') - .slice(0, 14) - .replace(/(\d{8})(\d{6})/, '$1-$2'); - - const summary = task - .slice(0, 30) - .toLowerCase() - .replace(/[^a-z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]+/g, '-') - .replace(/^-+|-+$/g, '') - || 'task'; - - return `${timestamp}-${summary}`; + return buildReportDir(task); } /** Create a new session log */ @@ -262,10 +252,12 @@ export function initNdjsonLog( return defaultManager.initNdjsonLog(sessionId, task, workflowName, projectDir); } + export function loadNdjsonLog(filepath: string): SessionLog | null { return defaultManager.loadNdjsonLog(filepath); } + export function generateSessionId(): string { return defaultManager.generateSessionId(); } diff --git a/src/github/issue.ts b/src/infra/github/issue.ts similarity index 98% rename from src/github/issue.ts rename to src/infra/github/issue.ts index cdee3c7..6bc2523 100644 --- a/src/github/issue.ts +++ b/src/infra/github/issue.ts @@ -6,7 +6,7 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../utils/debug.js'; +import { createLogger } from '../../shared/utils/debug.js'; import type { GitHubIssue, GhCliStatus } from './types.js'; export type { GitHubIssue, GhCliStatus }; diff --git a/src/github/pr.ts b/src/infra/github/pr.ts similarity index 94% rename from src/github/pr.ts rename to src/infra/github/pr.ts index 8b668bf..19cd859 100644 --- a/src/github/pr.ts +++ b/src/infra/github/pr.ts @@ -5,8 +5,8 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../utils/debug.js'; -import { getErrorMessage } from '../utils/error.js'; +import { createLogger } from '../../shared/utils/debug.js'; +import { getErrorMessage } from '../../shared/utils/error.js'; import { checkGhCli } from './issue.js'; import type { GitHubIssue, CreatePrOptions, CreatePrResult } from './types.js'; diff --git a/src/github/types.ts b/src/infra/github/types.ts similarity index 100% rename from src/github/types.ts rename to src/infra/github/types.ts diff --git a/src/providers/claude.ts b/src/infra/providers/claude.ts similarity index 95% rename from src/providers/claude.ts rename to src/infra/providers/claude.ts index 96b0f4d..8ef5189 100644 --- a/src/providers/claude.ts +++ b/src/infra/providers/claude.ts @@ -2,9 +2,9 @@ * Claude provider implementation */ -import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../claude/client.js'; +import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../../claude/client.js'; import { resolveAnthropicApiKey } from '../config/global/globalConfig.js'; -import type { AgentResponse } from '../models/types.js'; +import type { AgentResponse } from '../../core/models/index.js'; import type { Provider, ProviderCallOptions } from './types.js'; /** Claude provider - wraps existing Claude client */ diff --git a/src/providers/codex.ts b/src/infra/providers/codex.ts similarity index 93% rename from src/providers/codex.ts rename to src/infra/providers/codex.ts index 3913f6d..45a37c4 100644 --- a/src/providers/codex.ts +++ b/src/infra/providers/codex.ts @@ -2,9 +2,9 @@ * Codex provider implementation */ -import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/client.js'; +import { callCodex, callCodexCustom, type CodexCallOptions } from '../../codex/client.js'; import { resolveOpenaiApiKey } from '../config/global/globalConfig.js'; -import type { AgentResponse } from '../models/types.js'; +import type { AgentResponse } from '../../core/models/index.js'; import type { Provider, ProviderCallOptions } from './types.js'; /** Codex provider - wraps existing Codex client */ diff --git a/src/providers/index.ts b/src/infra/providers/index.ts similarity index 100% rename from src/providers/index.ts rename to src/infra/providers/index.ts diff --git a/src/providers/mock.ts b/src/infra/providers/mock.ts similarity index 82% rename from src/providers/mock.ts rename to src/infra/providers/mock.ts index d47b1c0..ef61799 100644 --- a/src/providers/mock.ts +++ b/src/infra/providers/mock.ts @@ -2,9 +2,9 @@ * Mock provider implementation */ -import { callMock, callMockCustom } from '../mock/client.js'; -import type { MockCallOptions } from '../mock/types.js'; -import type { AgentResponse } from '../models/types.js'; +import { callMock, callMockCustom } from '../../mock/client.js'; +import type { MockCallOptions } from '../../mock/types.js'; +import type { AgentResponse } from '../../core/models/index.js'; import type { Provider, ProviderCallOptions } from './types.js'; /** Mock provider - wraps existing Mock client */ diff --git a/src/providers/types.ts b/src/infra/providers/types.ts similarity index 91% rename from src/providers/types.ts rename to src/infra/providers/types.ts index 612d880..956b516 100644 --- a/src/providers/types.ts +++ b/src/infra/providers/types.ts @@ -2,8 +2,8 @@ * Type definitions for the provider abstraction layer */ -import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/types.js'; -import type { AgentResponse, PermissionMode } from '../models/types.js'; +import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../../claude/types.js'; +import type { AgentResponse, PermissionMode } from '../../core/models/index.js'; /** Common options for all providers */ export interface ProviderCallOptions { diff --git a/src/task/autoCommit.ts b/src/infra/task/autoCommit.ts similarity index 95% rename from src/task/autoCommit.ts rename to src/infra/task/autoCommit.ts index 8a9d640..9732b6c 100644 --- a/src/task/autoCommit.ts +++ b/src/infra/task/autoCommit.ts @@ -8,8 +8,8 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../utils/debug.js'; -import { getErrorMessage } from '../utils/error.js'; +import { createLogger } from '../../shared/utils/debug.js'; +import { getErrorMessage } from '../../shared/utils/error.js'; import { stageAndCommit } from './git.js'; const log = createLogger('autoCommit'); diff --git a/src/task/branchList.ts b/src/infra/task/branchList.ts similarity index 98% rename from src/task/branchList.ts rename to src/infra/task/branchList.ts index 0d4b03f..bc056d6 100644 --- a/src/task/branchList.ts +++ b/src/infra/task/branchList.ts @@ -7,7 +7,7 @@ */ import { execFileSync } from 'node:child_process'; -import { createLogger } from '../utils/debug.js'; +import { createLogger } from '../../shared/utils/debug.js'; import type { BranchInfo, BranchListItem } from './types.js'; diff --git a/src/task/clone.ts b/src/infra/task/clone.ts similarity index 98% rename from src/task/clone.ts rename to src/infra/task/clone.ts index 3820c90..eea8152 100644 --- a/src/task/clone.ts +++ b/src/infra/task/clone.ts @@ -10,8 +10,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { execFileSync } from 'node:child_process'; -import { createLogger } from '../utils/debug.js'; -import { slugify } from '../utils/slug.js'; +import { createLogger } from '../../shared/utils/debug.js'; +import { slugify } from '../../shared/utils/slug.js'; import { loadGlobalConfig } from '../config/global/globalConfig.js'; import type { WorktreeOptions, WorktreeResult } from './types.js'; diff --git a/src/task/display.ts b/src/infra/task/display.ts similarity index 97% rename from src/task/display.ts rename to src/infra/task/display.ts index 2a5aa04..7af6449 100644 --- a/src/task/display.ts +++ b/src/infra/task/display.ts @@ -5,7 +5,7 @@ */ import chalk from 'chalk'; -import { header, info, divider } from '../utils/ui.js'; +import { header, info, divider } from '../../shared/ui/index.js'; import type { TaskRunner } from './runner.js'; /** diff --git a/src/task/git.ts b/src/infra/task/git.ts similarity index 100% rename from src/task/git.ts rename to src/infra/task/git.ts diff --git a/src/task/index.ts b/src/infra/task/index.ts similarity index 100% rename from src/task/index.ts rename to src/infra/task/index.ts diff --git a/src/task/parser.ts b/src/infra/task/parser.ts similarity index 100% rename from src/task/parser.ts rename to src/infra/task/parser.ts diff --git a/src/task/runner.ts b/src/infra/task/runner.ts similarity index 100% rename from src/task/runner.ts rename to src/infra/task/runner.ts diff --git a/src/task/schema.ts b/src/infra/task/schema.ts similarity index 100% rename from src/task/schema.ts rename to src/infra/task/schema.ts diff --git a/src/task/summarize.ts b/src/infra/task/summarize.ts similarity index 98% rename from src/task/summarize.ts rename to src/infra/task/summarize.ts index a1d456d..a25e212 100644 --- a/src/task/summarize.ts +++ b/src/infra/task/summarize.ts @@ -7,7 +7,7 @@ import * as wanakana from 'wanakana'; import { loadGlobalConfig } from '../config/global/globalConfig.js'; import { getProvider, type ProviderType } from '../providers/index.js'; -import { createLogger } from '../utils/debug.js'; +import { createLogger } from '../../shared/utils/debug.js'; import type { SummarizeOptions } from './types.js'; export type { SummarizeOptions }; diff --git a/src/task/types.ts b/src/infra/task/types.ts similarity index 100% rename from src/task/types.ts rename to src/infra/task/types.ts diff --git a/src/task/watcher.ts b/src/infra/task/watcher.ts similarity index 97% rename from src/task/watcher.ts rename to src/infra/task/watcher.ts index 18a886e..d45c3b1 100644 --- a/src/task/watcher.ts +++ b/src/infra/task/watcher.ts @@ -5,7 +5,7 @@ * Uses polling (not fs.watch) for cross-platform reliability. */ -import { createLogger } from '../utils/debug.js'; +import { createLogger } from '../../shared/utils/debug.js'; import { TaskRunner } from './runner.js'; import type { TaskInfo } from './types.js'; diff --git a/src/mock/client.ts b/src/mock/client.ts index 1215e79..759686d 100644 --- a/src/mock/client.ts +++ b/src/mock/client.ts @@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'; import type { StreamEvent } from '../claude/process.js'; -import type { AgentResponse } from '../models/types.js'; +import type { AgentResponse } from '../core/models/index.js'; import { getScenarioQueue } from './scenario.js'; import type { MockCallOptions } from './types.js'; diff --git a/src/models/workflow.ts b/src/models/workflow.ts deleted file mode 100644 index e262334..0000000 --- a/src/models/workflow.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Workflow schemas and types are defined in: -// - schemas.ts (Zod schemas for YAML parsing) -// - workflow-types.ts (runtime types) -// This file is kept as a namespace placeholder. diff --git a/src/prompt/select.ts b/src/prompt/select.ts index b1d4e98..4e0da3a 100644 --- a/src/prompt/select.ts +++ b/src/prompt/select.ts @@ -5,7 +5,7 @@ */ import chalk from 'chalk'; -import { truncateText } from '../utils/text.js'; +import { truncateText } from '../shared/utils/text.js'; /** Option type for selectOption */ export interface SelectOptionItem { diff --git a/src/resources/index.ts b/src/resources/index.ts index 6ef6281..ce320cf 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -5,13 +5,14 @@ * Resources are organized into: * - resources/global/{lang}/workflows/ - Builtin workflows (loaded via fallback) * - resources/global/{lang}/agents/ - Builtin agents (loaded via fallback) + * - resources/global/{lang}/prompts/ - Builtin prompt templates * - resources/project/ - Project-level template files (.gitignore) */ import { readFileSync, readdirSync, existsSync, statSync, mkdirSync, writeFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import type { Language } from '../models/types.js'; +import type { Language } from '../core/models/index.js'; /** * Get the resources directory path @@ -102,4 +103,3 @@ function copyDirRecursive(srcDir: string, destDir: string, options: CopyOptions } } } - diff --git a/src/utils/LogManager.ts b/src/shared/ui/LogManager.ts similarity index 100% rename from src/utils/LogManager.ts rename to src/shared/ui/LogManager.ts diff --git a/src/utils/Spinner.ts b/src/shared/ui/Spinner.ts similarity index 100% rename from src/utils/Spinner.ts rename to src/shared/ui/Spinner.ts diff --git a/src/utils/StreamDisplay.ts b/src/shared/ui/StreamDisplay.ts similarity index 99% rename from src/utils/StreamDisplay.ts rename to src/shared/ui/StreamDisplay.ts index 02819bd..d10203a 100644 --- a/src/utils/StreamDisplay.ts +++ b/src/shared/ui/StreamDisplay.ts @@ -6,7 +6,7 @@ */ import chalk from 'chalk'; -import type { StreamEvent, StreamCallback } from '../claude/types.js'; +import type { StreamEvent, StreamCallback } from '../../claude/types.js'; import { truncate } from './LogManager.js'; /** Stream display manager for real-time Claude output */ diff --git a/src/utils/ui.ts b/src/shared/ui/index.ts similarity index 100% rename from src/utils/ui.ts rename to src/shared/ui/index.ts diff --git a/src/utils/debug.ts b/src/shared/utils/debug.ts similarity index 99% rename from src/utils/debug.ts rename to src/shared/utils/debug.ts index bd15245..1d99546 100644 --- a/src/utils/debug.ts +++ b/src/shared/utils/debug.ts @@ -6,7 +6,7 @@ import { existsSync, appendFileSync, mkdirSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import type { DebugConfig } from '../models/types.js'; +import type { DebugConfig } from '../../core/models/index.js'; /** * Debug logger singleton. diff --git a/src/utils/error.ts b/src/shared/utils/error.ts similarity index 100% rename from src/utils/error.ts rename to src/shared/utils/error.ts diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts new file mode 100644 index 0000000..45d8b1f --- /dev/null +++ b/src/shared/utils/index.ts @@ -0,0 +1,11 @@ +/** + * Shared utilities module - exports utility functions + */ + +export * from './debug.js'; +export * from './error.js'; +export * from './notification.js'; +export * from './slug.js'; +export * from './text.js'; +export * from './types.js'; +export * from './updateNotifier.js'; diff --git a/src/utils/notification.ts b/src/shared/utils/notification.ts similarity index 100% rename from src/utils/notification.ts rename to src/shared/utils/notification.ts diff --git a/src/shared/utils/reportDir.ts b/src/shared/utils/reportDir.ts new file mode 100644 index 0000000..84480ac --- /dev/null +++ b/src/shared/utils/reportDir.ts @@ -0,0 +1,20 @@ +/** + * Report directory name generation. + */ + +export function generateReportDir(task: string): string { + const now = new Date(); + const timestamp = now.toISOString() + .replace(/[-:T]/g, '') + .slice(0, 14) + .replace(/(\d{8})(\d{6})/, '$1-$2'); + + const summary = task + .slice(0, 30) + .toLowerCase() + .replace(/[^a-z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]+/g, '-') + .replace(/^-+|-+$/g, '') + || 'task'; + + return `${timestamp}-${summary}`; +} diff --git a/src/utils/slug.ts b/src/shared/utils/slug.ts similarity index 100% rename from src/utils/slug.ts rename to src/shared/utils/slug.ts diff --git a/src/utils/text.ts b/src/shared/utils/text.ts similarity index 100% rename from src/utils/text.ts rename to src/shared/utils/text.ts diff --git a/src/utils/types.ts b/src/shared/utils/types.ts similarity index 98% rename from src/utils/types.ts rename to src/shared/utils/types.ts index 97cba8c..edf470f 100644 --- a/src/utils/types.ts +++ b/src/shared/utils/types.ts @@ -80,6 +80,8 @@ export type NdjsonRecord = | NdjsonWorkflowComplete | NdjsonWorkflowAbort; +// --- Conversation log types --- + /** Pointer metadata for latest/previous log files */ export interface LatestLogPointer { sessionId: string; diff --git a/src/utils/updateNotifier.ts b/src/shared/utils/updateNotifier.ts similarity index 91% rename from src/utils/updateNotifier.ts rename to src/shared/utils/updateNotifier.ts index bce6c81..82578b1 100644 --- a/src/utils/updateNotifier.ts +++ b/src/shared/utils/updateNotifier.ts @@ -10,7 +10,7 @@ interface PkgInfo { } function loadPackageJson(): PkgInfo { - return require('../../package.json') as PkgInfo; + return require('../../../package.json') as PkgInfo; } /** diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index a63b353..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Utils module - exports utility functions - */ - -export * from './ui.js'; -export * from './session.js'; -export * from './debug.js'; -export * from './text.js';