resolved 52
This commit is contained in:
parent
2b35021d45
commit
e950a3f79c
118
README.md
118
README.md
@ -24,26 +24,18 @@ npm install -g takt
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run a task (will prompt for workflow selection and optional isolated clone)
|
||||
# Run a task (prompts for workflow selection, worktree, and PR creation)
|
||||
takt "Add a login feature"
|
||||
|
||||
# Run a GitHub issue as a task
|
||||
takt "#6"
|
||||
# Run a GitHub issue as a task (both are equivalent)
|
||||
takt '#6'
|
||||
takt --issue 6
|
||||
|
||||
# Add a task via AI conversation
|
||||
takt add
|
||||
# Interactive mode — refine task requirements with AI, then execute
|
||||
takt
|
||||
|
||||
# Run all pending tasks
|
||||
takt run
|
||||
|
||||
# Watch for tasks and auto-execute
|
||||
takt watch
|
||||
|
||||
# List task branches (merge or delete)
|
||||
takt list
|
||||
|
||||
# Switch workflow
|
||||
takt switch
|
||||
# Pipeline mode (non-interactive, for scripts and CI)
|
||||
takt --task "fix the auth bug" --auto-pr
|
||||
```
|
||||
|
||||
### What happens when you run a task
|
||||
@ -75,6 +67,14 @@ Choose `y` to run in a `git clone --shared` isolated environment, keeping your w
|
||||
|
||||
**3. Execution** — The selected workflow orchestrates multiple agents to complete the task.
|
||||
|
||||
**4. PR creation** (after worktree execution)
|
||||
|
||||
```
|
||||
? Create pull request? (y/N)
|
||||
```
|
||||
|
||||
If `--auto-pr` is specified, the PR is created automatically without asking.
|
||||
|
||||
### Recommended workflows
|
||||
|
||||
| Workflow | Best for |
|
||||
@ -87,11 +87,54 @@ Choose `y` to run in a `git clone --shared` isolated environment, keeping your w
|
||||
|
||||
## Commands
|
||||
|
||||
### Interactive Mode (default)
|
||||
|
||||
The standard mode for everyday development. Workflow selection, worktree creation, and PR creation are handled interactively.
|
||||
|
||||
```bash
|
||||
# Run a task
|
||||
takt "Add a login feature"
|
||||
|
||||
# Run a GitHub issue as a task (both are equivalent)
|
||||
takt '#6'
|
||||
takt --issue 6
|
||||
|
||||
# Interactive mode — refine task requirements with AI before executing
|
||||
takt
|
||||
|
||||
# Run a task and automatically create a PR (skip the confirmation prompt)
|
||||
takt '#6' --auto-pr
|
||||
```
|
||||
|
||||
When `--auto-pr` is not specified, you will be asked whether to create a PR after a successful worktree execution.
|
||||
|
||||
### Pipeline Mode (`--task`)
|
||||
|
||||
Specifying `--task` enters pipeline mode — fully non-interactive, suitable for scripts and CI integration. TAKT automatically creates a branch, runs the workflow, commits, and pushes.
|
||||
|
||||
```bash
|
||||
# Run a task in pipeline mode
|
||||
takt --task "fix the auth bug"
|
||||
|
||||
# Pipeline mode + automatic PR creation
|
||||
takt --task "fix the auth bug" --auto-pr
|
||||
|
||||
# Attach GitHub issue context
|
||||
takt --task "fix the auth bug" --issue 99 --auto-pr
|
||||
|
||||
# Specify workflow and branch
|
||||
takt --task "fix the auth bug" -w magi -b feat/fix-auth
|
||||
|
||||
# Specify repository (for PR creation)
|
||||
takt --task "fix the auth bug" --auto-pr --repo owner/repo
|
||||
```
|
||||
|
||||
In pipeline mode, PRs are **not** created unless `--auto-pr` is explicitly specified.
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `takt "task"` | Execute task with current workflow (session auto-continued) |
|
||||
| `takt "#N"` | Execute GitHub issue #N as a task |
|
||||
| `takt` | Interactive task input mode |
|
||||
| `takt run` | Run all pending tasks from `.takt/tasks/` |
|
||||
| `takt watch` | Watch `.takt/tasks/` and auto-execute tasks (stays resident) |
|
||||
| `takt add` | Add a new task via AI conversation |
|
||||
@ -102,6 +145,17 @@ Choose `y` to run in a `git clone --shared` isolated environment, keeping your w
|
||||
| `takt config` | Configure permission mode |
|
||||
| `takt --help` | Show help |
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-t, --task <text>` | Task content — **triggers pipeline (non-interactive) mode** |
|
||||
| `-i, --issue <N>` | GitHub issue number (equivalent to `#N` in interactive mode) |
|
||||
| `-w, --workflow <name>` | Workflow to use |
|
||||
| `-b, --branch <name>` | Branch name (auto-generated if omitted) |
|
||||
| `--auto-pr` | Create PR after execution (interactive: skip confirmation, pipeline: enable PR) |
|
||||
| `--repo <owner/repo>` | Repository for PR creation |
|
||||
|
||||
## Workflows
|
||||
|
||||
TAKT uses YAML-based workflow definitions with rule-based routing. Builtin workflows are embedded in the package; user workflows in `~/.takt/workflows/` take priority. Use `takt eject` to copy a builtin to `~/.takt/` for customization.
|
||||
@ -286,8 +340,27 @@ provider: claude # Default provider: claude or codex
|
||||
model: sonnet # Default model (optional)
|
||||
trusted_directories:
|
||||
- /path/to/trusted/dir
|
||||
|
||||
# Pipeline execution settings (optional)
|
||||
# Customize branch naming, commit messages, and PR body for pipeline mode.
|
||||
# pipeline:
|
||||
# default_branch_prefix: "takt/"
|
||||
# commit_message_template: "feat: {title} (#{issue})"
|
||||
# pr_body_template: |
|
||||
# ## Summary
|
||||
# {issue_body}
|
||||
# Closes #{issue}
|
||||
```
|
||||
|
||||
**Pipeline template variables:**
|
||||
|
||||
| Variable | Available in | Description |
|
||||
|----------|-------------|-------------|
|
||||
| `{title}` | commit message | Issue title |
|
||||
| `{issue}` | commit message, PR body | Issue number |
|
||||
| `{issue_body}` | PR body | Issue body text |
|
||||
| `{report}` | PR body | Workflow execution report |
|
||||
|
||||
**Model Resolution Priority:**
|
||||
1. Workflow step `model` (highest priority)
|
||||
2. Custom agent `model`
|
||||
@ -299,12 +372,13 @@ trusted_directories:
|
||||
|
||||
### Interactive Workflow
|
||||
|
||||
When running `takt "task"`, you are prompted to:
|
||||
When running `takt "Add a feature"`, you are prompted to:
|
||||
|
||||
1. **Select a workflow** - Choose from available workflows (arrow keys, ESC to cancel)
|
||||
2. **Create an isolated clone** (optional) - Optionally run the task in a `git clone --shared` for isolation
|
||||
2. **Create an isolated clone** (optional) - Run the task in a `git clone --shared` for isolation
|
||||
3. **Create a pull request** (after worktree execution) - Create a PR from the task branch
|
||||
|
||||
This interactive flow ensures each task runs with the right workflow and isolation level.
|
||||
If `--auto-pr` is specified, the PR confirmation is skipped and the PR is created automatically.
|
||||
|
||||
### Adding Custom Workflows
|
||||
|
||||
|
||||
@ -20,31 +20,23 @@ npm install -g takt
|
||||
## クイックスタート
|
||||
|
||||
```bash
|
||||
# タスクを実行(ワークフロー選択プロンプトが表示されます)
|
||||
takt "ログイン機能を追加して"
|
||||
# タスクを実行(ワークフロー選択・worktree・PR作成を対話的に案内)
|
||||
takt ログイン機能を追加して
|
||||
|
||||
# GitHub Issueをタスクとして実行
|
||||
takt "#6"
|
||||
# GitHub Issueをタスクとして実行(どちらも同じ)
|
||||
takt '#6'
|
||||
takt --issue 6
|
||||
|
||||
# AI会話でタスクを追加
|
||||
takt add
|
||||
# 対話モードでAIとタスク要件を詰めてから実行
|
||||
takt
|
||||
|
||||
# 保留中のタスクをすべて実行
|
||||
takt run
|
||||
|
||||
# タスクを監視して自動実行
|
||||
takt watch
|
||||
|
||||
# タスクブランチ一覧(マージ・削除)
|
||||
takt list
|
||||
|
||||
# ワークフローを切り替え
|
||||
takt switch
|
||||
# パイプライン実行(非対話・スクリプト/CI向け)
|
||||
takt --task "バグを修正して" --auto-pr
|
||||
```
|
||||
|
||||
### タスク実行の流れ
|
||||
|
||||
`takt "ログイン機能を追加して"` を実行すると、以下の対話フローが表示されます:
|
||||
`takt ログイン機能を追加して` を実行すると、以下の対話フローが表示されます:
|
||||
|
||||
**1. ワークフロー選択**
|
||||
|
||||
@ -71,6 +63,14 @@ Select workflow:
|
||||
|
||||
**3. 実行** — 選択したワークフローが複数のエージェントを連携させてタスクを完了します。
|
||||
|
||||
**4. PR作成**(worktree実行後)
|
||||
|
||||
```
|
||||
? Create pull request? (y/N)
|
||||
```
|
||||
|
||||
`--auto-pr` を指定している場合は確認なしで自動作成されます。
|
||||
|
||||
### おすすめワークフロー
|
||||
|
||||
| ワークフロー | おすすめ用途 |
|
||||
@ -83,11 +83,54 @@ Select workflow:
|
||||
|
||||
## コマンド一覧
|
||||
|
||||
### 対話モード(デフォルト)
|
||||
|
||||
日常の開発で使う基本モード。ワークフロー選択、worktree作成、PR作成を対話的に確認します。
|
||||
|
||||
```bash
|
||||
# タスクを実行
|
||||
takt ログイン機能を追加して
|
||||
|
||||
# GitHub Issueをタスクとして実行(どちらも同じ)
|
||||
takt '#6'
|
||||
takt --issue 6
|
||||
|
||||
# 対話モードでAIとタスク要件を詰めてから実行
|
||||
takt
|
||||
|
||||
# タスクを実行してPRを自動作成(確認プロンプトをスキップ)
|
||||
takt '#6' --auto-pr
|
||||
```
|
||||
|
||||
`--auto-pr` を指定しない場合、worktreeでの実行成功後に「PR作成する?」と確認されます。
|
||||
|
||||
### パイプラインモード(`--task`)
|
||||
|
||||
`--task` を指定すると非対話のパイプラインモードに入ります。ブランチ作成 → ワークフロー実行 → commit & push を自動で行います。スクリプトやCI連携に適しています。
|
||||
|
||||
```bash
|
||||
# タスクをパイプライン実行
|
||||
takt --task "バグを修正"
|
||||
|
||||
# パイプライン実行 + PR自動作成
|
||||
takt --task "バグを修正" --auto-pr
|
||||
|
||||
# Issue情報を紐付け
|
||||
takt --task "バグを修正" --issue 99 --auto-pr
|
||||
|
||||
# ワークフロー・ブランチ指定
|
||||
takt --task "バグを修正" -w magi -b feat/fix-bug
|
||||
|
||||
# リポジトリ指定(PR作成時)
|
||||
takt --task "バグを修正" --auto-pr --repo owner/repo
|
||||
```
|
||||
|
||||
パイプラインモードでは `--auto-pr` を指定しない限りPRは作成されません。
|
||||
|
||||
### サブコマンド
|
||||
|
||||
| コマンド | 説明 |
|
||||
|---------|------|
|
||||
| `takt "タスク"` | 現在のワークフローでタスクを実行(セッション自動継続) |
|
||||
| `takt "#N"` | GitHub Issue #Nをタスクとして実行 |
|
||||
| `takt` | 対話式タスク入力モード |
|
||||
| `takt run` | `.takt/tasks/` の保留中タスクをすべて実行 |
|
||||
| `takt watch` | `.takt/tasks/` を監視してタスクを自動実行(常駐プロセス) |
|
||||
| `takt add` | AI会話で新しいタスクを追加 |
|
||||
@ -98,6 +141,17 @@ Select workflow:
|
||||
| `takt config` | パーミッションモードを設定 |
|
||||
| `takt --help` | ヘルプを表示 |
|
||||
|
||||
### オプション
|
||||
|
||||
| オプション | 説明 |
|
||||
|-----------|------|
|
||||
| `-t, --task <text>` | タスク内容 — **パイプライン(非対話)モードのトリガー** |
|
||||
| `-i, --issue <N>` | GitHub Issue番号(対話モードでは `#N` と同じ) |
|
||||
| `-w, --workflow <name>` | ワークフロー指定 |
|
||||
| `-b, --branch <name>` | ブランチ名指定(省略時は自動生成) |
|
||||
| `--auto-pr` | PR作成(対話: 確認スキップ、パイプライン: PR有効化) |
|
||||
| `--repo <owner/repo>` | リポジトリ指定(PR作成時) |
|
||||
|
||||
## ワークフロー
|
||||
|
||||
TAKTはYAMLベースのワークフロー定義とルールベースルーティングを使用します。ビルトインワークフローはパッケージに埋め込まれており、`~/.takt/workflows/` のユーザーワークフローが優先されます。`takt eject` でビルトインを`~/.takt/`にコピーしてカスタマイズできます。
|
||||
@ -282,8 +336,27 @@ provider: claude # デフォルトプロバイダー: claude または c
|
||||
model: sonnet # デフォルトモデル(オプション)
|
||||
trusted_directories:
|
||||
- /path/to/trusted/dir
|
||||
|
||||
# パイプライン実行設定(オプション)
|
||||
# ブランチ名、コミットメッセージ、PRの本文をカスタマイズできます。
|
||||
# pipeline:
|
||||
# default_branch_prefix: "takt/"
|
||||
# commit_message_template: "feat: {title} (#{issue})"
|
||||
# pr_body_template: |
|
||||
# ## Summary
|
||||
# {issue_body}
|
||||
# Closes #{issue}
|
||||
```
|
||||
|
||||
**パイプラインテンプレート変数:**
|
||||
|
||||
| 変数 | 使用可能箇所 | 説明 |
|
||||
|------|-------------|------|
|
||||
| `{title}` | コミットメッセージ | Issueタイトル |
|
||||
| `{issue}` | コミットメッセージ、PR本文 | Issue番号 |
|
||||
| `{issue_body}` | PR本文 | Issue本文 |
|
||||
| `{report}` | PR本文 | ワークフロー実行レポート |
|
||||
|
||||
**モデル解決の優先順位:**
|
||||
1. ワークフローステップの `model`(最優先)
|
||||
2. カスタムエージェントの `model`
|
||||
|
||||
@ -27,6 +27,16 @@ provider: claude
|
||||
# OpenAI API key (optional, overridden by TAKT_OPENAI_API_KEY env var)
|
||||
# openai_api_key: ""
|
||||
|
||||
# Pipeline execution settings (optional)
|
||||
# Customize branch naming, commit messages, and PR body for pipeline mode (--task).
|
||||
# pipeline:
|
||||
# default_branch_prefix: "takt/"
|
||||
# commit_message_template: "feat: {title} (#{issue})"
|
||||
# pr_body_template: |
|
||||
# ## Summary
|
||||
# {issue_body}
|
||||
# Closes #{issue}
|
||||
|
||||
# Debug settings (optional)
|
||||
# debug:
|
||||
# enabled: false
|
||||
|
||||
@ -27,6 +27,16 @@ provider: claude
|
||||
# OpenAI APIキー (オプション、環境変数 TAKT_OPENAI_API_KEY で上書き可能)
|
||||
# openai_api_key: ""
|
||||
|
||||
# パイプライン実行設定 (オプション)
|
||||
# パイプラインモード (--task) のブランチ名、コミットメッセージ、PRの本文をカスタマイズできます。
|
||||
# pipeline:
|
||||
# default_branch_prefix: "takt/"
|
||||
# commit_message_template: "feat: {title} (#{issue})"
|
||||
# pr_body_template: |
|
||||
# ## Summary
|
||||
# {issue_body}
|
||||
# Closes #{issue}
|
||||
|
||||
# デバッグ設定 (オプション)
|
||||
# debug:
|
||||
# enabled: false
|
||||
|
||||
37
src/__tests__/exitCodes.test.ts
Normal file
37
src/__tests__/exitCodes.test.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Tests for exit codes
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
EXIT_SUCCESS,
|
||||
EXIT_GENERAL_ERROR,
|
||||
EXIT_ISSUE_FETCH_FAILED,
|
||||
EXIT_WORKFLOW_FAILED,
|
||||
EXIT_GIT_OPERATION_FAILED,
|
||||
EXIT_PR_CREATION_FAILED,
|
||||
} from '../exitCodes.js';
|
||||
|
||||
describe('exit codes', () => {
|
||||
it('should have distinct values', () => {
|
||||
const codes = [
|
||||
EXIT_SUCCESS,
|
||||
EXIT_GENERAL_ERROR,
|
||||
EXIT_ISSUE_FETCH_FAILED,
|
||||
EXIT_WORKFLOW_FAILED,
|
||||
EXIT_GIT_OPERATION_FAILED,
|
||||
EXIT_PR_CREATION_FAILED,
|
||||
];
|
||||
const unique = new Set(codes);
|
||||
expect(unique.size).toBe(codes.length);
|
||||
});
|
||||
|
||||
it('should match expected values from spec', () => {
|
||||
expect(EXIT_SUCCESS).toBe(0);
|
||||
expect(EXIT_GENERAL_ERROR).toBe(1);
|
||||
expect(EXIT_ISSUE_FETCH_FAILED).toBe(2);
|
||||
expect(EXIT_WORKFLOW_FAILED).toBe(3);
|
||||
expect(EXIT_GIT_OPERATION_FAILED).toBe(4);
|
||||
expect(EXIT_PR_CREATION_FAILED).toBe(5);
|
||||
});
|
||||
});
|
||||
54
src/__tests__/github-pr.test.ts
Normal file
54
src/__tests__/github-pr.test.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Tests for github/pr module
|
||||
*
|
||||
* Tests buildPrBody formatting.
|
||||
* createPullRequest/pushBranch call `gh`/`git` CLI, not unit-tested here.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildPrBody } from '../github/pr.js';
|
||||
import type { GitHubIssue } from '../github/issue.js';
|
||||
|
||||
describe('buildPrBody', () => {
|
||||
it('should build body with issue and report', () => {
|
||||
const issue: GitHubIssue = {
|
||||
number: 99,
|
||||
title: 'Add login feature',
|
||||
body: 'Implement username/password authentication.',
|
||||
labels: [],
|
||||
comments: [],
|
||||
};
|
||||
|
||||
const result = buildPrBody(issue, 'Workflow `default` completed.');
|
||||
|
||||
expect(result).toContain('## Summary');
|
||||
expect(result).toContain('Implement username/password authentication.');
|
||||
expect(result).toContain('## Execution Report');
|
||||
expect(result).toContain('Workflow `default` completed.');
|
||||
expect(result).toContain('Closes #99');
|
||||
});
|
||||
|
||||
it('should use title when body is empty', () => {
|
||||
const issue: GitHubIssue = {
|
||||
number: 10,
|
||||
title: 'Fix bug',
|
||||
body: '',
|
||||
labels: [],
|
||||
comments: [],
|
||||
};
|
||||
|
||||
const result = buildPrBody(issue, 'Done.');
|
||||
|
||||
expect(result).toContain('Fix bug');
|
||||
expect(result).toContain('Closes #10');
|
||||
});
|
||||
|
||||
it('should build body without issue', () => {
|
||||
const result = buildPrBody(undefined, 'Task completed.');
|
||||
|
||||
expect(result).toContain('## Summary');
|
||||
expect(result).toContain('## Execution Report');
|
||||
expect(result).toContain('Task completed.');
|
||||
expect(result).not.toContain('Closes');
|
||||
});
|
||||
});
|
||||
114
src/__tests__/globalConfig-defaults.test.ts
Normal file
114
src/__tests__/globalConfig-defaults.test.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Tests for loadGlobalConfig default values when config.yaml is missing
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock the home directory to use a temp directory
|
||||
const testHomeDir = join(tmpdir(), `takt-gc-test-${Date.now()}`);
|
||||
|
||||
vi.mock('node:os', async () => {
|
||||
const actual = await vi.importActual('node:os');
|
||||
return {
|
||||
...actual,
|
||||
homedir: () => testHomeDir,
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mocks are set up
|
||||
const { loadGlobalConfig, saveGlobalConfig } = await import('../config/globalConfig.js');
|
||||
const { getGlobalConfigPath } = await import('../config/paths.js');
|
||||
|
||||
describe('loadGlobalConfig', () => {
|
||||
beforeEach(() => {
|
||||
mkdirSync(testHomeDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(testHomeDir)) {
|
||||
rmSync(testHomeDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should return default values when config.yaml does not exist', () => {
|
||||
const config = loadGlobalConfig();
|
||||
|
||||
expect(config.language).toBe('en');
|
||||
expect(config.trustedDirectories).toEqual([]);
|
||||
expect(config.defaultWorkflow).toBe('default');
|
||||
expect(config.logLevel).toBe('info');
|
||||
expect(config.provider).toBe('claude');
|
||||
expect(config.model).toBeUndefined();
|
||||
expect(config.debug).toBeUndefined();
|
||||
expect(config.pipeline).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return a fresh copy each time (no shared reference)', () => {
|
||||
const config1 = loadGlobalConfig();
|
||||
const config2 = loadGlobalConfig();
|
||||
|
||||
config1.trustedDirectories.push('/tmp/test');
|
||||
expect(config2.trustedDirectories).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load from config.yaml when it exists', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(
|
||||
getGlobalConfigPath(),
|
||||
'language: ja\nprovider: codex\nlog_level: debug\n',
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
|
||||
expect(config.language).toBe('ja');
|
||||
expect(config.provider).toBe('codex');
|
||||
expect(config.logLevel).toBe('debug');
|
||||
});
|
||||
|
||||
it('should load pipeline config from config.yaml', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(
|
||||
getGlobalConfigPath(),
|
||||
[
|
||||
'language: en',
|
||||
'pipeline:',
|
||||
' default_branch_prefix: "feat/"',
|
||||
' commit_message_template: "fix: {title} (#{issue})"',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
|
||||
expect(config.pipeline).toBeDefined();
|
||||
expect(config.pipeline!.defaultBranchPrefix).toBe('feat/');
|
||||
expect(config.pipeline!.commitMessageTemplate).toBe('fix: {title} (#{issue})');
|
||||
expect(config.pipeline!.prBodyTemplate).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should save and reload pipeline config', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
// Create minimal config first
|
||||
writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8');
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
config.pipeline = {
|
||||
defaultBranchPrefix: 'takt/',
|
||||
commitMessageTemplate: 'feat: {title} (#{issue})',
|
||||
};
|
||||
saveGlobalConfig(config);
|
||||
|
||||
const reloaded = loadGlobalConfig();
|
||||
expect(reloaded.pipeline).toBeDefined();
|
||||
expect(reloaded.pipeline!.defaultBranchPrefix).toBe('takt/');
|
||||
expect(reloaded.pipeline!.commitMessageTemplate).toBe('feat: {title} (#{issue})');
|
||||
});
|
||||
});
|
||||
59
src/__tests__/initialization-noninteractive.test.ts
Normal file
59
src/__tests__/initialization-noninteractive.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Tests for initGlobalDirs non-interactive mode
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { mkdirSync, rmSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
// Mock the home directory to use a temp directory
|
||||
const testHomeDir = join(tmpdir(), `takt-init-ni-test-${Date.now()}`);
|
||||
|
||||
vi.mock('node:os', async () => {
|
||||
const actual = await vi.importActual('node:os');
|
||||
return {
|
||||
...actual,
|
||||
homedir: () => testHomeDir,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the prompt to track if it was called
|
||||
const mockSelectOption = vi.fn().mockResolvedValue('en');
|
||||
vi.mock('../prompt/index.js', () => ({
|
||||
selectOptionWithDefault: mockSelectOption,
|
||||
}));
|
||||
|
||||
// Import after mocks are set up
|
||||
const { initGlobalDirs, needsLanguageSetup } = await import('../config/initialization.js');
|
||||
const { getGlobalConfigPath, getGlobalConfigDir } = await import('../config/paths.js');
|
||||
|
||||
describe('initGlobalDirs with non-interactive mode', () => {
|
||||
beforeEach(() => {
|
||||
mkdirSync(testHomeDir, { recursive: true });
|
||||
mockSelectOption.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(testHomeDir)) {
|
||||
rmSync(testHomeDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should skip prompts when nonInteractive is true', async () => {
|
||||
expect(needsLanguageSetup()).toBe(true);
|
||||
|
||||
await initGlobalDirs({ nonInteractive: true });
|
||||
|
||||
// Prompts should NOT have been called
|
||||
expect(mockSelectOption).not.toHaveBeenCalled();
|
||||
// Config should still not exist (we use defaults via loadGlobalConfig fallback)
|
||||
expect(existsSync(getGlobalConfigPath())).toBe(false);
|
||||
});
|
||||
|
||||
it('should create global config directory even in non-interactive mode', async () => {
|
||||
await initGlobalDirs({ nonInteractive: true });
|
||||
|
||||
expect(existsSync(getGlobalConfigDir())).toBe(true);
|
||||
});
|
||||
});
|
||||
348
src/__tests__/pipelineExecution.test.ts
Normal file
348
src/__tests__/pipelineExecution.test.ts
Normal file
@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Tests for pipeline execution
|
||||
*
|
||||
* Tests the orchestration logic with mocked dependencies.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock all external dependencies
|
||||
const mockFetchIssue = vi.fn();
|
||||
const mockCheckGhCli = vi.fn().mockReturnValue({ available: true });
|
||||
vi.mock('../github/issue.js', () => ({
|
||||
fetchIssue: mockFetchIssue,
|
||||
formatIssueAsTask: vi.fn((issue: { title: string; body: string; number: number }) =>
|
||||
`## GitHub Issue #${issue.number}: ${issue.title}\n\n${issue.body}`
|
||||
),
|
||||
checkGhCli: mockCheckGhCli,
|
||||
}));
|
||||
|
||||
const mockCreatePullRequest = vi.fn();
|
||||
const mockPushBranch = vi.fn();
|
||||
const mockBuildPrBody = vi.fn(() => 'Default PR body');
|
||||
vi.mock('../github/pr.js', () => ({
|
||||
createPullRequest: mockCreatePullRequest,
|
||||
pushBranch: mockPushBranch,
|
||||
buildPrBody: mockBuildPrBody,
|
||||
}));
|
||||
|
||||
const mockExecuteTask = vi.fn();
|
||||
vi.mock('../commands/taskExecution.js', () => ({
|
||||
executeTask: mockExecuteTask,
|
||||
}));
|
||||
|
||||
// Mock loadGlobalConfig
|
||||
const mockLoadGlobalConfig = vi.fn();
|
||||
vi.mock('../config/globalConfig.js', () => ({
|
||||
loadGlobalConfig: mockLoadGlobalConfig,
|
||||
}));
|
||||
|
||||
// Mock execFileSync for git operations
|
||||
const mockExecFileSync = vi.fn();
|
||||
vi.mock('node:child_process', () => ({
|
||||
execFileSync: mockExecFileSync,
|
||||
}));
|
||||
|
||||
// Mock UI
|
||||
vi.mock('../utils/ui.js', () => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
status: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock debug logger
|
||||
vi.mock('../utils/debug.js', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const { executePipeline } = await import('../commands/pipelineExecution.js');
|
||||
|
||||
describe('executePipeline', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Default: git operations succeed
|
||||
mockExecFileSync.mockReturnValue('abc1234\n');
|
||||
// Default: no pipeline config
|
||||
mockLoadGlobalConfig.mockReturnValue({
|
||||
language: 'en',
|
||||
trustedDirectories: [],
|
||||
defaultWorkflow: 'default',
|
||||
logLevel: 'info',
|
||||
provider: 'claude',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return exit code 2 when neither --issue nor --task is specified', async () => {
|
||||
const exitCode = await executePipeline({
|
||||
workflow: 'default',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(2);
|
||||
});
|
||||
|
||||
it('should return exit code 2 when gh CLI is not available', async () => {
|
||||
mockCheckGhCli.mockReturnValueOnce({ available: false, error: 'gh not found' });
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
issueNumber: 99,
|
||||
workflow: 'default',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(2);
|
||||
});
|
||||
|
||||
it('should return exit code 2 when issue fetch fails', async () => {
|
||||
mockFetchIssue.mockImplementationOnce(() => {
|
||||
throw new Error('Issue not found');
|
||||
});
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
issueNumber: 999,
|
||||
workflow: 'default',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(2);
|
||||
});
|
||||
|
||||
it('should return exit code 3 when workflow fails', async () => {
|
||||
mockFetchIssue.mockReturnValueOnce({
|
||||
number: 99,
|
||||
title: 'Test issue',
|
||||
body: 'Test body',
|
||||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
mockExecuteTask.mockResolvedValueOnce(false);
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
issueNumber: 99,
|
||||
workflow: 'default',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(3);
|
||||
});
|
||||
|
||||
it('should return exit code 0 on successful task-only execution', async () => {
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
task: 'Fix the bug',
|
||||
workflow: 'default',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
||||
'Fix the bug',
|
||||
'/tmp/test',
|
||||
'default',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return exit code 5 when PR creation fails', async () => {
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
mockCreatePullRequest.mockReturnValueOnce({ success: false, error: 'PR failed' });
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
task: 'Fix the bug',
|
||||
workflow: 'default',
|
||||
autoPr: true,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(5);
|
||||
});
|
||||
|
||||
it('should create PR with correct branch when --auto-pr', async () => {
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/test/pr/1' });
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
task: 'Fix the bug',
|
||||
workflow: 'default',
|
||||
branch: 'fix/my-branch',
|
||||
autoPr: true,
|
||||
repo: 'owner/repo',
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
expect.objectContaining({
|
||||
branch: 'fix/my-branch',
|
||||
repo: 'owner/repo',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use --task when both --task and positional task are provided', async () => {
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
|
||||
const exitCode = await executePipeline({
|
||||
task: 'From --task flag',
|
||||
workflow: 'magi',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockExecuteTask).toHaveBeenCalledWith(
|
||||
'From --task flag',
|
||||
'/tmp/test',
|
||||
'magi',
|
||||
);
|
||||
});
|
||||
|
||||
describe('PipelineConfig template expansion', () => {
|
||||
it('should use commit_message_template when configured', async () => {
|
||||
mockLoadGlobalConfig.mockReturnValue({
|
||||
language: 'en',
|
||||
trustedDirectories: [],
|
||||
defaultWorkflow: 'default',
|
||||
logLevel: 'info',
|
||||
provider: 'claude',
|
||||
pipeline: {
|
||||
commitMessageTemplate: 'fix: {title} (#{issue})',
|
||||
},
|
||||
});
|
||||
|
||||
mockFetchIssue.mockReturnValueOnce({
|
||||
number: 42,
|
||||
title: 'Login broken',
|
||||
body: 'Cannot login.',
|
||||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
|
||||
await executePipeline({
|
||||
issueNumber: 42,
|
||||
workflow: 'default',
|
||||
branch: 'test-branch',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
// Verify commit was called with expanded template
|
||||
const commitCall = mockExecFileSync.mock.calls.find(
|
||||
(call: unknown[]) => call[0] === 'git' && (call[1] as string[])[0] === 'commit',
|
||||
);
|
||||
expect(commitCall).toBeDefined();
|
||||
expect((commitCall![1] as string[])[2]).toBe('fix: Login broken (#42)');
|
||||
});
|
||||
|
||||
it('should use default_branch_prefix when configured', async () => {
|
||||
mockLoadGlobalConfig.mockReturnValue({
|
||||
language: 'en',
|
||||
trustedDirectories: [],
|
||||
defaultWorkflow: 'default',
|
||||
logLevel: 'info',
|
||||
provider: 'claude',
|
||||
pipeline: {
|
||||
defaultBranchPrefix: 'feat/',
|
||||
},
|
||||
});
|
||||
|
||||
mockFetchIssue.mockReturnValueOnce({
|
||||
number: 10,
|
||||
title: 'Add feature',
|
||||
body: 'Please add.',
|
||||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
|
||||
await executePipeline({
|
||||
issueNumber: 10,
|
||||
workflow: 'default',
|
||||
autoPr: false,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
// Verify checkout -b was called with prefix
|
||||
const checkoutCall = mockExecFileSync.mock.calls.find(
|
||||
(call: unknown[]) => call[0] === 'git' && (call[1] as string[])[0] === 'checkout' && (call[1] as string[])[1] === '-b',
|
||||
);
|
||||
expect(checkoutCall).toBeDefined();
|
||||
const branchName = (checkoutCall![1] as string[])[2];
|
||||
expect(branchName).toMatch(/^feat\/issue-10-\d+$/);
|
||||
});
|
||||
|
||||
it('should use pr_body_template when configured for PR creation', async () => {
|
||||
mockLoadGlobalConfig.mockReturnValue({
|
||||
language: 'en',
|
||||
trustedDirectories: [],
|
||||
defaultWorkflow: 'default',
|
||||
logLevel: 'info',
|
||||
provider: 'claude',
|
||||
pipeline: {
|
||||
prBodyTemplate: '## Summary\n{issue_body}\n\nCloses #{issue}',
|
||||
},
|
||||
});
|
||||
|
||||
mockFetchIssue.mockReturnValueOnce({
|
||||
number: 50,
|
||||
title: 'Fix auth',
|
||||
body: 'Auth is broken.',
|
||||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/pr/1' });
|
||||
|
||||
await executePipeline({
|
||||
issueNumber: 50,
|
||||
workflow: 'default',
|
||||
branch: 'fix-auth',
|
||||
autoPr: true,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
// When prBodyTemplate is set, buildPrBody (mock) should NOT be called
|
||||
// Instead, the template is expanded directly
|
||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
expect.objectContaining({
|
||||
body: '## Summary\nAuth is broken.\n\nCloses #50',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to buildPrBody when no template is configured', async () => {
|
||||
mockExecuteTask.mockResolvedValueOnce(true);
|
||||
mockCreatePullRequest.mockReturnValueOnce({ success: true, url: 'https://github.com/pr/1' });
|
||||
|
||||
await executePipeline({
|
||||
task: 'Fix bug',
|
||||
workflow: 'default',
|
||||
branch: 'fix-branch',
|
||||
autoPr: true,
|
||||
cwd: '/tmp/test',
|
||||
});
|
||||
|
||||
// Should use buildPrBody (the mock)
|
||||
expect(mockBuildPrBody).toHaveBeenCalled();
|
||||
expect(mockCreatePullRequest).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
expect.objectContaining({
|
||||
body: 'Default PR body',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
102
src/cli.ts
102
src/cli.ts
@ -5,11 +5,16 @@
|
||||
*
|
||||
* Usage:
|
||||
* takt {task} - Execute task with current workflow (continues session)
|
||||
* takt #99 - Execute task from GitHub issue
|
||||
* takt run - Run all pending tasks from .takt/tasks/
|
||||
* takt switch - Switch workflow interactively
|
||||
* takt clear - Clear agent conversation sessions (reset to initial state)
|
||||
* takt --help - Show help
|
||||
* takt config - Select permission mode interactively
|
||||
*
|
||||
* Pipeline (non-interactive):
|
||||
* takt --task "fix bug" -w magi --auto-pr
|
||||
* takt --task "fix bug" --issue 99 --auto-pr
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
@ -34,6 +39,7 @@ import {
|
||||
watchTasks,
|
||||
listTasks,
|
||||
interactiveMode,
|
||||
executePipeline,
|
||||
} from './commands/index.js';
|
||||
import { listWorkflows } from './config/workflowLoader.js';
|
||||
import { selectOptionWithDefault, confirm } from './prompt/index.js';
|
||||
@ -43,6 +49,7 @@ import { summarizeTaskName } from './task/summarize.js';
|
||||
import { DEFAULT_WORKFLOW_NAME } from './constants.js';
|
||||
import { checkForUpdates } from './utils/updateNotifier.js';
|
||||
import { resolveIssueTask, isIssueReference } from './github/issue.js';
|
||||
import { createPullRequest, buildPrBody } from './github/pr.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version: cliVersion } = require('../package.json') as { version: string };
|
||||
@ -54,6 +61,9 @@ checkForUpdates();
|
||||
/** Resolved cwd shared across commands via preAction hook */
|
||||
let resolvedCwd = '';
|
||||
|
||||
/** Whether pipeline mode is active (--task specified, set in preAction) */
|
||||
let pipelineMode = false;
|
||||
|
||||
export interface WorktreeConfirmationResult {
|
||||
execCwd: string;
|
||||
isWorktree: boolean;
|
||||
@ -95,7 +105,11 @@ async function selectWorkflow(cwd: string): Promise<string | null> {
|
||||
* Execute a task with workflow selection, optional worktree, and auto-commit.
|
||||
* Shared by direct task execution and interactive mode.
|
||||
*/
|
||||
async function selectAndExecuteTask(cwd: string, task: string): Promise<void> {
|
||||
async function selectAndExecuteTask(
|
||||
cwd: string,
|
||||
task: string,
|
||||
options?: { autoPr?: boolean; repo?: string },
|
||||
): Promise<void> {
|
||||
const selectedWorkflow = await selectWorkflow(cwd);
|
||||
|
||||
if (selectedWorkflow === null) {
|
||||
@ -103,7 +117,7 @@ async function selectAndExecuteTask(cwd: string, task: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const { execCwd, isWorktree } = await confirmAndCreateWorktree(cwd, task);
|
||||
const { execCwd, isWorktree, branch } = await confirmAndCreateWorktree(cwd, task);
|
||||
|
||||
log.info('Starting task execution', { workflow: selectedWorkflow, worktree: isWorktree });
|
||||
const taskSuccess = await executeTask(task, execCwd, selectedWorkflow, cwd);
|
||||
@ -115,6 +129,26 @@ async function selectAndExecuteTask(cwd: string, task: string): Promise<void> {
|
||||
} else if (!commitResult.success) {
|
||||
error(`Auto-commit failed: ${commitResult.message}`);
|
||||
}
|
||||
|
||||
// PR creation: --auto-pr → create automatically, otherwise ask
|
||||
if (commitResult.success && commitResult.commitHash && branch) {
|
||||
const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false);
|
||||
if (shouldCreatePr) {
|
||||
info('Creating pull request...');
|
||||
const prBody = buildPrBody(undefined, `Workflow \`${selectedWorkflow}\` completed successfully.`);
|
||||
const prResult = createPullRequest(execCwd, {
|
||||
branch,
|
||||
title: task.length > 100 ? `${task.slice(0, 97)}...` : task,
|
||||
body: prBody,
|
||||
repo: options?.repo,
|
||||
});
|
||||
if (prResult.success) {
|
||||
success(`PR created: ${prResult.url}`);
|
||||
} else {
|
||||
error(`PR creation failed: ${prResult.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!taskSuccess) {
|
||||
@ -157,11 +191,24 @@ program
|
||||
.description('TAKT: Task Agent Koordination Tool')
|
||||
.version(cliVersion);
|
||||
|
||||
// --- Global options ---
|
||||
program
|
||||
.option('-i, --issue <number>', 'GitHub issue number (equivalent to #N)', (val: string) => parseInt(val, 10))
|
||||
.option('-w, --workflow <name>', 'Workflow to use')
|
||||
.option('-b, --branch <name>', 'Branch name (auto-generated if omitted)')
|
||||
.option('--auto-pr', 'Create PR after successful execution')
|
||||
.option('--repo <owner/repo>', 'Repository (defaults to current)')
|
||||
.option('-t, --task <string>', 'Task content (triggers pipeline/non-interactive mode)');
|
||||
|
||||
// Common initialization for all commands
|
||||
program.hook('preAction', async () => {
|
||||
resolvedCwd = resolve(process.cwd());
|
||||
|
||||
await initGlobalDirs();
|
||||
// Pipeline mode: triggered by --task (non-interactive)
|
||||
const rootOpts = program.opts();
|
||||
pipelineMode = rootOpts.task !== undefined;
|
||||
|
||||
await initGlobalDirs({ nonInteractive: pipelineMode });
|
||||
initProjectDirs(resolvedCwd);
|
||||
|
||||
const verbose = isVerboseMode(resolvedCwd);
|
||||
@ -181,7 +228,7 @@ program.hook('preAction', async () => {
|
||||
setLogLevel(config.logLevel);
|
||||
}
|
||||
|
||||
log.info('TAKT CLI starting', { version: cliVersion, cwd: resolvedCwd, verbose });
|
||||
log.info('TAKT CLI starting', { version: cliVersion, cwd: resolvedCwd, verbose, pipelineMode });
|
||||
});
|
||||
|
||||
// --- Subcommands ---
|
||||
@ -248,7 +295,7 @@ program
|
||||
await switchConfig(resolvedCwd, key);
|
||||
});
|
||||
|
||||
// --- Default action: task execution or interactive mode ---
|
||||
// --- Default action: task execution, interactive mode, or pipeline ---
|
||||
|
||||
/**
|
||||
* Check if the input is a task description (should execute directly)
|
||||
@ -265,9 +312,50 @@ function isDirectTask(input: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
program
|
||||
.argument('[task]', 'Task to execute (or GitHub issue reference like "#6")')
|
||||
.action(async (task?: string) => {
|
||||
const opts = program.opts();
|
||||
|
||||
// --- Pipeline mode (non-interactive): triggered by --task ---
|
||||
if (pipelineMode) {
|
||||
const exitCode = await executePipeline({
|
||||
issueNumber: opts.issue as number | undefined,
|
||||
task: opts.task as string,
|
||||
workflow: (opts.workflow as string | undefined) ?? DEFAULT_WORKFLOW_NAME,
|
||||
branch: opts.branch as string | undefined,
|
||||
autoPr: opts.autoPr === true,
|
||||
repo: opts.repo as string | undefined,
|
||||
cwd: resolvedCwd,
|
||||
});
|
||||
|
||||
if (exitCode !== 0) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Normal (interactive) mode ---
|
||||
|
||||
const prOptions = {
|
||||
autoPr: opts.autoPr === true,
|
||||
repo: opts.repo as string | undefined,
|
||||
};
|
||||
|
||||
// Resolve --issue N to task text (same as #N)
|
||||
const issueFromOption = opts.issue as number | undefined;
|
||||
if (issueFromOption) {
|
||||
try {
|
||||
const resolvedTask = resolveIssueTask(`#${issueFromOption}`);
|
||||
await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions);
|
||||
} catch (e) {
|
||||
error(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (task && isDirectTask(task)) {
|
||||
// Resolve #N issue references to task text
|
||||
let resolvedTask: string = task;
|
||||
@ -281,7 +369,7 @@ program
|
||||
}
|
||||
}
|
||||
|
||||
await selectAndExecuteTask(resolvedCwd, resolvedTask);
|
||||
await selectAndExecuteTask(resolvedCwd, resolvedTask, prOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -292,7 +380,7 @@ program
|
||||
return;
|
||||
}
|
||||
|
||||
await selectAndExecuteTask(resolvedCwd, result.task);
|
||||
await selectAndExecuteTask(resolvedCwd, result.task, prOptions);
|
||||
});
|
||||
|
||||
program.parse();
|
||||
|
||||
@ -12,3 +12,4 @@ export { switchWorkflow } from './workflow.js';
|
||||
export { switchConfig, getCurrentPermissionMode, setPermissionMode, type PermissionMode } from './config.js';
|
||||
export { listTasks } from './listTasks.js';
|
||||
export { interactiveMode } from './interactive.js';
|
||||
export { executePipeline, type PipelineExecutionOptions } from './pipelineExecution.js';
|
||||
|
||||
240
src/commands/pipelineExecution.ts
Normal file
240
src/commands/pipelineExecution.ts
Normal file
@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Pipeline execution flow
|
||||
*
|
||||
* Orchestrates the full pipeline:
|
||||
* 1. Fetch issue content
|
||||
* 2. Create branch
|
||||
* 3. Run workflow
|
||||
* 4. Commit & push
|
||||
* 5. Create PR
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fetchIssue, formatIssueAsTask, checkGhCli, type GitHubIssue } from '../github/issue.js';
|
||||
import { createPullRequest, pushBranch, buildPrBody } from '../github/pr.js';
|
||||
import { executeTask } from './taskExecution.js';
|
||||
import { loadGlobalConfig } from '../config/globalConfig.js';
|
||||
import { info, error, success, status } from '../utils/ui.js';
|
||||
import { createLogger } from '../utils/debug.js';
|
||||
import type { PipelineConfig } from '../models/types.js';
|
||||
import {
|
||||
EXIT_ISSUE_FETCH_FAILED,
|
||||
EXIT_WORKFLOW_FAILED,
|
||||
EXIT_GIT_OPERATION_FAILED,
|
||||
EXIT_PR_CREATION_FAILED,
|
||||
} from '../exitCodes.js';
|
||||
|
||||
const log = createLogger('pipeline');
|
||||
|
||||
export interface PipelineExecutionOptions {
|
||||
/** GitHub issue number */
|
||||
issueNumber?: number;
|
||||
/** Task content (alternative to issue) */
|
||||
task?: string;
|
||||
/** Workflow name */
|
||||
workflow: string;
|
||||
/** Branch name (auto-generated if omitted) */
|
||||
branch?: string;
|
||||
/** Whether to create a PR after successful execution */
|
||||
autoPr: boolean;
|
||||
/** Repository in owner/repo format */
|
||||
repo?: string;
|
||||
/** Working directory */
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand template variables in a string.
|
||||
* Supported: {title}, {issue}, {issue_body}, {report}
|
||||
*/
|
||||
function expandTemplate(template: string, vars: Record<string, string>): string {
|
||||
return template.replace(/\{(\w+)\}/g, (match, key: string) => vars[key] ?? match);
|
||||
}
|
||||
|
||||
/** Generate a branch name for pipeline execution */
|
||||
function generatePipelineBranchName(pipelineConfig: PipelineConfig | undefined, issueNumber?: number): string {
|
||||
const prefix = pipelineConfig?.defaultBranchPrefix ?? 'takt/';
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
if (issueNumber) {
|
||||
return `${prefix}issue-${issueNumber}-${timestamp}`;
|
||||
}
|
||||
return `${prefix}pipeline-${timestamp}`;
|
||||
}
|
||||
|
||||
/** Create and checkout a new branch */
|
||||
function createBranch(cwd: string, branch: string): void {
|
||||
execFileSync('git', ['checkout', '-b', branch], {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
}
|
||||
|
||||
/** Stage all changes and create a commit */
|
||||
function commitChanges(cwd: string, message: string): string | undefined {
|
||||
execFileSync('git', ['add', '-A'], { cwd, stdio: 'pipe' });
|
||||
|
||||
const statusOutput = execFileSync('git', ['status', '--porcelain'], {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
if (!statusOutput.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
execFileSync('git', ['commit', '-m', message], { cwd, stdio: 'pipe' });
|
||||
|
||||
return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
}
|
||||
|
||||
/** Build commit message from template or defaults */
|
||||
function buildCommitMessage(
|
||||
pipelineConfig: PipelineConfig | undefined,
|
||||
issue: GitHubIssue | undefined,
|
||||
taskText: string | undefined,
|
||||
): string {
|
||||
const template = pipelineConfig?.commitMessageTemplate;
|
||||
if (template && issue) {
|
||||
return expandTemplate(template, {
|
||||
title: issue.title,
|
||||
issue: String(issue.number),
|
||||
});
|
||||
}
|
||||
// Default commit message
|
||||
return issue
|
||||
? `feat: ${issue.title} (#${issue.number})`
|
||||
: `takt: ${taskText ?? 'pipeline task'}`;
|
||||
}
|
||||
|
||||
/** Build PR body from template or defaults */
|
||||
function buildPipelinePrBody(
|
||||
pipelineConfig: PipelineConfig | undefined,
|
||||
issue: GitHubIssue | undefined,
|
||||
report: string,
|
||||
): string {
|
||||
const template = pipelineConfig?.prBodyTemplate;
|
||||
if (template && issue) {
|
||||
return expandTemplate(template, {
|
||||
title: issue.title,
|
||||
issue: String(issue.number),
|
||||
issue_body: issue.body || issue.title,
|
||||
report,
|
||||
});
|
||||
}
|
||||
return buildPrBody(issue, report);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the full pipeline.
|
||||
*
|
||||
* Returns a process exit code (0 on success, 2-5 on specific failures).
|
||||
*/
|
||||
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
|
||||
const { cwd, workflow, autoPr } = options;
|
||||
const globalConfig = loadGlobalConfig();
|
||||
const pipelineConfig = globalConfig.pipeline;
|
||||
let issue: GitHubIssue | undefined;
|
||||
let task: string;
|
||||
|
||||
// --- Step 1: Resolve task content ---
|
||||
if (options.issueNumber) {
|
||||
info(`Fetching issue #${options.issueNumber}...`);
|
||||
try {
|
||||
const ghStatus = checkGhCli();
|
||||
if (!ghStatus.available) {
|
||||
error(ghStatus.error ?? 'gh CLI is not available');
|
||||
return EXIT_ISSUE_FETCH_FAILED;
|
||||
}
|
||||
issue = fetchIssue(options.issueNumber);
|
||||
task = formatIssueAsTask(issue);
|
||||
success(`Issue #${options.issueNumber} fetched: "${issue.title}"`);
|
||||
} catch (err) {
|
||||
error(`Failed to fetch issue #${options.issueNumber}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return EXIT_ISSUE_FETCH_FAILED;
|
||||
}
|
||||
} else if (options.task) {
|
||||
task = options.task;
|
||||
} else {
|
||||
error('Either --issue or --task must be specified');
|
||||
return EXIT_ISSUE_FETCH_FAILED;
|
||||
}
|
||||
|
||||
// --- Step 2: Create branch ---
|
||||
const branch = options.branch ?? generatePipelineBranchName(pipelineConfig, options.issueNumber);
|
||||
info(`Creating branch: ${branch}`);
|
||||
try {
|
||||
createBranch(cwd, branch);
|
||||
success(`Branch created: ${branch}`);
|
||||
} catch (err) {
|
||||
error(`Failed to create branch: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return EXIT_GIT_OPERATION_FAILED;
|
||||
}
|
||||
|
||||
// --- Step 3: Run workflow ---
|
||||
info(`Running workflow: ${workflow}`);
|
||||
log.info('Pipeline workflow execution starting', { workflow, branch, issueNumber: options.issueNumber });
|
||||
|
||||
const taskSuccess = await executeTask(task, cwd, workflow);
|
||||
|
||||
if (!taskSuccess) {
|
||||
error(`Workflow '${workflow}' failed`);
|
||||
return EXIT_WORKFLOW_FAILED;
|
||||
}
|
||||
success(`Workflow '${workflow}' completed`);
|
||||
|
||||
// --- Step 4: Commit & push ---
|
||||
const commitMessage = buildCommitMessage(pipelineConfig, issue, options.task);
|
||||
|
||||
info('Committing changes...');
|
||||
try {
|
||||
const commitHash = commitChanges(cwd, commitMessage);
|
||||
if (commitHash) {
|
||||
success(`Changes committed: ${commitHash}`);
|
||||
} else {
|
||||
info('No changes to commit');
|
||||
}
|
||||
|
||||
info(`Pushing to origin/${branch}...`);
|
||||
pushBranch(cwd, branch);
|
||||
success(`Pushed to origin/${branch}`);
|
||||
} catch (err) {
|
||||
error(`Git operation failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return EXIT_GIT_OPERATION_FAILED;
|
||||
}
|
||||
|
||||
// --- Step 5: Create PR (if --auto-pr) ---
|
||||
if (autoPr) {
|
||||
info('Creating pull request...');
|
||||
const prTitle = issue ? issue.title : (options.task ?? 'Pipeline task');
|
||||
const report = `Workflow \`${workflow}\` completed successfully.`;
|
||||
const prBody = buildPipelinePrBody(pipelineConfig, issue, report);
|
||||
|
||||
const prResult = createPullRequest(cwd, {
|
||||
branch,
|
||||
title: prTitle,
|
||||
body: prBody,
|
||||
repo: options.repo,
|
||||
});
|
||||
|
||||
if (prResult.success) {
|
||||
success(`PR created: ${prResult.url}`);
|
||||
} else {
|
||||
error(`PR creation failed: ${prResult.error}`);
|
||||
return EXIT_PR_CREATION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Summary ---
|
||||
console.log();
|
||||
status('Issue', issue ? `#${issue.number} "${issue.title}"` : 'N/A');
|
||||
status('Branch', branch);
|
||||
status('Workflow', workflow);
|
||||
status('Result', 'Success', 'green');
|
||||
|
||||
return 0;
|
||||
}
|
||||
@ -12,14 +12,22 @@ import type { GlobalConfig, DebugConfig, Language } from '../models/types.js';
|
||||
import { getGlobalConfigPath, getProjectConfigPath } from './paths.js';
|
||||
import { DEFAULT_LANGUAGE } from '../constants.js';
|
||||
|
||||
/** Create default global configuration (fresh instance each call) */
|
||||
function createDefaultGlobalConfig(): GlobalConfig {
|
||||
return {
|
||||
language: DEFAULT_LANGUAGE,
|
||||
trustedDirectories: [],
|
||||
defaultWorkflow: 'default',
|
||||
logLevel: 'info',
|
||||
provider: 'claude',
|
||||
};
|
||||
}
|
||||
|
||||
/** Load global configuration */
|
||||
export function loadGlobalConfig(): GlobalConfig {
|
||||
const configPath = getGlobalConfigPath();
|
||||
if (!existsSync(configPath)) {
|
||||
throw new Error(
|
||||
`Global config not found: ${configPath}\n` +
|
||||
'Run takt once to initialize the configuration.'
|
||||
);
|
||||
return createDefaultGlobalConfig();
|
||||
}
|
||||
const content = readFileSync(configPath, 'utf-8');
|
||||
const raw = parseYaml(content);
|
||||
@ -39,6 +47,11 @@ export function loadGlobalConfig(): GlobalConfig {
|
||||
disabledBuiltins: parsed.disabled_builtins,
|
||||
anthropicApiKey: parsed.anthropic_api_key,
|
||||
openaiApiKey: parsed.openai_api_key,
|
||||
pipeline: parsed.pipeline ? {
|
||||
defaultBranchPrefix: parsed.pipeline.default_branch_prefix,
|
||||
commitMessageTemplate: parsed.pipeline.commit_message_template,
|
||||
prBodyTemplate: parsed.pipeline.pr_body_template,
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,6 +86,15 @@ export function saveGlobalConfig(config: GlobalConfig): void {
|
||||
if (config.openaiApiKey) {
|
||||
raw.openai_api_key = config.openaiApiKey;
|
||||
}
|
||||
if (config.pipeline) {
|
||||
const pipelineRaw: Record<string, unknown> = {};
|
||||
if (config.pipeline.defaultBranchPrefix) pipelineRaw.default_branch_prefix = config.pipeline.defaultBranchPrefix;
|
||||
if (config.pipeline.commitMessageTemplate) pipelineRaw.commit_message_template = config.pipeline.commitMessageTemplate;
|
||||
if (config.pipeline.prBodyTemplate) pipelineRaw.pr_body_template = config.pipeline.prBodyTemplate;
|
||||
if (Object.keys(pipelineRaw).length > 0) {
|
||||
raw.pipeline = pipelineRaw;
|
||||
}
|
||||
}
|
||||
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
||||
}
|
||||
|
||||
|
||||
@ -76,16 +76,32 @@ export async function promptProviderSelection(): Promise<'claude' | 'codex'> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Options for global directory initialization */
|
||||
export interface InitGlobalDirsOptions {
|
||||
/** Skip interactive prompts (CI/non-TTY environments) */
|
||||
nonInteractive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize global takt directory structure with language selection.
|
||||
* On first run, creates config.yaml from language template.
|
||||
* Agents/workflows are NOT copied — they are loaded via builtin fallback.
|
||||
*
|
||||
* In non-interactive mode (pipeline mode or no TTY), skips prompts
|
||||
* and uses default values so takt works in pipeline/CI environments without config.yaml.
|
||||
*/
|
||||
export async function initGlobalDirs(): Promise<void> {
|
||||
export async function initGlobalDirs(options?: InitGlobalDirsOptions): Promise<void> {
|
||||
ensureDir(getGlobalConfigDir());
|
||||
ensureDir(getGlobalLogsDir());
|
||||
|
||||
if (needsLanguageSetup()) {
|
||||
const isInteractive = !options?.nonInteractive && process.stdin.isTTY === true;
|
||||
|
||||
if (!isInteractive) {
|
||||
// Pipeline / non-interactive: skip prompts, use defaults via loadGlobalConfig() fallback
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = await promptLanguageSelection();
|
||||
const provider = await promptProviderSelection();
|
||||
|
||||
|
||||
13
src/exitCodes.ts
Normal file
13
src/exitCodes.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Process exit codes for takt CLI
|
||||
*
|
||||
* Fine-grained exit codes allow pipelines to distinguish
|
||||
* between different failure modes.
|
||||
*/
|
||||
|
||||
export const EXIT_SUCCESS = 0;
|
||||
export const EXIT_GENERAL_ERROR = 1;
|
||||
export const EXIT_ISSUE_FETCH_FAILED = 2;
|
||||
export const EXIT_WORKFLOW_FAILED = 3;
|
||||
export const EXIT_GIT_OPERATION_FAILED = 4;
|
||||
export const EXIT_PR_CREATION_FAILED = 5;
|
||||
@ -5,7 +5,7 @@
|
||||
* for workflow execution or task creation.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createLogger } from '../utils/debug.js';
|
||||
|
||||
const log = createLogger('github');
|
||||
@ -31,11 +31,11 @@ export interface GhCliStatus {
|
||||
*/
|
||||
export function checkGhCli(): GhCliStatus {
|
||||
try {
|
||||
execSync('gh auth status', { stdio: 'pipe' });
|
||||
execFileSync('gh', ['auth', 'status'], { stdio: 'pipe' });
|
||||
return { available: true };
|
||||
} catch {
|
||||
try {
|
||||
execSync('gh --version', { stdio: 'pipe' });
|
||||
execFileSync('gh', ['--version'], { stdio: 'pipe' });
|
||||
return {
|
||||
available: false,
|
||||
error: 'gh CLI is installed but not authenticated. Run `gh auth login` first.',
|
||||
@ -56,8 +56,9 @@ export function checkGhCli(): GhCliStatus {
|
||||
export function fetchIssue(issueNumber: number): GitHubIssue {
|
||||
log.debug('Fetching issue', { issueNumber });
|
||||
|
||||
const raw = execSync(
|
||||
`gh issue view ${issueNumber} --json number,title,body,labels,comments`,
|
||||
const raw = execFileSync(
|
||||
'gh',
|
||||
['issue', 'view', String(issueNumber), '--json', 'number,title,body,labels,comments'],
|
||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
||||
);
|
||||
|
||||
|
||||
113
src/github/pr.ts
Normal file
113
src/github/pr.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* GitHub Pull Request utilities
|
||||
*
|
||||
* Creates PRs via `gh` CLI for CI/CD integration.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createLogger } from '../utils/debug.js';
|
||||
import { checkGhCli, type GitHubIssue } from './issue.js';
|
||||
|
||||
const log = createLogger('github-pr');
|
||||
|
||||
export interface CreatePrOptions {
|
||||
/** Branch to create PR from */
|
||||
branch: string;
|
||||
/** PR title */
|
||||
title: string;
|
||||
/** PR body (markdown) */
|
||||
body: string;
|
||||
/** Base branch (default: repo default branch) */
|
||||
base?: string;
|
||||
/** Repository in owner/repo format (optional, uses current repo if omitted) */
|
||||
repo?: string;
|
||||
}
|
||||
|
||||
export interface CreatePrResult {
|
||||
success: boolean;
|
||||
/** PR URL on success */
|
||||
url?: string;
|
||||
/** Error message on failure */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a branch to origin.
|
||||
* Throws on failure.
|
||||
*/
|
||||
export function pushBranch(cwd: string, branch: string): void {
|
||||
log.info('Pushing branch to origin', { branch });
|
||||
execFileSync('git', ['push', 'origin', branch], {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Pull Request via `gh pr create`.
|
||||
*/
|
||||
export function createPullRequest(cwd: string, options: CreatePrOptions): CreatePrResult {
|
||||
const ghStatus = checkGhCli();
|
||||
if (!ghStatus.available) {
|
||||
return { success: false, error: ghStatus.error ?? 'gh CLI is not available' };
|
||||
}
|
||||
|
||||
const args = [
|
||||
'pr', 'create',
|
||||
'--title', options.title,
|
||||
'--body', options.body,
|
||||
'--head', options.branch,
|
||||
];
|
||||
|
||||
if (options.base) {
|
||||
args.push('--base', options.base);
|
||||
}
|
||||
|
||||
if (options.repo) {
|
||||
args.push('--repo', options.repo);
|
||||
}
|
||||
|
||||
log.info('Creating PR', { branch: options.branch, title: options.title });
|
||||
|
||||
try {
|
||||
const output = execFileSync('gh', args, {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const url = output.trim();
|
||||
log.info('PR created', { url });
|
||||
|
||||
return { success: true, url };
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
log.error('PR creation failed', { error: errorMessage });
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build PR body from issue and execution report.
|
||||
*/
|
||||
export function buildPrBody(issue: GitHubIssue | undefined, report: string): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push('## Summary');
|
||||
if (issue) {
|
||||
parts.push('');
|
||||
parts.push(issue.body || issue.title);
|
||||
}
|
||||
|
||||
parts.push('');
|
||||
parts.push('## Execution Report');
|
||||
parts.push('');
|
||||
parts.push(report);
|
||||
|
||||
if (issue) {
|
||||
parts.push('');
|
||||
parts.push(`Closes #${issue.number}`);
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
@ -159,6 +159,13 @@ export const DebugConfigSchema = z.object({
|
||||
/** Language setting schema */
|
||||
export const LanguageSchema = z.enum(['en', 'ja']);
|
||||
|
||||
/** Pipeline execution config schema */
|
||||
export const PipelineConfigSchema = z.object({
|
||||
default_branch_prefix: z.string().optional(),
|
||||
commit_message_template: z.string().optional(),
|
||||
pr_body_template: z.string().optional(),
|
||||
});
|
||||
|
||||
/** Global config schema */
|
||||
export const GlobalConfigSchema = z.object({
|
||||
language: LanguageSchema.optional().default(DEFAULT_LANGUAGE),
|
||||
@ -176,6 +183,8 @@ export const GlobalConfigSchema = z.object({
|
||||
anthropic_api_key: z.string().optional(),
|
||||
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
|
||||
openai_api_key: z.string().optional(),
|
||||
/** Pipeline execution settings */
|
||||
pipeline: PipelineConfigSchema.optional(),
|
||||
});
|
||||
|
||||
/** Project config schema */
|
||||
|
||||
@ -178,6 +178,16 @@ export interface DebugConfig {
|
||||
/** Language setting for takt */
|
||||
export type Language = 'en' | 'ja';
|
||||
|
||||
/** Pipeline execution configuration */
|
||||
export interface PipelineConfig {
|
||||
/** Branch name prefix for pipeline-created branches (default: "takt/") */
|
||||
defaultBranchPrefix?: string;
|
||||
/** Commit message template. Variables: {title}, {issue} */
|
||||
commitMessageTemplate?: string;
|
||||
/** PR body template. Variables: {issue_body}, {report}, {issue} */
|
||||
prBodyTemplate?: string;
|
||||
}
|
||||
|
||||
/** Global configuration for takt */
|
||||
export interface GlobalConfig {
|
||||
language: Language;
|
||||
@ -195,6 +205,8 @@ export interface GlobalConfig {
|
||||
anthropicApiKey?: string;
|
||||
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
|
||||
openaiApiKey?: string;
|
||||
/** Pipeline execution settings */
|
||||
pipeline?: PipelineConfig;
|
||||
}
|
||||
|
||||
/** Project-level configuration */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user