feat: answer status, autoCommit, permission_mode, verbose logging
- answer: planner が質問と判断したら COMPLETE で終了する仕組み - autoCommit: worktree タスク完了時に自動 git commit - permission_mode: workflow YAML でステップごとの権限指定 - verbose: verbose 時のファイル+stderr 二重出力修正
This commit is contained in:
parent
354e9c48a3
commit
d900ee8bc4
@ -39,6 +39,7 @@ steps:
|
|||||||
| Situation | Judgment |
|
| Situation | Judgment |
|
||||||
|-----------|----------|
|
|-----------|----------|
|
||||||
| Requirements clear and implementable | DONE |
|
| Requirements clear and implementable | DONE |
|
||||||
|
| User is asking a question (not an implementation task) | ANSWER |
|
||||||
| Requirements unclear, insufficient info | BLOCKED |
|
| Requirements unclear, insufficient info | BLOCKED |
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
@ -46,6 +47,7 @@ steps:
|
|||||||
| Situation | Tag |
|
| Situation | Tag |
|
||||||
|-----------|-----|
|
|-----------|-----|
|
||||||
| Analysis complete | `[PLANNER:DONE]` |
|
| Analysis complete | `[PLANNER:DONE]` |
|
||||||
|
| Question answered | `[PLANNER:ANSWER]` |
|
||||||
| Insufficient info | `[PLANNER:BLOCKED]` |
|
| Insufficient info | `[PLANNER:BLOCKED]` |
|
||||||
|
|
||||||
### Output Examples
|
### Output Examples
|
||||||
@ -55,6 +57,13 @@ steps:
|
|||||||
[PLANNER:DONE]
|
[PLANNER:DONE]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**ANSWER case:**
|
||||||
|
```
|
||||||
|
{Answer to the question}
|
||||||
|
|
||||||
|
[PLANNER:ANSWER]
|
||||||
|
```
|
||||||
|
|
||||||
**BLOCKED case:**
|
**BLOCKED case:**
|
||||||
```
|
```
|
||||||
[PLANNER:BLOCKED]
|
[PLANNER:BLOCKED]
|
||||||
@ -80,10 +89,15 @@ steps:
|
|||||||
## Instructions
|
## Instructions
|
||||||
Analyze the task and create an implementation plan.
|
Analyze the task and create an implementation plan.
|
||||||
|
|
||||||
|
**Judgment criteria:**
|
||||||
|
- If the user input is an implementation task → create a plan and output `[PLANNER:DONE]`
|
||||||
|
- If the user input is a question → research, answer, and output `[PLANNER:ANSWER]`
|
||||||
|
- If there is insufficient information → output `[PLANNER:BLOCKED]`
|
||||||
|
|
||||||
**Note:** If returned from implement step (Previous Response exists),
|
**Note:** If returned from implement step (Previous Response exists),
|
||||||
review and revise the plan based on that feedback (replan).
|
review and revise the plan based on that feedback (replan).
|
||||||
|
|
||||||
**Tasks:**
|
**Tasks (for implementation tasks):**
|
||||||
1. Understand the requirements
|
1. Understand the requirements
|
||||||
2. Identify impact scope
|
2. Identify impact scope
|
||||||
3. Decide implementation approach
|
3. Decide implementation approach
|
||||||
@ -117,6 +131,8 @@ steps:
|
|||||||
transitions:
|
transitions:
|
||||||
- condition: done
|
- condition: done
|
||||||
next_step: implement
|
next_step: implement
|
||||||
|
- condition: answer
|
||||||
|
next_step: COMPLETE
|
||||||
- condition: blocked
|
- condition: blocked
|
||||||
next_step: ABORT
|
next_step: ABORT
|
||||||
|
|
||||||
@ -131,6 +147,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
||||||
|
|
||||||
@ -360,6 +377,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
||||||
|
|
||||||
@ -545,6 +563,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
||||||
|
|
||||||
@ -727,6 +746,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
||||||
|
|
||||||
@ -796,6 +816,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ steps:
|
|||||||
| Situation | Judgment |
|
| Situation | Judgment |
|
||||||
|-----------|----------|
|
|-----------|----------|
|
||||||
| Requirements clear and implementable | DONE |
|
| Requirements clear and implementable | DONE |
|
||||||
|
| User is asking a question (not an implementation task) | ANSWER |
|
||||||
| Requirements unclear, insufficient info | BLOCKED |
|
| Requirements unclear, insufficient info | BLOCKED |
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
@ -47,6 +48,7 @@ steps:
|
|||||||
| Situation | Tag |
|
| Situation | Tag |
|
||||||
|-----------|-----|
|
|-----------|-----|
|
||||||
| Analysis complete | `[PLANNER:DONE]` |
|
| Analysis complete | `[PLANNER:DONE]` |
|
||||||
|
| Question answered | `[PLANNER:ANSWER]` |
|
||||||
| Insufficient info | `[PLANNER:BLOCKED]` |
|
| Insufficient info | `[PLANNER:BLOCKED]` |
|
||||||
|
|
||||||
### Output Examples
|
### Output Examples
|
||||||
@ -56,6 +58,13 @@ steps:
|
|||||||
[PLANNER:DONE]
|
[PLANNER:DONE]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**ANSWER case:**
|
||||||
|
```
|
||||||
|
{Answer to the question}
|
||||||
|
|
||||||
|
[PLANNER:ANSWER]
|
||||||
|
```
|
||||||
|
|
||||||
**BLOCKED case:**
|
**BLOCKED case:**
|
||||||
```
|
```
|
||||||
[PLANNER:BLOCKED]
|
[PLANNER:BLOCKED]
|
||||||
@ -81,10 +90,15 @@ steps:
|
|||||||
## Instructions
|
## Instructions
|
||||||
Analyze the task and create an implementation plan.
|
Analyze the task and create an implementation plan.
|
||||||
|
|
||||||
|
**Judgment criteria:**
|
||||||
|
- If the user input is an implementation task → create a plan and output `[PLANNER:DONE]`
|
||||||
|
- If the user input is a question → research, answer, and output `[PLANNER:ANSWER]`
|
||||||
|
- If there is insufficient information → output `[PLANNER:BLOCKED]`
|
||||||
|
|
||||||
**Note:** If returned from implement step (Previous Response exists),
|
**Note:** If returned from implement step (Previous Response exists),
|
||||||
review and revise the plan based on that feedback (replan).
|
review and revise the plan based on that feedback (replan).
|
||||||
|
|
||||||
**Tasks:**
|
**Tasks (for implementation tasks):**
|
||||||
1. Understand the requirements
|
1. Understand the requirements
|
||||||
2. Identify impact scope
|
2. Identify impact scope
|
||||||
3. Decide implementation approach
|
3. Decide implementation approach
|
||||||
@ -118,6 +132,8 @@ steps:
|
|||||||
transitions:
|
transitions:
|
||||||
- condition: done
|
- condition: done
|
||||||
next_step: implement
|
next_step: implement
|
||||||
|
- condition: answer
|
||||||
|
next_step: COMPLETE
|
||||||
- condition: blocked
|
- condition: blocked
|
||||||
next_step: ABORT
|
next_step: ABORT
|
||||||
|
|
||||||
@ -132,6 +148,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
# ⚠️ REQUIRED: Status Output Rules ⚠️
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ steps:
|
|||||||
| 状況 | 判定 |
|
| 状況 | 判定 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 要件が明確で実装可能 | DONE |
|
| 要件が明確で実装可能 | DONE |
|
||||||
|
| ユーザーが質問をしている(実装タスクではない) | ANSWER |
|
||||||
| 要件が不明確、情報不足 | BLOCKED |
|
| 要件が不明確、情報不足 | BLOCKED |
|
||||||
|
|
||||||
## 出力フォーマット
|
## 出力フォーマット
|
||||||
@ -46,6 +47,7 @@ steps:
|
|||||||
| 状況 | タグ |
|
| 状況 | タグ |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 分析完了 | `[PLANNER:DONE]` |
|
| 分析完了 | `[PLANNER:DONE]` |
|
||||||
|
| 質問への回答 | `[PLANNER:ANSWER]` |
|
||||||
| 情報不足 | `[PLANNER:BLOCKED]` |
|
| 情報不足 | `[PLANNER:BLOCKED]` |
|
||||||
|
|
||||||
### 出力例
|
### 出力例
|
||||||
@ -55,6 +57,13 @@ steps:
|
|||||||
[PLANNER:DONE]
|
[PLANNER:DONE]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**ANSWER の場合:**
|
||||||
|
```
|
||||||
|
{質問への回答}
|
||||||
|
|
||||||
|
[PLANNER:ANSWER]
|
||||||
|
```
|
||||||
|
|
||||||
**BLOCKED の場合:**
|
**BLOCKED の場合:**
|
||||||
```
|
```
|
||||||
[PLANNER:BLOCKED]
|
[PLANNER:BLOCKED]
|
||||||
@ -80,10 +89,15 @@ steps:
|
|||||||
## Instructions
|
## Instructions
|
||||||
タスクを分析し、実装方針を立ててください。
|
タスクを分析し、実装方針を立ててください。
|
||||||
|
|
||||||
|
**判断基準:**
|
||||||
|
- ユーザーの入力が実装タスクの場合 → 計画を立てて `[PLANNER:DONE]`
|
||||||
|
- ユーザーの入力が質問の場合 → 調査・回答して `[PLANNER:ANSWER]`
|
||||||
|
- 情報不足の場合 → `[PLANNER:BLOCKED]`
|
||||||
|
|
||||||
**注意:** Previous Responseがある場合は差し戻しのため、
|
**注意:** Previous Responseがある場合は差し戻しのため、
|
||||||
その内容を踏まえて計画を見直してください(replan)。
|
その内容を踏まえて計画を見直してください(replan)。
|
||||||
|
|
||||||
**やること:**
|
**やること(実装タスクの場合):**
|
||||||
1. タスクの要件を理解する
|
1. タスクの要件を理解する
|
||||||
2. 影響範囲を特定する
|
2. 影響範囲を特定する
|
||||||
3. 実装アプローチを決める
|
3. 実装アプローチを決める
|
||||||
@ -117,6 +131,8 @@ steps:
|
|||||||
transitions:
|
transitions:
|
||||||
- condition: done
|
- condition: done
|
||||||
next_step: implement
|
next_step: implement
|
||||||
|
- condition: answer
|
||||||
|
next_step: COMPLETE
|
||||||
- condition: blocked
|
- condition: blocked
|
||||||
next_step: ABORT
|
next_step: ABORT
|
||||||
|
|
||||||
@ -131,6 +147,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ 必須: ステータス出力ルール ⚠️
|
# ⚠️ 必須: ステータス出力ルール ⚠️
|
||||||
|
|
||||||
@ -372,6 +389,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ 必須: ステータス出力ルール ⚠️
|
# ⚠️ 必須: ステータス出力ルール ⚠️
|
||||||
|
|
||||||
@ -556,6 +574,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ 必須: ステータス出力ルール ⚠️
|
# ⚠️ 必須: ステータス出力ルール ⚠️
|
||||||
|
|
||||||
@ -737,6 +756,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ 必須: ステータス出力ルール ⚠️
|
# ⚠️ 必須: ステータス出力ルール ⚠️
|
||||||
|
|
||||||
@ -805,6 +825,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ 必須: ステータス出力ルール ⚠️
|
# ⚠️ 必須: ステータス出力ルール ⚠️
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ steps:
|
|||||||
| 状況 | 判定 |
|
| 状況 | 判定 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 要件が明確で実装可能 | DONE |
|
| 要件が明確で実装可能 | DONE |
|
||||||
|
| ユーザーが質問をしている(実装タスクではない) | ANSWER |
|
||||||
| 要件が不明確、情報不足 | BLOCKED |
|
| 要件が不明確、情報不足 | BLOCKED |
|
||||||
|
|
||||||
## 出力フォーマット
|
## 出力フォーマット
|
||||||
@ -47,6 +48,7 @@ steps:
|
|||||||
| 状況 | タグ |
|
| 状況 | タグ |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 分析完了 | `[PLANNER:DONE]` |
|
| 分析完了 | `[PLANNER:DONE]` |
|
||||||
|
| 質問への回答 | `[PLANNER:ANSWER]` |
|
||||||
| 情報不足 | `[PLANNER:BLOCKED]` |
|
| 情報不足 | `[PLANNER:BLOCKED]` |
|
||||||
|
|
||||||
### 出力例
|
### 出力例
|
||||||
@ -56,6 +58,13 @@ steps:
|
|||||||
[PLANNER:DONE]
|
[PLANNER:DONE]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**ANSWER の場合:**
|
||||||
|
```
|
||||||
|
{質問への回答}
|
||||||
|
|
||||||
|
[PLANNER:ANSWER]
|
||||||
|
```
|
||||||
|
|
||||||
**BLOCKED の場合:**
|
**BLOCKED の場合:**
|
||||||
```
|
```
|
||||||
[PLANNER:BLOCKED]
|
[PLANNER:BLOCKED]
|
||||||
@ -81,10 +90,15 @@ steps:
|
|||||||
## Instructions
|
## Instructions
|
||||||
タスクを分析し、実装方針を立ててください。
|
タスクを分析し、実装方針を立ててください。
|
||||||
|
|
||||||
|
**判断基準:**
|
||||||
|
- ユーザーの入力が実装タスクの場合 → 計画を立てて `[PLANNER:DONE]`
|
||||||
|
- ユーザーの入力が質問の場合 → 調査・回答して `[PLANNER:ANSWER]`
|
||||||
|
- 情報不足の場合 → `[PLANNER:BLOCKED]`
|
||||||
|
|
||||||
**注意:** Previous Responseがある場合は差し戻しのため、
|
**注意:** Previous Responseがある場合は差し戻しのため、
|
||||||
その内容を踏まえて計画を見直してください(replan)。
|
その内容を踏まえて計画を見直してください(replan)。
|
||||||
|
|
||||||
**やること:**
|
**やること(実装タスクの場合):**
|
||||||
1. タスクの要件を理解する
|
1. タスクの要件を理解する
|
||||||
2. 影響範囲を特定する
|
2. 影響範囲を特定する
|
||||||
3. 実装アプローチを決める
|
3. 実装アプローチを決める
|
||||||
@ -118,6 +132,8 @@ steps:
|
|||||||
transitions:
|
transitions:
|
||||||
- condition: done
|
- condition: done
|
||||||
next_step: implement
|
next_step: implement
|
||||||
|
- condition: answer
|
||||||
|
next_step: COMPLETE
|
||||||
- condition: blocked
|
- condition: blocked
|
||||||
next_step: ABORT
|
next_step: ABORT
|
||||||
|
|
||||||
@ -132,6 +148,7 @@ steps:
|
|||||||
- Bash
|
- Bash
|
||||||
- WebSearch
|
- WebSearch
|
||||||
- WebFetch
|
- WebFetch
|
||||||
|
permission_mode: acceptEdits
|
||||||
status_rules_prompt: |
|
status_rules_prompt: |
|
||||||
# ⚠️ 必須: ステータス出力ルール ⚠️
|
# ⚠️ 必須: ステータス出力ルール ⚠️
|
||||||
|
|
||||||
|
|||||||
142
src/__tests__/autoCommit.test.ts
Normal file
142
src/__tests__/autoCommit.test.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* Tests for autoCommitWorktree
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { autoCommitWorktree } from '../task/autoCommit.js';
|
||||||
|
|
||||||
|
// Mock child_process.execFileSync
|
||||||
|
vi.mock('node:child_process', () => ({
|
||||||
|
execFileSync: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
const mockExecFileSync = vi.mocked(execFileSync);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('autoCommitWorktree', () => {
|
||||||
|
it('should create a commit when there are changes', () => {
|
||||||
|
// git add -A: no output needed
|
||||||
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
||||||
|
const argsArr = args as string[];
|
||||||
|
if (argsArr[0] === 'status') {
|
||||||
|
return 'M src/index.ts\n';
|
||||||
|
}
|
||||||
|
if (argsArr[0] === 'rev-parse') {
|
||||||
|
return 'abc1234\n';
|
||||||
|
}
|
||||||
|
return Buffer.from('');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = autoCommitWorktree('/tmp/worktree', 'my-task');
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.commitHash).toBe('abc1234');
|
||||||
|
expect(result.message).toContain('abc1234');
|
||||||
|
|
||||||
|
// Verify git add -A was called
|
||||||
|
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||||
|
'git',
|
||||||
|
['add', '-A'],
|
||||||
|
expect.objectContaining({ cwd: '/tmp/worktree' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify commit was called with correct message (no co-author)
|
||||||
|
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||||
|
'git',
|
||||||
|
['commit', '-m', 'takt: my-task'],
|
||||||
|
expect.objectContaining({ cwd: '/tmp/worktree' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success with no commit when there are no changes', () => {
|
||||||
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
||||||
|
const argsArr = args as string[];
|
||||||
|
if (argsArr[0] === 'status') {
|
||||||
|
return ''; // No changes
|
||||||
|
}
|
||||||
|
return Buffer.from('');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = autoCommitWorktree('/tmp/worktree', 'my-task');
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.commitHash).toBeUndefined();
|
||||||
|
expect(result.message).toBe('No changes to commit');
|
||||||
|
|
||||||
|
// Verify git add -A was called
|
||||||
|
expect(mockExecFileSync).toHaveBeenCalledWith(
|
||||||
|
'git',
|
||||||
|
['add', '-A'],
|
||||||
|
expect.objectContaining({ cwd: '/tmp/worktree' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify commit was NOT called
|
||||||
|
expect(mockExecFileSync).not.toHaveBeenCalledWith(
|
||||||
|
'git',
|
||||||
|
['commit', '-m', expect.any(String)],
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return failure when git command fails', () => {
|
||||||
|
mockExecFileSync.mockImplementation(() => {
|
||||||
|
throw new Error('git error: not a git repository');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = autoCommitWorktree('/tmp/worktree', 'my-task');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.commitHash).toBeUndefined();
|
||||||
|
expect(result.message).toContain('Auto-commit failed');
|
||||||
|
expect(result.message).toContain('not a git repository');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include co-author in commit message', () => {
|
||||||
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
||||||
|
const argsArr = args as string[];
|
||||||
|
if (argsArr[0] === 'status') {
|
||||||
|
return 'M file.ts\n';
|
||||||
|
}
|
||||||
|
if (argsArr[0] === 'rev-parse') {
|
||||||
|
return 'def5678\n';
|
||||||
|
}
|
||||||
|
return Buffer.from('');
|
||||||
|
});
|
||||||
|
|
||||||
|
autoCommitWorktree('/tmp/worktree', 'test-task');
|
||||||
|
|
||||||
|
// Find the commit call
|
||||||
|
const commitCall = mockExecFileSync.mock.calls.find(
|
||||||
|
call => (call[1] as string[])[0] === 'commit'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(commitCall).toBeDefined();
|
||||||
|
const commitMessage = (commitCall![1] as string[])[2];
|
||||||
|
expect(commitMessage).toBe('takt: test-task');
|
||||||
|
expect(commitMessage).not.toContain('Co-Authored-By');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the correct commit message format', () => {
|
||||||
|
mockExecFileSync.mockImplementation((cmd, args) => {
|
||||||
|
const argsArr = args as string[];
|
||||||
|
if (argsArr[0] === 'status') {
|
||||||
|
return 'A new-file.ts\n';
|
||||||
|
}
|
||||||
|
if (argsArr[0] === 'rev-parse') {
|
||||||
|
return 'aaa1111\n';
|
||||||
|
}
|
||||||
|
return Buffer.from('');
|
||||||
|
});
|
||||||
|
|
||||||
|
autoCommitWorktree('/tmp/worktree', '認証機能を追加する');
|
||||||
|
|
||||||
|
const commitCall = mockExecFileSync.mock.calls.find(
|
||||||
|
call => (call[1] as string[])[0] === 'commit'
|
||||||
|
);
|
||||||
|
expect((commitCall![1] as string[])[2]).toBe('takt: 認証機能を追加する');
|
||||||
|
});
|
||||||
|
});
|
||||||
233
src/__tests__/debug.test.ts
Normal file
233
src/__tests__/debug.test.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/**
|
||||||
|
* Tests for debug logging utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
initDebugLogger,
|
||||||
|
resetDebugLogger,
|
||||||
|
createLogger,
|
||||||
|
isDebugEnabled,
|
||||||
|
getDebugLogFile,
|
||||||
|
setVerboseConsole,
|
||||||
|
isVerboseConsole,
|
||||||
|
debugLog,
|
||||||
|
infoLog,
|
||||||
|
errorLog,
|
||||||
|
} from '../utils/debug.js';
|
||||||
|
import { existsSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
|
||||||
|
describe('debug logging', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetDebugLogger();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetDebugLogger();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initDebugLogger', () => {
|
||||||
|
it('should not enable debug when config is undefined', () => {
|
||||||
|
initDebugLogger(undefined, '/tmp');
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
expect(getDebugLogFile()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not enable debug when enabled is false', () => {
|
||||||
|
initDebugLogger({ enabled: false }, '/tmp');
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable debug when enabled is true', () => {
|
||||||
|
initDebugLogger({ enabled: true }, '/tmp');
|
||||||
|
expect(isDebugEnabled()).toBe(true);
|
||||||
|
expect(getDebugLogFile()).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use custom log file when provided', () => {
|
||||||
|
const logDir = join(tmpdir(), 'takt-test-debug-' + Date.now());
|
||||||
|
mkdirSync(logDir, { recursive: true });
|
||||||
|
const logFile = join(logDir, 'test.log');
|
||||||
|
|
||||||
|
try {
|
||||||
|
initDebugLogger({ enabled: true, logFile }, '/tmp');
|
||||||
|
expect(getDebugLogFile()).toBe(logFile);
|
||||||
|
expect(existsSync(logFile)).toBe(true);
|
||||||
|
|
||||||
|
const content = readFileSync(logFile, 'utf-8');
|
||||||
|
expect(content).toContain('TAKT Debug Log');
|
||||||
|
} finally {
|
||||||
|
rmSync(logDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only initialize once', () => {
|
||||||
|
initDebugLogger({ enabled: true }, '/tmp');
|
||||||
|
const firstFile = getDebugLogFile();
|
||||||
|
|
||||||
|
initDebugLogger({ enabled: false }, '/tmp');
|
||||||
|
expect(isDebugEnabled()).toBe(true);
|
||||||
|
expect(getDebugLogFile()).toBe(firstFile);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetDebugLogger', () => {
|
||||||
|
it('should reset all state', () => {
|
||||||
|
initDebugLogger({ enabled: true }, '/tmp');
|
||||||
|
setVerboseConsole(true);
|
||||||
|
|
||||||
|
resetDebugLogger();
|
||||||
|
|
||||||
|
expect(isDebugEnabled()).toBe(false);
|
||||||
|
expect(getDebugLogFile()).toBeNull();
|
||||||
|
expect(isVerboseConsole()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setVerboseConsole / isVerboseConsole', () => {
|
||||||
|
it('should default to false', () => {
|
||||||
|
expect(isVerboseConsole()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable verbose console', () => {
|
||||||
|
setVerboseConsole(true);
|
||||||
|
expect(isVerboseConsole()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable verbose console', () => {
|
||||||
|
setVerboseConsole(true);
|
||||||
|
setVerboseConsole(false);
|
||||||
|
expect(isVerboseConsole()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verbose console output', () => {
|
||||||
|
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
stderrSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not output to stderr when verbose is disabled', () => {
|
||||||
|
debugLog('test', 'hello');
|
||||||
|
expect(stderrSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output debug to stderr when verbose is enabled', () => {
|
||||||
|
setVerboseConsole(true);
|
||||||
|
debugLog('test', 'hello debug');
|
||||||
|
|
||||||
|
expect(stderrSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const output = stderrSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(output).toContain('[DEBUG]');
|
||||||
|
expect(output).toContain('[test]');
|
||||||
|
expect(output).toContain('hello debug');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output info to stderr when verbose is enabled', () => {
|
||||||
|
setVerboseConsole(true);
|
||||||
|
infoLog('mycomp', 'info message');
|
||||||
|
|
||||||
|
expect(stderrSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const output = stderrSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(output).toContain('[INFO]');
|
||||||
|
expect(output).toContain('[mycomp]');
|
||||||
|
expect(output).toContain('info message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output error to stderr when verbose is enabled', () => {
|
||||||
|
setVerboseConsole(true);
|
||||||
|
errorLog('mycomp', 'error message');
|
||||||
|
|
||||||
|
expect(stderrSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const output = stderrSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(output).toContain('[ERROR]');
|
||||||
|
expect(output).toContain('[mycomp]');
|
||||||
|
expect(output).toContain('error message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include timestamp in console output', () => {
|
||||||
|
setVerboseConsole(true);
|
||||||
|
debugLog('test', 'with timestamp');
|
||||||
|
|
||||||
|
const output = stderrSpy.mock.calls[0]?.[0] as string;
|
||||||
|
// Timestamp format: HH:mm:ss.SSS
|
||||||
|
expect(output).toMatch(/\[\d{2}:\d{2}:\d{2}\.\d{3}\]/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createLogger', () => {
|
||||||
|
it('should create a logger with the given component name', () => {
|
||||||
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||||||
|
setVerboseConsole(true);
|
||||||
|
|
||||||
|
const log = createLogger('my-component');
|
||||||
|
log.debug('test message');
|
||||||
|
|
||||||
|
const output = stderrSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(output).toContain('[my-component]');
|
||||||
|
|
||||||
|
stderrSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide debug, info, error, enter, exit methods', () => {
|
||||||
|
const log = createLogger('test');
|
||||||
|
expect(typeof log.debug).toBe('function');
|
||||||
|
expect(typeof log.info).toBe('function');
|
||||||
|
expect(typeof log.error).toBe('function');
|
||||||
|
expect(typeof log.enter).toBe('function');
|
||||||
|
expect(typeof log.exit).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('file logging with verbose console', () => {
|
||||||
|
it('should write to both file and stderr when both are enabled', () => {
|
||||||
|
const logDir = join(tmpdir(), 'takt-test-debug-both-' + Date.now());
|
||||||
|
mkdirSync(logDir, { recursive: true });
|
||||||
|
const logFile = join(logDir, 'test.log');
|
||||||
|
|
||||||
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
initDebugLogger({ enabled: true, logFile }, '/tmp');
|
||||||
|
setVerboseConsole(true);
|
||||||
|
|
||||||
|
debugLog('test', 'dual output');
|
||||||
|
|
||||||
|
// Check stderr
|
||||||
|
expect(stderrSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const stderrOutput = stderrSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(stderrOutput).toContain('dual output');
|
||||||
|
|
||||||
|
// Check file
|
||||||
|
const fileContent = readFileSync(logFile, 'utf-8');
|
||||||
|
expect(fileContent).toContain('dual output');
|
||||||
|
} finally {
|
||||||
|
stderrSpy.mockRestore();
|
||||||
|
rmSync(logDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should output to stderr even when file logging is disabled', () => {
|
||||||
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// File logging not enabled, but verbose console is
|
||||||
|
setVerboseConsole(true);
|
||||||
|
debugLog('test', 'console only');
|
||||||
|
|
||||||
|
expect(stderrSpy).toHaveBeenCalledTimes(1);
|
||||||
|
const output = stderrSpy.mock.calls[0]?.[0] as string;
|
||||||
|
expect(output).toContain('console only');
|
||||||
|
} finally {
|
||||||
|
stderrSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
AgentTypeSchema,
|
AgentTypeSchema,
|
||||||
StatusSchema,
|
StatusSchema,
|
||||||
TransitionConditionSchema,
|
TransitionConditionSchema,
|
||||||
|
PermissionModeSchema,
|
||||||
WorkflowConfigRawSchema,
|
WorkflowConfigRawSchema,
|
||||||
CustomAgentConfigSchema,
|
CustomAgentConfigSchema,
|
||||||
GlobalConfigSchema,
|
GlobalConfigSchema,
|
||||||
@ -33,6 +34,7 @@ describe('StatusSchema', () => {
|
|||||||
expect(StatusSchema.parse('approved')).toBe('approved');
|
expect(StatusSchema.parse('approved')).toBe('approved');
|
||||||
expect(StatusSchema.parse('rejected')).toBe('rejected');
|
expect(StatusSchema.parse('rejected')).toBe('rejected');
|
||||||
expect(StatusSchema.parse('blocked')).toBe('blocked');
|
expect(StatusSchema.parse('blocked')).toBe('blocked');
|
||||||
|
expect(StatusSchema.parse('answer')).toBe('answer');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid statuses', () => {
|
it('should reject invalid statuses', () => {
|
||||||
@ -47,6 +49,7 @@ describe('TransitionConditionSchema', () => {
|
|||||||
expect(TransitionConditionSchema.parse('approved')).toBe('approved');
|
expect(TransitionConditionSchema.parse('approved')).toBe('approved');
|
||||||
expect(TransitionConditionSchema.parse('rejected')).toBe('rejected');
|
expect(TransitionConditionSchema.parse('rejected')).toBe('rejected');
|
||||||
expect(TransitionConditionSchema.parse('always')).toBe('always');
|
expect(TransitionConditionSchema.parse('always')).toBe('always');
|
||||||
|
expect(TransitionConditionSchema.parse('answer')).toBe('answer');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject invalid conditions', () => {
|
it('should reject invalid conditions', () => {
|
||||||
@ -55,6 +58,19 @@ describe('TransitionConditionSchema', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PermissionModeSchema', () => {
|
||||||
|
it('should accept valid permission modes', () => {
|
||||||
|
expect(PermissionModeSchema.parse('default')).toBe('default');
|
||||||
|
expect(PermissionModeSchema.parse('acceptEdits')).toBe('acceptEdits');
|
||||||
|
expect(PermissionModeSchema.parse('bypassPermissions')).toBe('bypassPermissions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid permission modes', () => {
|
||||||
|
expect(() => PermissionModeSchema.parse('readOnly')).toThrow();
|
||||||
|
expect(() => PermissionModeSchema.parse('admin')).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('WorkflowConfigRawSchema', () => {
|
describe('WorkflowConfigRawSchema', () => {
|
||||||
it('should parse valid workflow config', () => {
|
it('should parse valid workflow config', () => {
|
||||||
const config = {
|
const config = {
|
||||||
@ -80,6 +96,61 @@ describe('WorkflowConfigRawSchema', () => {
|
|||||||
expect(result.max_iterations).toBe(10);
|
expect(result.max_iterations).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse step with permission_mode', () => {
|
||||||
|
const config = {
|
||||||
|
name: 'test-workflow',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: 'implement',
|
||||||
|
agent: 'coder',
|
||||||
|
allowed_tools: ['Read', 'Edit', 'Write', 'Bash'],
|
||||||
|
permission_mode: 'acceptEdits',
|
||||||
|
instruction: '{task}',
|
||||||
|
transitions: [
|
||||||
|
{ condition: 'done', next_step: 'COMPLETE' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = WorkflowConfigRawSchema.parse(config);
|
||||||
|
expect(result.steps[0]?.permission_mode).toBe('acceptEdits');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow omitting permission_mode', () => {
|
||||||
|
const config = {
|
||||||
|
name: 'test-workflow',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: 'plan',
|
||||||
|
agent: 'planner',
|
||||||
|
instruction: '{task}',
|
||||||
|
transitions: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = WorkflowConfigRawSchema.parse(config);
|
||||||
|
expect(result.steps[0]?.permission_mode).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid permission_mode', () => {
|
||||||
|
const config = {
|
||||||
|
name: 'test-workflow',
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: 'step1',
|
||||||
|
agent: 'coder',
|
||||||
|
permission_mode: 'superAdmin',
|
||||||
|
instruction: '{task}',
|
||||||
|
transitions: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => WorkflowConfigRawSchema.parse(config)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('should require at least one step', () => {
|
it('should require at least one step', () => {
|
||||||
const config = {
|
const config = {
|
||||||
name: 'empty-workflow',
|
name: 'empty-workflow',
|
||||||
@ -172,6 +243,7 @@ describe('GENERIC_STATUS_PATTERNS', () => {
|
|||||||
expect(GENERIC_STATUS_PATTERNS.done).toBeDefined();
|
expect(GENERIC_STATUS_PATTERNS.done).toBeDefined();
|
||||||
expect(GENERIC_STATUS_PATTERNS.blocked).toBeDefined();
|
expect(GENERIC_STATUS_PATTERNS.blocked).toBeDefined();
|
||||||
expect(GENERIC_STATUS_PATTERNS.improve).toBeDefined();
|
expect(GENERIC_STATUS_PATTERNS.improve).toBeDefined();
|
||||||
|
expect(GENERIC_STATUS_PATTERNS.answer).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have valid regex patterns', () => {
|
it('should have valid regex patterns', () => {
|
||||||
@ -187,5 +259,7 @@ describe('GENERIC_STATUS_PATTERNS', () => {
|
|||||||
expect(new RegExp(GENERIC_STATUS_PATTERNS.done).test('[CUSTOM:DONE]')).toBe(true);
|
expect(new RegExp(GENERIC_STATUS_PATTERNS.done).test('[CUSTOM:DONE]')).toBe(true);
|
||||||
expect(new RegExp(GENERIC_STATUS_PATTERNS.done).test('[CODER:FIXED]')).toBe(true);
|
expect(new RegExp(GENERIC_STATUS_PATTERNS.done).test('[CODER:FIXED]')).toBe(true);
|
||||||
expect(new RegExp(GENERIC_STATUS_PATTERNS.improve).test('[MAGI:IMPROVE]')).toBe(true);
|
expect(new RegExp(GENERIC_STATUS_PATTERNS.improve).test('[MAGI:IMPROVE]')).toBe(true);
|
||||||
|
expect(new RegExp(GENERIC_STATUS_PATTERNS.answer).test('[PLANNER:ANSWER]')).toBe(true);
|
||||||
|
expect(new RegExp(GENERIC_STATUS_PATTERNS.answer).test('[MY_AGENT:ANSWER]')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,7 +15,10 @@ import { loadCustomAgents, loadAgentPrompt } from '../config/loader.js';
|
|||||||
import { loadGlobalConfig } from '../config/globalConfig.js';
|
import { loadGlobalConfig } from '../config/globalConfig.js';
|
||||||
import { loadProjectConfig } from '../config/projectConfig.js';
|
import { loadProjectConfig } from '../config/projectConfig.js';
|
||||||
import { getProvider, type ProviderType, type ProviderCallOptions } from '../providers/index.js';
|
import { getProvider, type ProviderType, type ProviderCallOptions } from '../providers/index.js';
|
||||||
import type { AgentResponse, CustomAgentConfig } from '../models/types.js';
|
import type { AgentResponse, CustomAgentConfig, PermissionMode } from '../models/types.js';
|
||||||
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
|
||||||
|
const log = createLogger('runner');
|
||||||
|
|
||||||
export type { StreamCallback };
|
export type { StreamCallback };
|
||||||
|
|
||||||
@ -31,6 +34,8 @@ export interface RunAgentOptions {
|
|||||||
allowedTools?: string[];
|
allowedTools?: string[];
|
||||||
/** Status output rules to inject into system prompt */
|
/** Status output rules to inject into system prompt */
|
||||||
statusRulesPrompt?: string;
|
statusRulesPrompt?: string;
|
||||||
|
/** Permission mode for tool execution (from workflow step) */
|
||||||
|
permissionMode?: PermissionMode;
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
@ -103,6 +108,7 @@ export async function runCustomAgent(
|
|||||||
sessionId: options.sessionId,
|
sessionId: options.sessionId,
|
||||||
allowedTools,
|
allowedTools,
|
||||||
model: resolveModel(options.cwd, options, agentConfig),
|
model: resolveModel(options.cwd, options, agentConfig),
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
@ -118,6 +124,7 @@ export async function runCustomAgent(
|
|||||||
sessionId: options.sessionId,
|
sessionId: options.sessionId,
|
||||||
allowedTools,
|
allowedTools,
|
||||||
model: resolveModel(options.cwd, options, agentConfig),
|
model: resolveModel(options.cwd, options, agentConfig),
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
@ -143,6 +150,7 @@ export async function runCustomAgent(
|
|||||||
allowedTools,
|
allowedTools,
|
||||||
model: resolveModel(options.cwd, options, agentConfig),
|
model: resolveModel(options.cwd, options, agentConfig),
|
||||||
statusPatterns: agentConfig.statusPatterns,
|
statusPatterns: agentConfig.statusPatterns,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
@ -194,6 +202,15 @@ export async function runAgent(
|
|||||||
options: RunAgentOptions
|
options: RunAgentOptions
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
const agentName = extractAgentName(agentSpec);
|
const agentName = extractAgentName(agentSpec);
|
||||||
|
log.debug('Running agent', {
|
||||||
|
agentSpec,
|
||||||
|
agentName,
|
||||||
|
provider: options.provider,
|
||||||
|
model: options.model,
|
||||||
|
hasAgentPath: !!options.agentPath,
|
||||||
|
hasSession: !!options.sessionId,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
|
});
|
||||||
|
|
||||||
// If agentPath is provided (from workflow), use it to load prompt
|
// If agentPath is provided (from workflow), use it to load prompt
|
||||||
if (options.agentPath) {
|
if (options.agentPath) {
|
||||||
@ -216,6 +233,7 @@ export async function runAgent(
|
|||||||
allowedTools: options.allowedTools,
|
allowedTools: options.allowedTools,
|
||||||
model: resolveModel(options.cwd, options),
|
model: resolveModel(options.cwd, options),
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { executeClaudeCli, type ClaudeSpawnOptions, type StreamCallback, type PermissionHandler, type AskUserQuestionHandler } from './process.js';
|
import { executeClaudeCli, type ClaudeSpawnOptions, type StreamCallback, type PermissionHandler, type AskUserQuestionHandler } from './process.js';
|
||||||
import type { AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
|
import type { AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import type { AgentResponse, Status } from '../models/types.js';
|
import type { AgentResponse, Status, PermissionMode } from '../models/types.js';
|
||||||
import { GENERIC_STATUS_PATTERNS } from '../models/schemas.js';
|
import { GENERIC_STATUS_PATTERNS } from '../models/schemas.js';
|
||||||
|
|
||||||
/** Options for calling Claude */
|
/** Options for calling Claude */
|
||||||
@ -20,6 +20,8 @@ export interface ClaudeCallOptions {
|
|||||||
statusPatterns?: Record<string, string>;
|
statusPatterns?: Record<string, string>;
|
||||||
/** SDK agents to register for sub-agent execution */
|
/** SDK agents to register for sub-agent execution */
|
||||||
agents?: Record<string, AgentDefinition>;
|
agents?: Record<string, AgentDefinition>;
|
||||||
|
/** Permission mode for tool execution (from workflow step) */
|
||||||
|
permissionMode?: PermissionMode;
|
||||||
/** Enable streaming mode with callback for real-time output */
|
/** Enable streaming mode with callback for real-time output */
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
/** Custom permission handler for interactive permission prompts */
|
/** Custom permission handler for interactive permission prompts */
|
||||||
@ -112,6 +114,7 @@ export async function callClaude(
|
|||||||
maxTurns: options.maxTurns,
|
maxTurns: options.maxTurns,
|
||||||
systemPrompt: options.systemPrompt,
|
systemPrompt: options.systemPrompt,
|
||||||
agents: options.agents,
|
agents: options.agents,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
@ -145,6 +148,7 @@ export async function callClaudeCustom(
|
|||||||
model: options.model,
|
model: options.model,
|
||||||
maxTurns: options.maxTurns,
|
maxTurns: options.maxTurns,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
@ -193,6 +197,7 @@ export async function callClaudeSkill(
|
|||||||
allowedTools: options.allowedTools,
|
allowedTools: options.allowedTools,
|
||||||
model: options.model,
|
model: options.model,
|
||||||
maxTurns: options.maxTurns,
|
maxTurns: options.maxTurns,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
|
|||||||
32
src/cli.ts
32
src/cli.ts
@ -22,7 +22,7 @@ import {
|
|||||||
} from './config/index.js';
|
} from './config/index.js';
|
||||||
import { clearAgentSessions, getCurrentWorkflow, isVerboseMode } from './config/paths.js';
|
import { clearAgentSessions, getCurrentWorkflow, isVerboseMode } from './config/paths.js';
|
||||||
import { info, error, success, setLogLevel } from './utils/ui.js';
|
import { info, error, success, setLogLevel } from './utils/ui.js';
|
||||||
import { initDebugLogger, createLogger } from './utils/debug.js';
|
import { initDebugLogger, createLogger, setVerboseConsole } from './utils/debug.js';
|
||||||
import {
|
import {
|
||||||
executeTask,
|
executeTask,
|
||||||
runAllTasks,
|
runAllTasks,
|
||||||
@ -57,25 +57,33 @@ program
|
|||||||
// Initialize project directories (.takt/)
|
// Initialize project directories (.takt/)
|
||||||
initProjectDirs(cwd);
|
initProjectDirs(cwd);
|
||||||
|
|
||||||
// Initialize debug logger from config
|
// Determine verbose mode and initialize logging
|
||||||
const debugConfig = getEffectiveDebugConfig(cwd);
|
const verbose = isVerboseMode(cwd);
|
||||||
|
let debugConfig = getEffectiveDebugConfig(cwd);
|
||||||
|
|
||||||
|
// verbose=true enables file logging automatically
|
||||||
|
if (verbose && (!debugConfig || !debugConfig.enabled)) {
|
||||||
|
debugConfig = { enabled: true };
|
||||||
|
}
|
||||||
|
|
||||||
initDebugLogger(debugConfig, cwd);
|
initDebugLogger(debugConfig, cwd);
|
||||||
|
|
||||||
|
// Enable verbose console output (stderr) for debug logs
|
||||||
|
if (verbose) {
|
||||||
|
setVerboseConsole(true);
|
||||||
|
setLogLevel('debug');
|
||||||
|
} else {
|
||||||
|
const config = loadGlobalConfig();
|
||||||
|
setLogLevel(config.logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
log.info('TAKT CLI starting', {
|
log.info('TAKT CLI starting', {
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
cwd,
|
cwd,
|
||||||
task: task || null,
|
task: task || null,
|
||||||
|
verbose,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set log level from config
|
|
||||||
if (isVerboseMode(cwd)) {
|
|
||||||
setLogLevel('debug');
|
|
||||||
log.debug('Verbose mode enabled (from config)');
|
|
||||||
} else {
|
|
||||||
const config = loadGlobalConfig();
|
|
||||||
setLogLevel(config.logLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle slash commands
|
// Handle slash commands
|
||||||
if (task?.startsWith('/')) {
|
if (task?.startsWith('/')) {
|
||||||
const parts = task.slice(1).split(/\s+/);
|
const parts = task.slice(1).split(/\s+/);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
import { loadWorkflow } from '../config/index.js';
|
import { loadWorkflow } from '../config/index.js';
|
||||||
import { TaskRunner, type TaskInfo } from '../task/index.js';
|
import { TaskRunner, type TaskInfo } from '../task/index.js';
|
||||||
import { createWorktree } from '../task/worktree.js';
|
import { createWorktree } from '../task/worktree.js';
|
||||||
|
import { autoCommitWorktree } from '../task/autoCommit.js';
|
||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
info,
|
info,
|
||||||
@ -45,6 +46,71 @@ export async function executeTask(
|
|||||||
return result.success;
|
return result.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a task: resolve worktree → run workflow → auto-commit → record completion.
|
||||||
|
*
|
||||||
|
* Shared by runAllTasks() and watchTasks() to avoid duplicated
|
||||||
|
* resolve → execute → autoCommit → complete logic.
|
||||||
|
*
|
||||||
|
* @returns true if the task succeeded
|
||||||
|
*/
|
||||||
|
export async function executeAndCompleteTask(
|
||||||
|
task: TaskInfo,
|
||||||
|
taskRunner: TaskRunner,
|
||||||
|
cwd: string,
|
||||||
|
workflowName: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
const executionLog: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { execCwd, execWorkflow, isWorktree } = resolveTaskExecution(task, cwd, workflowName);
|
||||||
|
|
||||||
|
const taskSuccess = await executeTask(task.content, execCwd, execWorkflow);
|
||||||
|
const completedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
if (taskSuccess && isWorktree) {
|
||||||
|
const commitResult = autoCommitWorktree(execCwd, task.name);
|
||||||
|
if (commitResult.success && commitResult.commitHash) {
|
||||||
|
info(`Auto-committed: ${commitResult.commitHash}`);
|
||||||
|
} else if (!commitResult.success) {
|
||||||
|
error(`Auto-commit failed: ${commitResult.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taskRunner.completeTask({
|
||||||
|
task,
|
||||||
|
success: taskSuccess,
|
||||||
|
response: taskSuccess ? 'Task completed successfully' : 'Task failed',
|
||||||
|
executionLog,
|
||||||
|
startedAt,
|
||||||
|
completedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (taskSuccess) {
|
||||||
|
success(`Task "${task.name}" completed`);
|
||||||
|
} else {
|
||||||
|
error(`Task "${task.name}" failed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskSuccess;
|
||||||
|
} catch (err) {
|
||||||
|
const completedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
taskRunner.completeTask({
|
||||||
|
task,
|
||||||
|
success: false,
|
||||||
|
response: getErrorMessage(err),
|
||||||
|
executionLog,
|
||||||
|
startedAt,
|
||||||
|
completedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
error(`Task "${task.name}" error: ${getErrorMessage(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run all pending tasks from .takt/tasks/
|
* Run all pending tasks from .takt/tasks/
|
||||||
*
|
*
|
||||||
@ -76,46 +142,12 @@ export async function runAllTasks(
|
|||||||
info(`=== Task: ${task.name} ===`);
|
info(`=== Task: ${task.name} ===`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const startedAt = new Date().toISOString();
|
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName);
|
||||||
const executionLog: string[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Resolve execution directory and workflow from task data
|
|
||||||
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) {
|
if (taskSuccess) {
|
||||||
successCount++;
|
successCount++;
|
||||||
success(`Task "${task.name}" completed`);
|
|
||||||
} else {
|
} else {
|
||||||
failCount++;
|
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)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 次のタスクを動的に取得(新しく追加されたタスクも含む)
|
// 次のタスクを動的に取得(新しく追加されたタスクも含む)
|
||||||
@ -140,15 +172,16 @@ export function resolveTaskExecution(
|
|||||||
task: TaskInfo,
|
task: TaskInfo,
|
||||||
defaultCwd: string,
|
defaultCwd: string,
|
||||||
defaultWorkflow: string
|
defaultWorkflow: string
|
||||||
): { execCwd: string; execWorkflow: string } {
|
): { execCwd: string; execWorkflow: string; isWorktree: boolean } {
|
||||||
const data = task.data;
|
const data = task.data;
|
||||||
|
|
||||||
// No structured data: use defaults
|
// No structured data: use defaults
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return { execCwd: defaultCwd, execWorkflow: defaultWorkflow };
|
return { execCwd: defaultCwd, execWorkflow: defaultWorkflow, isWorktree: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
let execCwd = defaultCwd;
|
let execCwd = defaultCwd;
|
||||||
|
let isWorktree = false;
|
||||||
|
|
||||||
// Handle worktree
|
// Handle worktree
|
||||||
if (data.worktree) {
|
if (data.worktree) {
|
||||||
@ -158,11 +191,12 @@ export function resolveTaskExecution(
|
|||||||
taskSlug: task.name,
|
taskSlug: task.name,
|
||||||
});
|
});
|
||||||
execCwd = result.path;
|
execCwd = result.path;
|
||||||
|
isWorktree = true;
|
||||||
info(`Worktree created: ${result.path} (branch: ${result.branch})`);
|
info(`Worktree created: ${result.path} (branch: ${result.branch})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle workflow override
|
// Handle workflow override
|
||||||
const execWorkflow = data.workflow || defaultWorkflow;
|
const execWorkflow = data.workflow || defaultWorkflow;
|
||||||
|
|
||||||
return { execCwd, execWorkflow };
|
return { execCwd, execWorkflow, isWorktree };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,10 @@ import { getCurrentWorkflow } from '../config/paths.js';
|
|||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
info,
|
info,
|
||||||
error,
|
|
||||||
success,
|
success,
|
||||||
status,
|
status,
|
||||||
} from '../utils/ui.js';
|
} from '../utils/ui.js';
|
||||||
import { getErrorMessage } from '../utils/error.js';
|
import { executeAndCompleteTask } from './taskExecution.js';
|
||||||
import { executeTask, resolveTaskExecution } from './taskExecution.js';
|
|
||||||
import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
|
import { DEFAULT_WORKFLOW_NAME } from '../constants.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,44 +51,12 @@ export async function watchTasks(cwd: string): Promise<void> {
|
|||||||
info(`=== Task ${taskCount}: ${task.name} ===`);
|
info(`=== Task ${taskCount}: ${task.name} ===`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const startedAt = new Date().toISOString();
|
const taskSuccess = await executeAndCompleteTask(task, taskRunner, cwd, workflowName);
|
||||||
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) {
|
if (taskSuccess) {
|
||||||
successCount++;
|
successCount++;
|
||||||
success(`Task "${task.name}" completed`);
|
|
||||||
} else {
|
} else {
|
||||||
failCount++;
|
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();
|
console.log();
|
||||||
|
|||||||
@ -69,6 +69,7 @@ function normalizeWorkflowConfig(raw: unknown, workflowDir: string): WorkflowCon
|
|||||||
allowedTools: step.allowed_tools,
|
allowedTools: step.allowed_tools,
|
||||||
provider: step.provider,
|
provider: step.provider,
|
||||||
model: step.model,
|
model: step.model,
|
||||||
|
permissionMode: step.permission_mode,
|
||||||
instructionTemplate: step.instruction_template || step.instruction || '{task}',
|
instructionTemplate: step.instruction_template || step.instruction || '{task}',
|
||||||
statusRulesPrompt: step.status_rules_prompt,
|
statusRulesPrompt: step.status_rules_prompt,
|
||||||
transitions: step.transitions.map((t) => ({
|
transitions: step.transitions.map((t) => ({
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export const StatusSchema = z.enum([
|
|||||||
'improve',
|
'improve',
|
||||||
'cancelled',
|
'cancelled',
|
||||||
'interrupted',
|
'interrupted',
|
||||||
|
'answer',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,6 +34,7 @@ export const StatusSchema = z.enum([
|
|||||||
* - approved: Review passed
|
* - approved: Review passed
|
||||||
* - rejected: Review failed, needs major rework
|
* - rejected: Review failed, needs major rework
|
||||||
* - improve: Needs improvement (security concerns, quality issues)
|
* - improve: Needs improvement (security concerns, quality issues)
|
||||||
|
* - answer: Question answered (complete workflow as success)
|
||||||
* - always: Unconditional transition
|
* - always: Unconditional transition
|
||||||
*/
|
*/
|
||||||
export const TransitionConditionSchema = z.enum([
|
export const TransitionConditionSchema = z.enum([
|
||||||
@ -41,6 +43,7 @@ export const TransitionConditionSchema = z.enum([
|
|||||||
'approved',
|
'approved',
|
||||||
'rejected',
|
'rejected',
|
||||||
'improve',
|
'improve',
|
||||||
|
'answer',
|
||||||
'always',
|
'always',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -53,6 +56,9 @@ export const WorkflowTransitionSchema = z.object({
|
|||||||
nextStep: z.string().min(1),
|
nextStep: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Permission mode schema for tool execution */
|
||||||
|
export const PermissionModeSchema = z.enum(['default', 'acceptEdits', 'bypassPermissions']);
|
||||||
|
|
||||||
/** Workflow step schema - raw YAML format */
|
/** Workflow step schema - raw YAML format */
|
||||||
export const WorkflowStepRawSchema = z.object({
|
export const WorkflowStepRawSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@ -62,6 +68,8 @@ export const WorkflowStepRawSchema = z.object({
|
|||||||
allowed_tools: z.array(z.string()).optional(),
|
allowed_tools: z.array(z.string()).optional(),
|
||||||
provider: z.enum(['claude', 'codex', 'mock']).optional(),
|
provider: z.enum(['claude', 'codex', 'mock']).optional(),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
|
/** Permission mode for tool execution in this step */
|
||||||
|
permission_mode: PermissionModeSchema.optional(),
|
||||||
instruction: z.string().optional(),
|
instruction: z.string().optional(),
|
||||||
instruction_template: z.string().optional(),
|
instruction_template: z.string().optional(),
|
||||||
status_rules_prompt: z.string().optional(),
|
status_rules_prompt: z.string().optional(),
|
||||||
@ -141,4 +149,5 @@ export const GENERIC_STATUS_PATTERNS: Record<string, string> = {
|
|||||||
improve: '\\[[\\w-]+:IMPROVE\\]',
|
improve: '\\[[\\w-]+:IMPROVE\\]',
|
||||||
done: '\\[[\\w-]+:(DONE|FIXED)\\]',
|
done: '\\[[\\w-]+:(DONE|FIXED)\\]',
|
||||||
blocked: '\\[[\\w-]+:BLOCKED\\]',
|
blocked: '\\[[\\w-]+:BLOCKED\\]',
|
||||||
|
answer: '\\[[\\w-]+:ANSWER\\]',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,8 @@ export type Status =
|
|||||||
| 'rejected'
|
| 'rejected'
|
||||||
| 'improve'
|
| 'improve'
|
||||||
| 'cancelled'
|
| 'cancelled'
|
||||||
| 'interrupted';
|
| 'interrupted'
|
||||||
|
| 'answer';
|
||||||
|
|
||||||
/** Condition types for workflow transitions */
|
/** Condition types for workflow transitions */
|
||||||
export type TransitionCondition =
|
export type TransitionCondition =
|
||||||
@ -24,6 +25,7 @@ export type TransitionCondition =
|
|||||||
| 'approved'
|
| 'approved'
|
||||||
| 'rejected'
|
| 'rejected'
|
||||||
| 'improve'
|
| 'improve'
|
||||||
|
| 'answer'
|
||||||
| 'always';
|
| 'always';
|
||||||
|
|
||||||
/** Response from an agent execution */
|
/** Response from an agent execution */
|
||||||
@ -53,6 +55,9 @@ export interface WorkflowTransition {
|
|||||||
/** Behavior when no status marker is found in agent output */
|
/** Behavior when no status marker is found in agent output */
|
||||||
export type OnNoStatusBehavior = 'complete' | 'continue' | 'stay';
|
export type OnNoStatusBehavior = 'complete' | 'continue' | 'stay';
|
||||||
|
|
||||||
|
/** Permission mode for tool execution */
|
||||||
|
export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
|
||||||
|
|
||||||
/** Single step in a workflow */
|
/** Single step in a workflow */
|
||||||
export interface WorkflowStep {
|
export interface WorkflowStep {
|
||||||
name: string;
|
name: string;
|
||||||
@ -68,6 +73,8 @@ export interface WorkflowStep {
|
|||||||
provider?: 'claude' | 'codex' | 'mock';
|
provider?: 'claude' | 'codex' | 'mock';
|
||||||
/** Model override for this step */
|
/** Model override for this step */
|
||||||
model?: string;
|
model?: string;
|
||||||
|
/** Permission mode for tool execution in this step */
|
||||||
|
permissionMode?: PermissionMode;
|
||||||
instructionTemplate: string;
|
instructionTemplate: string;
|
||||||
/** Status output rules to be injected into system prompt */
|
/** Status output rules to be injected into system prompt */
|
||||||
statusRulesPrompt?: string;
|
statusRulesPrompt?: string;
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export class ClaudeProvider implements Provider {
|
|||||||
model: options.model,
|
model: options.model,
|
||||||
systemPrompt: options.systemPrompt,
|
systemPrompt: options.systemPrompt,
|
||||||
statusPatterns: options.statusPatterns,
|
statusPatterns: options.statusPatterns,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
@ -32,6 +33,7 @@ export class ClaudeProvider implements Provider {
|
|||||||
allowedTools: options.allowedTools,
|
allowedTools: options.allowedTools,
|
||||||
model: options.model,
|
model: options.model,
|
||||||
statusPatterns: options.statusPatterns,
|
statusPatterns: options.statusPatterns,
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/process.js';
|
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/process.js';
|
||||||
import type { AgentResponse } from '../models/types.js';
|
import type { AgentResponse, PermissionMode } from '../models/types.js';
|
||||||
import { ClaudeProvider } from './claude.js';
|
import { ClaudeProvider } from './claude.js';
|
||||||
import { CodexProvider } from './codex.js';
|
import { CodexProvider } from './codex.js';
|
||||||
import { MockProvider } from './mock.js';
|
import { MockProvider } from './mock.js';
|
||||||
@ -19,6 +19,8 @@ export interface ProviderCallOptions {
|
|||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
allowedTools?: string[];
|
allowedTools?: string[];
|
||||||
statusPatterns?: Record<string, string>;
|
statusPatterns?: Record<string, string>;
|
||||||
|
/** Permission mode for tool execution (from workflow step) */
|
||||||
|
permissionMode?: PermissionMode;
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
|
|||||||
86
src/task/autoCommit.ts
Normal file
86
src/task/autoCommit.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Auto-commit for worktree tasks
|
||||||
|
*
|
||||||
|
* After a successful workflow completion in a worktree,
|
||||||
|
* automatically stages all changes and creates a commit.
|
||||||
|
* No co-author trailer is added.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
|
||||||
|
const log = createLogger('autoCommit');
|
||||||
|
|
||||||
|
export interface AutoCommitResult {
|
||||||
|
/** Whether the commit was created successfully */
|
||||||
|
success: boolean;
|
||||||
|
/** The short commit hash (if committed) */
|
||||||
|
commitHash?: string;
|
||||||
|
/** Human-readable message */
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-commit all changes in a worktree directory.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Stage all changes (git add -A)
|
||||||
|
* 2. Check if there are staged changes (git status --porcelain)
|
||||||
|
* 3. If changes exist, create a commit with "takt: {taskName}"
|
||||||
|
*
|
||||||
|
* @param worktreeCwd - The worktree directory
|
||||||
|
* @param taskName - Task name used in commit message
|
||||||
|
*/
|
||||||
|
export function autoCommitWorktree(worktreeCwd: string, taskName: string): AutoCommitResult {
|
||||||
|
log.info('Auto-commit starting', { cwd: worktreeCwd, taskName });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Stage all changes
|
||||||
|
execFileSync('git', ['add', '-A'], {
|
||||||
|
cwd: worktreeCwd,
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there are staged changes
|
||||||
|
const statusOutput = execFileSync('git', ['status', '--porcelain'], {
|
||||||
|
cwd: worktreeCwd,
|
||||||
|
stdio: 'pipe',
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!statusOutput.trim()) {
|
||||||
|
log.info('No changes to commit');
|
||||||
|
return { success: true, message: 'No changes to commit' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create commit (no co-author)
|
||||||
|
const commitMessage = `takt: ${taskName}`;
|
||||||
|
execFileSync('git', ['commit', '-m', commitMessage], {
|
||||||
|
cwd: worktreeCwd,
|
||||||
|
stdio: 'pipe',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the short commit hash
|
||||||
|
const commitHash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
||||||
|
cwd: worktreeCwd,
|
||||||
|
stdio: 'pipe',
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
log.info('Auto-commit created', { commitHash, message: commitMessage });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
commitHash,
|
||||||
|
message: `Committed: ${commitHash} - ${commitMessage}`,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
log.error('Auto-commit failed', { error: errorMessage });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Auto-commit failed: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,4 +13,5 @@ export { showTaskList } from './display.js';
|
|||||||
export { TaskFileSchema, type TaskFileData } from './schema.js';
|
export { TaskFileSchema, type TaskFileData } from './schema.js';
|
||||||
export { parseTaskFile, parseTaskFiles, type ParsedTask } from './parser.js';
|
export { parseTaskFile, parseTaskFiles, type ParsedTask } from './parser.js';
|
||||||
export { createWorktree, removeWorktree, type WorktreeOptions, type WorktreeResult } from './worktree.js';
|
export { createWorktree, removeWorktree, type WorktreeOptions, type WorktreeResult } from './worktree.js';
|
||||||
|
export { autoCommitWorktree, type AutoCommitResult } from './autoCommit.js';
|
||||||
export { TaskWatcher, type TaskWatcherOptions } from './watcher.js';
|
export { TaskWatcher, type TaskWatcherOptions } from './watcher.js';
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Debug logging utilities for takt
|
* Debug logging utilities for takt
|
||||||
* Writes debug logs to file when enabled in config
|
* Writes debug logs to file when enabled in config.
|
||||||
|
* When verbose console is enabled, also outputs to stderr.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
import { existsSync, appendFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||||
@ -13,6 +14,9 @@ let debugEnabled = false;
|
|||||||
let debugLogFile: string | null = null;
|
let debugLogFile: string | null = null;
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
|
/** Verbose console output state */
|
||||||
|
let verboseConsoleEnabled = false;
|
||||||
|
|
||||||
/** Get default debug log file path */
|
/** Get default debug log file path */
|
||||||
function getDefaultLogFile(): string {
|
function getDefaultLogFile(): string {
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
@ -61,6 +65,17 @@ export function resetDebugLogger(): void {
|
|||||||
debugEnabled = false;
|
debugEnabled = false;
|
||||||
debugLogFile = null;
|
debugLogFile = null;
|
||||||
initialized = false;
|
initialized = false;
|
||||||
|
verboseConsoleEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enable or disable verbose console output */
|
||||||
|
export function setVerboseConsole(enabled: boolean): void {
|
||||||
|
verboseConsoleEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if verbose console is enabled */
|
||||||
|
export function isVerboseConsole(): boolean {
|
||||||
|
return verboseConsoleEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if debug is enabled */
|
/** Check if debug is enabled */
|
||||||
@ -92,13 +107,23 @@ function formatLogMessage(level: string, component: string, message: string, dat
|
|||||||
return logLine;
|
return logLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write a debug log entry */
|
/** Format a compact console log line */
|
||||||
export function debugLog(component: string, message: string, data?: unknown): void {
|
function formatConsoleMessage(level: string, component: string, message: string): string {
|
||||||
|
const timestamp = new Date().toISOString().slice(11, 23); // HH:mm:ss.SSS
|
||||||
|
return `[${timestamp}] [${level}] [${component}] ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write a log entry to verbose console (stderr) and/or file */
|
||||||
|
function writeLog(level: string, component: string, message: string, data?: unknown): void {
|
||||||
|
if (verboseConsoleEnabled) {
|
||||||
|
process.stderr.write(formatConsoleMessage(level, component, message) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
if (!debugEnabled || !debugLogFile) {
|
if (!debugEnabled || !debugLogFile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logLine = formatLogMessage('DEBUG', component, message, data);
|
const logLine = formatLogMessage(level, component, message, data);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appendFileSync(debugLogFile, logLine + '\n', 'utf-8');
|
appendFileSync(debugLogFile, logLine + '\n', 'utf-8');
|
||||||
@ -107,34 +132,19 @@ export function debugLog(component: string, message: string, data?: unknown): vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Write a debug log entry */
|
||||||
|
export function debugLog(component: string, message: string, data?: unknown): void {
|
||||||
|
writeLog('DEBUG', component, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
/** Write an info log entry */
|
/** Write an info log entry */
|
||||||
export function infoLog(component: string, message: string, data?: unknown): void {
|
export function infoLog(component: string, message: string, data?: unknown): void {
|
||||||
if (!debugEnabled || !debugLogFile) {
|
writeLog('INFO', component, message, data);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logLine = formatLogMessage('INFO', component, message, data);
|
|
||||||
|
|
||||||
try {
|
|
||||||
appendFileSync(debugLogFile, logLine + '\n', 'utf-8');
|
|
||||||
} catch {
|
|
||||||
// Silently fail
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write an error log entry */
|
/** Write an error log entry */
|
||||||
export function errorLog(component: string, message: string, data?: unknown): void {
|
export function errorLog(component: string, message: string, data?: unknown): void {
|
||||||
if (!debugEnabled || !debugLogFile) {
|
writeLog('ERROR', component, message, data);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logLine = formatLogMessage('ERROR', component, message, data);
|
|
||||||
|
|
||||||
try {
|
|
||||||
appendFileSync(debugLogFile, logLine + '\n', 'utf-8');
|
|
||||||
} catch {
|
|
||||||
// Silently fail
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Log function entry with arguments */
|
/** Log function entry with arguments */
|
||||||
|
|||||||
@ -25,6 +25,9 @@ import {
|
|||||||
incrementStepIteration,
|
incrementStepIteration,
|
||||||
} from './state-manager.js';
|
} from './state-manager.js';
|
||||||
import { generateReportDir } from '../utils/session.js';
|
import { generateReportDir } from '../utils/session.js';
|
||||||
|
import { createLogger } from '../utils/debug.js';
|
||||||
|
|
||||||
|
const log = createLogger('engine');
|
||||||
|
|
||||||
// Re-export types for backward compatibility
|
// Re-export types for backward compatibility
|
||||||
export type {
|
export type {
|
||||||
@ -60,6 +63,12 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
this.ensureReportDirExists();
|
this.ensureReportDirExists();
|
||||||
this.validateConfig();
|
this.validateConfig();
|
||||||
this.state = createInitialState(config, options);
|
this.state = createInitialState(config, options);
|
||||||
|
log.debug('WorkflowEngine initialized', {
|
||||||
|
workflow: config.name,
|
||||||
|
steps: config.steps.map(s => s.name),
|
||||||
|
initialStep: config.initialStep,
|
||||||
|
maxIterations: config.maxIterations,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure report directory exists (always in original cwd) */
|
/** Ensure report directory exists (always in original cwd) */
|
||||||
@ -146,6 +155,13 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
const stepIteration = incrementStepIteration(this.state, step.name);
|
const stepIteration = incrementStepIteration(this.state, step.name);
|
||||||
const instruction = this.buildInstruction(step, stepIteration);
|
const instruction = this.buildInstruction(step, stepIteration);
|
||||||
const sessionId = this.state.agentSessions.get(step.agent);
|
const sessionId = this.state.agentSessions.get(step.agent);
|
||||||
|
log.debug('Running step', {
|
||||||
|
step: step.name,
|
||||||
|
agent: step.agent,
|
||||||
|
stepIteration,
|
||||||
|
iteration: this.state.iteration,
|
||||||
|
sessionId: sessionId ?? 'new',
|
||||||
|
});
|
||||||
|
|
||||||
const agentOptions: RunAgentOptions = {
|
const agentOptions: RunAgentOptions = {
|
||||||
cwd: this.cwd,
|
cwd: this.cwd,
|
||||||
@ -155,6 +171,7 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
statusRulesPrompt: step.statusRulesPrompt,
|
statusRulesPrompt: step.statusRulesPrompt,
|
||||||
provider: step.provider,
|
provider: step.provider,
|
||||||
model: step.model,
|
model: step.model,
|
||||||
|
permissionMode: step.permissionMode,
|
||||||
onStream: this.options.onStream,
|
onStream: this.options.onStream,
|
||||||
onPermissionRequest: this.options.onPermissionRequest,
|
onPermissionRequest: this.options.onPermissionRequest,
|
||||||
onAskUserQuestion: this.options.onAskUserQuestion,
|
onAskUserQuestion: this.options.onAskUserQuestion,
|
||||||
@ -239,6 +256,11 @@ export class WorkflowEngine extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = determineNextStep(step, response.status, this.config);
|
const nextStep = determineNextStep(step, response.status, this.config);
|
||||||
|
log.debug('Step transition', {
|
||||||
|
from: step.name,
|
||||||
|
status: response.status,
|
||||||
|
nextStep,
|
||||||
|
});
|
||||||
|
|
||||||
if (nextStep === COMPLETE_STEP) {
|
if (nextStep === COMPLETE_STEP) {
|
||||||
this.state.status = 'completed';
|
this.state.status = 'completed';
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export function matchesCondition(
|
|||||||
approved: ['approved'],
|
approved: ['approved'],
|
||||||
rejected: ['rejected'],
|
rejected: ['rejected'],
|
||||||
improve: ['improve'],
|
improve: ['improve'],
|
||||||
|
answer: ['answer'],
|
||||||
pending: [],
|
pending: [],
|
||||||
in_progress: [],
|
in_progress: [],
|
||||||
cancelled: [],
|
cancelled: [],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user