From 7323d6d2884b38d7a0c8d6cc82363068cc9c2a1d Mon Sep 17 00:00:00 2001 From: nrslib <38722970+nrslib@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:29:02 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=BF=E3=82=B9=E3=82=AF=E3=82=92=E7=9B=A3?= =?UTF-8?q?=E8=A6=96=E3=81=99=E3=82=8B=20/watch=20=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 30 +++++- README.md | 176 +++++++++++++++++++++++++-------- docs/README.ja.md | 180 ++++++++++++++++++++++++++-------- package.json | 3 +- src/__tests__/watcher.test.ts | 145 +++++++++++++++++++++++++++ src/cli.ts | 7 +- src/commands/help.ts | 2 + src/commands/index.ts | 1 + src/commands/taskExecution.ts | 2 +- src/commands/watchTasks.ts | 118 ++++++++++++++++++++++ src/task/index.ts | 1 + src/task/watcher.ts | 90 +++++++++++++++++ 12 files changed, 670 insertions(+), 85 deletions(-) create mode 100644 src/__tests__/watcher.test.ts create mode 100644 src/commands/watchTasks.ts create mode 100644 src/task/watcher.ts diff --git a/CLAUDE.md b/CLAUDE.md index db0c502..ec8565b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Claude Code. It enables YAML-based workflow definitions that coordinate multiple AI agents through state machine transitions. -## Commands +## Development Commands | Command | Description | |---------|-------------| @@ -17,13 +17,26 @@ TAKT (Task Agent Koordination Tool) is a multi-agent orchestration system for Cl | `npx vitest run src/__tests__/client.test.ts` | Run single test file | | `npx vitest run -t "pattern"` | Run tests matching pattern | +## CLI Slash Commands + +| Command | Description | +|---------|-------------| +| `takt /run-tasks` | Execute all pending tasks from `.takt/tasks/` once | +| `takt /watch` | Watch `.takt/tasks/` and auto-execute tasks (resident process) | +| `takt /add-task` | Add a new task interactively (YAML format) | +| `takt /switch` | Switch workflow interactively | +| `takt /clear` | Clear agent conversation sessions (reset state) | +| `takt /refresh-builtin` | Update builtin resources from `resources/` to `~/.takt/` | +| `takt /help` | Show help message | +| `takt /config` | Display current configuration | + ## Architecture ### Core Flow ``` CLI (cli.ts) - → Slash commands (/run-tasks, /switch, /clear, /help, /config) + → Slash commands (/run-tasks, /watch, /add-task, /switch, /clear, /refresh-builtin, /help, /config) → or executeTask() → WorkflowEngine (workflow/engine.ts) → runAgent() (agents/runner.ts) @@ -63,6 +76,11 @@ CLI (cli.ts) - `agentLoader.ts` - Agent prompt file loading - `paths.ts` - Directory structure (`.takt/`, `~/.takt/`), session management +**Task Management** (`src/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) + ### Data Flow 1. User provides task or slash command → CLI @@ -166,3 +184,11 @@ model: opus # Default model for all steps (unless overridden) - `rejected` - Review failed, needs major rework - `improve` - Needs improvement (security concerns, quality issues) - `always` - Unconditional transition + +## Testing Notes + +- Vitest for testing framework +- Tests use file system fixtures in `__tests__/` subdirectories +- Mock workflows and agent configs for integration tests +- Test single files: `npx vitest run src/__tests__/filename.test.ts` +- Pattern matching: `npx vitest run -t "test pattern"` diff --git a/README.md b/README.md index e28163d..5483430 100644 --- a/README.md +++ b/README.md @@ -26,22 +26,32 @@ npm install -g takt # Run a task (will prompt for workflow selection) takt "Add a login feature" -# Switch workflow -takt /switch +# Add a task to the queue +takt /add-task "Fix the login bug" # Run all pending tasks takt /run-tasks + +# Watch for tasks and auto-execute +takt /watch + +# Switch workflow +takt /switch ``` ## Commands | Command | Description | |---------|-------------| -| `takt "task"` | Execute task with workflow selection | +| `takt "task"` | Execute task with current workflow (continues session) | | `takt -r "task"` | Execute task, resuming previous session | -| `takt /run-tasks` | Run all pending tasks | +| `takt /run-tasks` | Run all pending tasks from `.takt/tasks/` | +| `takt /watch` | Watch `.takt/tasks/` and auto-execute tasks (stays resident) | +| `takt /add-task` | Add a new task interactively (YAML format) | | `takt /switch` | Switch workflow interactively | | `takt /clear` | Clear agent conversation sessions | +| `takt /refresh-builtin` | Update builtin agents/workflows to latest version | +| `takt /config` | Display current configuration | | `takt /help` | Show help | ## Workflows @@ -88,11 +98,28 @@ steps: next_step: implement ``` +## Built-in Workflows + +TAKT ships with several built-in workflows: + +| Workflow | Description | +|----------|-------------| +| `default` | Full development workflow: plan → implement → architect review → AI review → security review → supervisor approval. Includes fix loops for each review stage. | +| `simple` | Simplified version of default: plan → implement → architect review → AI review → supervisor. No intermediate fix steps. | +| `research` | Research workflow: planner → digger → supervisor. Autonomously researches topics without asking questions. | +| `expert-review` | Comprehensive review with domain experts: CQRS+ES, Frontend, AI, Security, QA reviews with fix loops. | +| `magi` | Deliberation system inspired by Evangelion. Three AI personas (MELCHIOR, BALTHASAR, CASPER) analyze and vote. | + +Switch between workflows with `takt /switch`. + ## Built-in Agents - **coder** - Implements features and fixes bugs - **architect** - Reviews code and provides feedback - **supervisor** - Final verification and approval +- **planner** - Task analysis and implementation planning +- **ai-reviewer** - AI-generated code quality review +- **security** - Security vulnerability assessment ## Custom Agents @@ -149,6 +176,14 @@ Available Codex models: ├── config.yaml # Global config (provider, model, workflows, etc.) ├── workflows/ # Workflow definitions └── agents/ # Agent prompt files + +.takt/ # Project-level config +├── agents.yaml # Custom agent definitions +├── tasks/ # Pending task files (.yaml, .md) +├── completed/ # Completed tasks with reports +├── worktrees/ # Git worktrees for isolated task execution +├── reports/ # Execution reports (auto-generated) +└── logs/ # Session logs ``` ### Global Configuration @@ -189,22 +224,6 @@ takt -r "The bug occurs when the password contains special characters" The `-r` flag preserves the agent's conversation history, allowing for natural back-and-forth interaction. -### Playing with MAGI System - -MAGI is a deliberation system inspired by Evangelion. Three AI personas analyze your question from different perspectives and vote: - -```bash -# Select 'magi' workflow when prompted -takt "Should we migrate from REST to GraphQL?" -``` - -The three MAGI personas: -- **MELCHIOR-1** (Scientist): Logical, data-driven analysis -- **BALTHASAR-2** (Nurturer): Team and human-centered perspective -- **CASPER-3** (Pragmatist): Practical, real-world considerations - -Each persona votes: APPROVE, REJECT, or CONDITIONAL. The final decision is made by majority vote. - ### Adding Custom Workflows Create your own workflow by adding YAML files to `~/.takt/workflows/`: @@ -268,27 +287,33 @@ You are a code reviewer focused on security. - [REVIEWER:REJECT] if issues found (list them) ``` -### Using `/run-tasks` for Batch Processing +### Task Management -The `/run-tasks` command executes all task files in `.takt/tasks/` directory: +TAKT supports batch task processing through task files in `.takt/tasks/`. Both `.yaml`/`.yml` and `.md` file formats are supported. + +#### Adding Tasks with `/add-task` ```bash -# Create task files as you think of them -echo "Add unit tests for the auth module" > .takt/tasks/001-add-tests.md -echo "Refactor the database layer" > .takt/tasks/002-refactor-db.md -echo "Update API documentation" > .takt/tasks/003-update-docs.md +# Quick add (no worktree) +takt /add-task "Add authentication feature" -# Run all pending tasks -takt /run-tasks +# Interactive mode (prompts for worktree, branch, workflow options) +takt /add-task ``` -**How it works:** -- Tasks are executed in alphabetical order (use prefixes like `001-`, `002-` for ordering) -- Each task file should contain a description of what needs to be done -- Completed tasks are moved to `.takt/completed/` with execution reports -- New tasks added during execution will be picked up dynamically +#### Task File Formats -**Task file format:** +**YAML format** (recommended, supports worktree/branch/workflow options): + +```yaml +# .takt/tasks/add-auth.yaml +task: "Add authentication feature" +worktree: true # Run in isolated git worktree +branch: "feat/add-auth" # Branch name (auto-generated if omitted) +workflow: "default" # Workflow override (uses current if omitted) +``` + +**Markdown format** (simple, backward compatible): ```markdown # .takt/tasks/add-login-feature.md @@ -301,10 +326,35 @@ Requirements: - Error handling for failed attempts ``` -This is perfect for: -- Brainstorming sessions where you capture ideas as files -- Breaking down large features into smaller tasks -- Automated pipelines that generate task files +#### Git Worktree Isolation + +YAML task files can specify `worktree` to run each task in an isolated git worktree, keeping the main working directory clean: + +- `worktree: true` - Auto-create at `.takt/worktrees/{timestamp}-{task-slug}/` +- `worktree: "/path/to/dir"` - Create at specified path +- `branch: "feat/xxx"` - Use specified branch (auto-generated as `takt/{timestamp}-{slug}` if omitted) +- Omit `worktree` - Run in current working directory (default) + +#### Running Tasks with `/run-tasks` + +```bash +takt /run-tasks +``` + +- Tasks are executed in alphabetical order (use prefixes like `001-`, `002-` for ordering) +- Completed tasks are moved to `.takt/completed/` with execution reports +- New tasks added during execution will be picked up dynamically + +#### Watching Tasks with `/watch` + +```bash +takt /watch +``` + +Watch mode polls `.takt/tasks/` for new task files and auto-executes them as they appear. The process stays resident until `Ctrl+C`. This is useful for: +- CI/CD pipelines that generate task files +- Automated workflows where tasks are added by external processes +- Long-running development sessions where tasks are queued over time ### Workflow Variables @@ -313,11 +363,56 @@ Available variables in `instruction_template`: | Variable | Description | |----------|-------------| | `{task}` | Original user request | -| `{iteration}` | Current iteration number | -| `{max_iterations}` | Maximum iterations | +| `{iteration}` | Workflow-wide turn count (total steps executed) | +| `{max_iterations}` | Maximum iterations allowed | +| `{step_iteration}` | Per-step iteration count (how many times THIS step has run) | | `{previous_response}` | Previous step's output (requires `pass_previous_response: true`) | | `{user_inputs}` | Additional user inputs during workflow | | `{git_diff}` | Current git diff (uncommitted changes) | +| `{report_dir}` | Report directory name (e.g., `20250126-143052-task-summary`) | + +### Designing Workflows + +Each workflow step requires three key elements: + +**1. Agent** - A Markdown file containing the system prompt: + +```yaml +agent: ~/.takt/agents/default/coder.md # Path to agent prompt file +agent_name: coder # Display name (optional) +``` + +**2. Status Rules** - Define how the agent signals completion. Agents output status markers like `[CODER:DONE]` or `[ARCHITECT:REJECT]` that TAKT detects to drive transitions: + +```yaml +status_rules_prompt: | + Your final output MUST include a status tag: + - `[CODER:DONE]` if implementation is complete + - `[CODER:BLOCKED]` if you cannot proceed +``` + +**3. Transitions** - Route to the next step based on status: + +```yaml +transitions: + - condition: done # Maps to status tag DONE + next_step: review # Go to review step + - condition: blocked # Maps to status tag BLOCKED + next_step: ABORT # End workflow with failure +``` + +Available transition conditions: `done`, `blocked`, `approved`, `rejected`, `improve`, `always`. +Special next_step values: `COMPLETE` (success), `ABORT` (failure). + +**Step options:** + +| Option | Default | Description | +|--------|---------|-------------| +| `pass_previous_response` | `true` | Pass previous step's output to `{previous_response}` | +| `on_no_status` | - | Behavior when no status is detected: `complete`, `continue`, `stay` | +| `allowed_tools` | - | List of tools the agent can use (Read, Glob, Grep, Edit, Write, Bash, etc.) | +| `provider` | - | Override provider for this step (`claude` or `codex`) | +| `model` | - | Override model for this step | ## API Usage @@ -375,6 +470,7 @@ This ensures the project works correctly in a clean Node.js 20 environment. - [Agent Guide](./docs/agents.md) - Configure custom agents - [Changelog](./CHANGELOG.md) - Version history - [Security Policy](./SECURITY.md) - Vulnerability reporting +- [Blog: TAKT - AI Agent Orchestration](https://zenn.dev/nrs/articles/c6842288a526d7) - Design philosophy and practical usage guide (Japanese) ## License diff --git a/docs/README.ja.md b/docs/README.ja.md index 6a9c866..8e2172f 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -1,12 +1,14 @@ # TAKT -**T**ask **A**gent **K**oordination **T**ool - Claude Code向けのマルチエージェントオーケストレーションシステム(Codex対応予定) +**T**ask **A**gent **K**oordination **T**ool - Claude CodeとOpenAI Codex向けのマルチエージェントオーケストレーションシステム > **Note**: このプロジェクトは個人のペースで開発されています。詳細は[免責事項](#免責事項)をご覧ください。 ## 必要条件 -- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) がインストール・設定済みであること +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) または Codex がインストール・設定済みであること + +TAKTはClaude CodeとCodexの両方をプロバイダーとしてサポートしています。セットアップ時にプロバイダーを選択できます。 ## インストール @@ -20,22 +22,32 @@ npm install -g takt # タスクを実行(ワークフロー選択プロンプトが表示されます) takt "ログイン機能を追加して" -# ワークフローを切り替え -takt /switch +# タスクをキューに追加 +takt /add-task "ログインのバグを修正" # 保留中のタスクをすべて実行 takt /run-tasks + +# タスクを監視して自動実行 +takt /watch + +# ワークフローを切り替え +takt /switch ``` ## コマンド一覧 | コマンド | 説明 | |---------|------| -| `takt "タスク"` | ワークフロー選択後にタスクを実行 | +| `takt "タスク"` | 現在のワークフローでタスクを実行(セッション継続) | | `takt -r "タスク"` | 前回のセッションを再開してタスクを実行 | -| `takt /run-tasks` | 保留中のタスクをすべて実行 | +| `takt /run-tasks` | `.takt/tasks/` の保留中タスクをすべて実行 | +| `takt /watch` | `.takt/tasks/` を監視してタスクを自動実行(常駐プロセス) | +| `takt /add-task` | 新しいタスクを対話的に追加(YAML形式) | | `takt /switch` | ワークフローを対話的に切り替え | | `takt /clear` | エージェントの会話セッションをクリア | +| `takt /refresh-builtin` | ビルトインのエージェント/ワークフローを最新版に更新 | +| `takt /config` | 現在の設定を表示 | | `takt /help` | ヘルプを表示 | ## 実践的な使い方ガイド @@ -54,43 +66,33 @@ takt -r "パスワードに特殊文字が含まれているとバグが発生 `-r`フラグはエージェントの会話履歴を保持し、自然なやり取りを可能にします。 -### MAGIシステムで遊ぶ +### タスク管理 -MAGIはエヴァンゲリオンにインスパイアされた審議システムです。3つのAIペルソナがあなたの質問を異なる視点から分析し、投票します: +TAKTは`.takt/tasks/`内のタスクファイルによるバッチ処理をサポートしています。`.yaml`/`.yml`と`.md`の両方のファイル形式に対応しています。 + +#### `/add-task` でタスクを追加 ```bash -# プロンプトが表示されたら'magi'ワークフローを選択 -takt "RESTからGraphQLに移行すべきか?" +# クイック追加(worktreeなし) +takt /add-task "認証機能を追加" + +# 対話モード(worktree、ブランチ、ワークフローオプションを指定可能) +takt /add-task ``` -3つのMAGIペルソナ: -- **MELCHIOR-1**(科学者):論理的、データ駆動の分析 -- **BALTHASAR-2**(母性):チームと人間中心の視点 -- **CASPER-3**(現実主義者):実用的で現実的な考慮 +#### タスクファイルの形式 -各ペルソナは APPROVE、REJECT、または CONDITIONAL で投票します。最終決定は多数決で行われます。 +**YAML形式**(推奨、worktree/branch/workflowオプション対応): -### `/run-tasks` でバッチ処理 - -`/run-tasks`コマンドは`.takt/tasks/`ディレクトリ内のすべてのタスクファイルを実行します: - -```bash -# 思いつくままにタスクファイルを作成 -echo "認証モジュールのユニットテストを追加" > .takt/tasks/001-add-tests.md -echo "データベースレイヤーをリファクタリング" > .takt/tasks/002-refactor-db.md -echo "APIドキュメントを更新" > .takt/tasks/003-update-docs.md - -# すべての保留タスクを実行 -takt /run-tasks +```yaml +# .takt/tasks/add-auth.yaml +task: "認証機能を追加する" +worktree: true # 隔離されたgit worktreeで実行 +branch: "feat/add-auth" # ブランチ名(省略時は自動生成) +workflow: "default" # ワークフロー指定(省略時は現在のもの) ``` -**動作の仕組み:** -- タスクはアルファベット順に実行されます(`001-`、`002-`のようなプレフィックスで順序を制御) -- 各タスクファイルには実行すべき内容の説明を含めます -- 完了したタスクは実行レポートとともに`.takt/completed/`に移動されます -- 実行中に追加された新しいタスクも動的に取得されます - -**タスクファイルの形式:** +**Markdown形式**(シンプル、後方互換): ```markdown # .takt/tasks/add-login-feature.md @@ -103,10 +105,35 @@ takt /run-tasks - 失敗時のエラーハンドリング ``` -これは以下のような場合に最適です: -- アイデアをファイルとしてキャプチャするブレインストーミングセッション -- 大きな機能を小さなタスクに分割する場合 -- タスクファイルを生成する自動化パイプライン +#### Git Worktree による隔離実行 + +YAMLタスクファイルで`worktree`を指定すると、各タスクを隔離されたgit worktreeで実行し、メインの作業ディレクトリをクリーンに保てます: + +- `worktree: true` - `.takt/worktrees/{timestamp}-{task-slug}/`に自動作成 +- `worktree: "/path/to/dir"` - 指定パスに作成 +- `branch: "feat/xxx"` - 指定ブランチを使用(省略時は`takt/{timestamp}-{slug}`で自動生成) +- `worktree`省略 - カレントディレクトリで実行(デフォルト) + +#### `/run-tasks` でタスクを実行 + +```bash +takt /run-tasks +``` + +- タスクはアルファベット順に実行されます(`001-`、`002-`のようなプレフィックスで順序を制御) +- 完了したタスクは実行レポートとともに`.takt/completed/`に移動されます +- 実行中に追加された新しいタスクも動的に取得されます + +#### `/watch` でタスクを監視 + +```bash +takt /watch +``` + +ウォッチモードは`.takt/tasks/`をポーリングし、新しいタスクファイルが現れると自動実行します。`Ctrl+C`で停止する常駐プロセスです。以下のような場合に便利です: +- タスクファイルを生成するCI/CDパイプライン +- 外部プロセスがタスクを追加する自動化ワークフロー +- タスクを順次キューイングする長時間の開発セッション ### カスタムワークフローの追加 @@ -178,11 +205,56 @@ agent: /path/to/custom/agent.md | 変数 | 説明 | |------|------| | `{task}` | 元のユーザーリクエスト | -| `{iteration}` | 現在のイテレーション番号 | +| `{iteration}` | ワークフロー全体のターン数(実行された全ステップ数) | | `{max_iterations}` | 最大イテレーション数 | +| `{step_iteration}` | ステップごとのイテレーション数(このステップが実行された回数) | | `{previous_response}` | 前のステップの出力(`pass_previous_response: true`が必要) | | `{user_inputs}` | ワークフロー中の追加ユーザー入力 | | `{git_diff}` | 現在のgit diff(コミットされていない変更) | +| `{report_dir}` | レポートディレクトリ名(例:`20250126-143052-task-summary`) | + +### ワークフローの設計 + +各ワークフローステップには3つの重要な要素が必要です。 + +**1. エージェント** - システムプロンプトを含むMarkdownファイル: + +```yaml +agent: ~/.takt/agents/default/coder.md # エージェントプロンプトファイルのパス +agent_name: coder # 表示名(オプション) +``` + +**2. ステータスルール** - エージェントが完了を通知する方法を定義。エージェントは`[CODER:DONE]`や`[ARCHITECT:REJECT]`のようなステータスマーカーを出力し、TAKTがそれを検出して遷移を駆動します: + +```yaml +status_rules_prompt: | + 最終出力には必ずステータスタグを含めてください: + - `[CODER:DONE]` 実装が完了した場合 + - `[CODER:BLOCKED]` 進行できない場合 +``` + +**3. 遷移** - ステータスに基づいて次のステップにルーティング: + +```yaml +transitions: + - condition: done # ステータスタグDONEに対応 + next_step: review # reviewステップへ遷移 + - condition: blocked # ステータスタグBLOCKEDに対応 + next_step: ABORT # ワークフローを失敗終了 +``` + +使用可能な遷移条件:`done`、`blocked`、`approved`、`rejected`、`improve`、`always` +特殊なnext_step値:`COMPLETE`(成功)、`ABORT`(失敗) + +**ステップオプション:** + +| オプション | デフォルト | 説明 | +|-----------|-----------|------| +| `pass_previous_response` | `true` | 前のステップの出力を`{previous_response}`に渡す | +| `on_no_status` | - | ステータス未検出時の動作:`complete`、`continue`、`stay` | +| `allowed_tools` | - | エージェントが使用できるツール一覧(Read, Glob, Grep, Edit, Write, Bash等) | +| `provider` | - | このステップのプロバイダーを上書き(`claude`または`codex`) | +| `model` | - | このステップのモデルを上書き | ## ワークフロー @@ -215,11 +287,28 @@ steps: next_step: implement ``` +## ビルトインワークフロー + +TAKTには複数のビルトインワークフローが同梱されています: + +| ワークフロー | 説明 | +|------------|------| +| `default` | フル開発ワークフロー:計画 → 実装 → アーキテクトレビュー → AIレビュー → セキュリティレビュー → スーパーバイザー承認。各レビュー段階に修正ループあり。 | +| `simple` | defaultの簡略版:計画 → 実装 → アーキテクトレビュー → AIレビュー → スーパーバイザー。中間の修正ステップなし。 | +| `research` | リサーチワークフロー:プランナー → ディガー → スーパーバイザー。質問せずに自律的にリサーチを実行。 | +| `expert-review` | ドメインエキスパートによる包括的レビュー:CQRS+ES、フロントエンド、AI、セキュリティ、QAレビューと修正ループ。 | +| `magi` | エヴァンゲリオンにインスパイアされた審議システム。3つのAIペルソナ(MELCHIOR、BALTHASAR、CASPER)が分析し投票。 | + +`takt /switch` でワークフローを切り替えられます。 + ## ビルトインエージェント - **coder** - 機能を実装しバグを修正 - **architect** - コードをレビューしフィードバックを提供 - **supervisor** - 最終検証と承認 +- **planner** - タスク分析と実装計画 +- **ai-reviewer** - AI生成コードの品質レビュー +- **security** - セキュリティ脆弱性の評価 ## カスタムエージェント @@ -230,6 +319,8 @@ agents: - name: my-reviewer prompt_file: .takt/prompts/reviewer.md allowed_tools: [Read, Glob, Grep] + provider: claude # オプション:claude または codex + model: opus # Claude: opus/sonnet/haiku、Codex: gpt-5.2-codex 等 status_patterns: approved: "\\[APPROVE\\]" rejected: "\\[REJECT\\]" @@ -239,9 +330,17 @@ agents: ``` ~/.takt/ -├── config.yaml # グローバル設定 +├── config.yaml # グローバル設定(プロバイダー、モデル、ワークフロー等) ├── workflows/ # ワークフロー定義 └── agents/ # エージェントプロンプトファイル + +.takt/ # プロジェクトレベルの設定 +├── agents.yaml # カスタムエージェント定義 +├── tasks/ # 保留中のタスクファイル(.yaml, .md) +├── completed/ # 完了したタスクとレポート +├── worktrees/ # タスク隔離実行用のgit worktree +├── reports/ # 実行レポート(自動生成) +└── logs/ # セッションログ ``` ## API使用例 @@ -300,6 +399,7 @@ docker compose run --rm build - [Agent Guide](./agents.md) - カスタムエージェントの設定 - [Changelog](../CHANGELOG.md) - バージョン履歴 - [Security Policy](../SECURITY.md) - 脆弱性報告 +- [ブログ:TAKT - AIエージェントオーケストレーション](https://zenn.dev/nrs/articles/c6842288a526d7) - 設計思想と実践的な使い方ガイド ## ライセンス diff --git a/package.json b/package.json index 3146062..a23909b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "test": "vitest run", "test:watch": "vitest", "lint": "eslint src/", - "prepublishOnly": "npm run lint && npm run build && npm run test" + "prepublishOnly": "npm run lint && npm run build && npm run test", + "postversion": "git push --follow-tags" }, "keywords": [ "claude", diff --git a/src/__tests__/watcher.test.ts b/src/__tests__/watcher.test.ts new file mode 100644 index 0000000..0dabc8a --- /dev/null +++ b/src/__tests__/watcher.test.ts @@ -0,0 +1,145 @@ +/** + * TaskWatcher tests + */ + +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/runner.js'; + +describe('TaskWatcher', () => { + const testDir = `/tmp/takt-watcher-test-${Date.now()}`; + + beforeEach(() => { + mkdirSync(join(testDir, '.takt', 'tasks'), { recursive: true }); + mkdirSync(join(testDir, '.takt', 'completed'), { recursive: true }); + }); + + afterEach(() => { + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true, force: true }); + } + }); + + describe('constructor', () => { + it('should create watcher with default options', () => { + const watcher = new TaskWatcher(testDir); + expect(watcher.isRunning()).toBe(false); + }); + + it('should accept custom poll interval', () => { + const watcher = new TaskWatcher(testDir, { pollInterval: 500 }); + expect(watcher.isRunning()).toBe(false); + }); + }); + + describe('watch', () => { + it('should detect and process a task file', async () => { + const watcher = new TaskWatcher(testDir, { pollInterval: 50 }); + const processed: string[] = []; + + // Pre-create a task file + writeFileSync( + join(testDir, '.takt', 'tasks', 'test-task.md'), + 'Test task content' + ); + + // Start watching, stop after first task + const watchPromise = watcher.watch(async (task: TaskInfo) => { + processed.push(task.name); + // Stop after processing to avoid infinite loop in test + watcher.stop(); + }); + + await watchPromise; + + expect(processed).toEqual(['test-task']); + expect(watcher.isRunning()).toBe(false); + }); + + it('should wait when no tasks are available', async () => { + const watcher = new TaskWatcher(testDir, { pollInterval: 50 }); + let pollCount = 0; + + // Start watching, add a task after a delay + const watchPromise = watcher.watch(async (task: TaskInfo) => { + pollCount++; + watcher.stop(); + }); + + // Add task after short delay (after at least one empty poll) + await new Promise((resolve) => setTimeout(resolve, 100)); + writeFileSync( + join(testDir, '.takt', 'tasks', 'delayed-task.md'), + 'Delayed task' + ); + + await watchPromise; + + expect(pollCount).toBe(1); + }); + + it('should process multiple tasks sequentially', async () => { + const watcher = new TaskWatcher(testDir, { pollInterval: 50 }); + const processed: string[] = []; + + // Pre-create two task files + writeFileSync( + join(testDir, '.takt', 'tasks', 'a-task.md'), + 'First task' + ); + writeFileSync( + join(testDir, '.takt', 'tasks', 'b-task.md'), + 'Second task' + ); + + const watchPromise = watcher.watch(async (task: TaskInfo) => { + processed.push(task.name); + // Remove the task file to simulate completion + rmSync(task.filePath); + if (processed.length >= 2) { + watcher.stop(); + } + }); + + await watchPromise; + + expect(processed).toEqual(['a-task', 'b-task']); + }); + }); + + describe('stop', () => { + it('should stop the watcher gracefully', async () => { + const watcher = new TaskWatcher(testDir, { pollInterval: 50 }); + + // Start watching, stop after a short delay + const watchPromise = watcher.watch(async () => { + // Should not be called since no tasks + }); + + // Stop after short delay + setTimeout(() => watcher.stop(), 100); + + await watchPromise; + + expect(watcher.isRunning()).toBe(false); + }); + + it('should abort sleep immediately when stopped', async () => { + const watcher = new TaskWatcher(testDir, { pollInterval: 10000 }); + + const start = Date.now(); + const watchPromise = watcher.watch(async () => {}); + + // Stop after 50ms, should not wait the full 10s + setTimeout(() => watcher.stop(), 50); + + await watchPromise; + + const elapsed = Date.now() - start; + // Should complete well under the 10s poll interval + expect(elapsed).toBeLessThan(1000); + }); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index 291ea44..799abd2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -31,6 +31,7 @@ import { switchConfig, addTask, refreshBuiltin, + watchTasks, } from './commands/index.js'; import { listWorkflows } from './config/workflowLoader.js'; import { selectOptionWithDefault } from './prompt/index.js'; @@ -114,9 +115,13 @@ program await refreshBuiltin(); return; + case 'watch': + await watchTasks(cwd); + return; + default: error(`Unknown command: /${command}`); - info('Available: /run-tasks, /add-task, /switch, /clear, /refresh-builtin, /help, /config'); + info('Available: /run-tasks, /watch, /add-task, /switch, /clear, /refresh-builtin, /help, /config'); process.exit(1); } } diff --git a/src/commands/help.ts b/src/commands/help.ts index c5cf42e..b852937 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -15,6 +15,7 @@ export function showHelp(): void { Usage: takt {task} Execute task with current workflow (continues session) takt /run-tasks Run all pending tasks from .takt/tasks/ + takt /watch Watch for tasks and auto-execute (stays resident) takt /add-task Add a new task (interactive, YAML format) takt /switch Switch workflow interactively takt /clear Clear agent conversation sessions (reset to initial state) @@ -26,6 +27,7 @@ Examples: takt /add-task "認証機能を追加する" # Quick add task takt /add-task # Interactive task creation takt /clear # Clear sessions, start fresh + takt /watch # Watch & auto-execute tasks takt /refresh-builtin # Update builtin resources takt /switch takt /run-tasks diff --git a/src/commands/index.ts b/src/commands/index.ts index 208b865..ecb2489 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -6,6 +6,7 @@ export { executeWorkflow, type WorkflowExecutionResult, type WorkflowExecutionOp export { executeTask, runAllTasks } from './taskExecution.js'; export { addTask } from './addTask.js'; export { refreshBuiltin } from './refreshBuiltin.js'; +export { watchTasks } from './watchTasks.js'; export { showHelp } from './help.js'; export { withAgentSession } from './session.js'; export { switchWorkflow } from './workflow.js'; diff --git a/src/commands/taskExecution.ts b/src/commands/taskExecution.ts index 436d494..6c40c0f 100644 --- a/src/commands/taskExecution.ts +++ b/src/commands/taskExecution.ts @@ -136,7 +136,7 @@ export async function runAllTasks( * Resolve execution directory and workflow from task data. * If the task has worktree settings, create a worktree and use it as cwd. */ -function resolveTaskExecution( +export function resolveTaskExecution( task: TaskInfo, defaultCwd: string, defaultWorkflow: string diff --git a/src/commands/watchTasks.ts b/src/commands/watchTasks.ts new file mode 100644 index 0000000..f0c7069 --- /dev/null +++ b/src/commands/watchTasks.ts @@ -0,0 +1,118 @@ +/** + * /watch command implementation + * + * Watches .takt/tasks/ for new task files and executes them automatically. + * 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 { + header, + info, + error, + success, + status, +} from '../utils/ui.js'; +import { createLogger } from '../utils/debug.js'; +import { getErrorMessage } from '../utils/error.js'; +import { executeTask, resolveTaskExecution } from './taskExecution.js'; +import { DEFAULT_WORKFLOW_NAME } from '../constants.js'; + +const log = createLogger('watch'); + +/** + * Watch for tasks and execute them as they appear. + * Runs until Ctrl+C. + */ +export async function watchTasks(cwd: string): Promise { + const workflowName = getCurrentWorkflow(cwd) || DEFAULT_WORKFLOW_NAME; + const taskRunner = new TaskRunner(cwd); + const watcher = new TaskWatcher(cwd); + + let taskCount = 0; + let successCount = 0; + let failCount = 0; + + header('TAKT Watch Mode'); + info(`Workflow: ${workflowName}`); + info(`Watching: ${taskRunner.getTasksDir()}`); + info('Waiting for tasks... (Ctrl+C to stop)'); + console.log(); + + // Graceful shutdown on SIGINT + const onSigInt = () => { + console.log(); + info('Stopping watch...'); + watcher.stop(); + }; + process.on('SIGINT', onSigInt); + + try { + await watcher.watch(async (task: TaskInfo) => { + taskCount++; + console.log(); + info(`=== Task ${taskCount}: ${task.name} ===`); + console.log(); + + const startedAt = new Date().toISOString(); + const executionLog: string[] = []; + + try { + const { execCwd, execWorkflow } = resolveTaskExecution(task, cwd, workflowName); + const taskSuccess = await executeTask(task.content, execCwd, execWorkflow); + const completedAt = new Date().toISOString(); + + taskRunner.completeTask({ + task, + success: taskSuccess, + response: taskSuccess ? 'Task completed successfully' : 'Task failed', + executionLog, + startedAt, + completedAt, + }); + + if (taskSuccess) { + successCount++; + success(`Task "${task.name}" completed`); + } else { + failCount++; + error(`Task "${task.name}" failed`); + } + } catch (err) { + failCount++; + const completedAt = new Date().toISOString(); + + taskRunner.completeTask({ + task, + success: false, + response: getErrorMessage(err), + executionLog, + startedAt, + completedAt, + }); + + error(`Task "${task.name}" error: ${getErrorMessage(err)}`); + } + + console.log(); + info('Waiting for tasks... (Ctrl+C to stop)'); + }); + } finally { + process.removeListener('SIGINT', onSigInt); + } + + // Summary on exit + if (taskCount > 0) { + console.log(); + header('Watch Summary'); + status('Total', String(taskCount)); + status('Success', String(successCount), successCount === taskCount ? 'green' : undefined); + if (failCount > 0) { + status('Failed', String(failCount), 'red'); + } + } + + success('Watch stopped.'); +} diff --git a/src/task/index.ts b/src/task/index.ts index 046ca19..543f9ff 100644 --- a/src/task/index.ts +++ b/src/task/index.ts @@ -13,3 +13,4 @@ export { showTaskList } from './display.js'; export { TaskFileSchema, type TaskFileData } from './schema.js'; export { parseTaskFile, parseTaskFiles, type ParsedTask } from './parser.js'; export { createWorktree, removeWorktree, type WorktreeOptions, type WorktreeResult } from './worktree.js'; +export { TaskWatcher, type TaskWatcherOptions } from './watcher.js'; diff --git a/src/task/watcher.ts b/src/task/watcher.ts new file mode 100644 index 0000000..6edfe78 --- /dev/null +++ b/src/task/watcher.ts @@ -0,0 +1,90 @@ +/** + * Task directory watcher + * + * Polls .takt/tasks/ for new task files and invokes a callback when found. + * Uses polling (not fs.watch) for cross-platform reliability. + */ + +import { createLogger } from '../utils/debug.js'; +import { TaskRunner, type TaskInfo } from './runner.js'; + +const log = createLogger('watcher'); + +export interface TaskWatcherOptions { + /** Polling interval in milliseconds (default: 2000) */ + pollInterval?: number; +} + +const DEFAULT_POLL_INTERVAL = 2000; + +export class TaskWatcher { + private runner: TaskRunner; + private pollInterval: number; + private running = false; + private abortController: AbortController | null = null; + + constructor(projectDir: string, options?: TaskWatcherOptions) { + this.runner = new TaskRunner(projectDir); + this.pollInterval = options?.pollInterval ?? DEFAULT_POLL_INTERVAL; + } + + /** + * Start watching for tasks. + * Resolves only when stop() is called. + */ + async watch(onTask: (task: TaskInfo) => Promise): Promise { + this.running = true; + this.abortController = new AbortController(); + + log.info('Watch started', { pollInterval: this.pollInterval }); + + while (this.running) { + const task = this.runner.getNextTask(); + + if (task) { + log.info('Task found', { name: task.name }); + await onTask(task); + // After task execution, immediately check for next task (no sleep) + continue; + } + + // No tasks: wait before next poll + await this.sleep(this.pollInterval); + } + + log.info('Watch stopped'); + } + + /** Stop watching */ + stop(): void { + this.running = false; + this.abortController?.abort(); + } + + /** Whether the watcher is currently active */ + isRunning(): boolean { + return this.running; + } + + /** + * Sleep with abort support. + * Resolves early if stop() is called. + */ + private sleep(ms: number): Promise { + return new Promise((resolve) => { + const signal = this.abortController?.signal; + + if (signal?.aborted) { + resolve(); + return; + } + + const timer = setTimeout(resolve, ms); + + signal?.addEventListener('abort', () => { + clearTimeout(timer); + resolve(); + }, { once: true }); + }); + } +}