タスクを監視する /watch を追加

This commit is contained in:
nrslib 2026-01-27 23:29:02 +09:00
parent 7270b29044
commit 7323d6d288
12 changed files with 670 additions and 85 deletions

View File

@ -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"`

176
README.md
View File

@ -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

View File

@ -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) - 設計思想と実践的な使い方ガイド
## ライセンス

View File

@ -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",

View File

@ -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);
});
});
});

View File

@ -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);
}
}

View File

@ -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

View File

@ -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';

View File

@ -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

118
src/commands/watchTasks.ts Normal file
View File

@ -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<void> {
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.');
}

View File

@ -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';

90
src/task/watcher.ts Normal file
View File

@ -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<void>): Promise<void> {
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<void> {
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 });
});
}
}