commit
4c6861457c
40
CHANGELOG.md
40
CHANGELOG.md
@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [0.10.0] - 2026-02-09
|
||||
|
||||
### Added
|
||||
|
||||
- **`structural-reform` builtin piece**: Full project review and structural reform — iterative codebase restructuring with staged file splits, powered by `loop_monitors`
|
||||
- **`unit-test` builtin piece**: Unit test focused piece — test analysis → test implementation → review → fix, with `loop_monitors` for cycle control
|
||||
- **`test-planner` persona**: Specialized persona for analyzing codebase and planning comprehensive test strategies
|
||||
- **Interactive mode variants**: Four selectable modes after piece selection — `assistant` (default: AI-guided requirement refinement), `persona` (conversation with first movement's persona), `quiet` (generate instructions without questions), `passthrough` (user input used as-is)
|
||||
- **`persona_providers` config**: Per-persona provider overrides (e.g., `{ coder: 'codex' }`) — route specific personas to different providers without creating hybrid pieces
|
||||
- **`task_poll_interval_ms` config**: Configurable polling interval for `takt run` to detect new tasks during execution (default: 500ms, range: 100–5000ms)
|
||||
- **`interactive_mode` piece field**: Piece-level default interactive mode override (e.g., set `passthrough` for pieces that don't benefit from AI planning)
|
||||
- **Task-level output prefixing**: Colored `[taskName]` prefix on all output lines during parallel `takt run` execution, preventing mid-line interleaving between concurrent tasks
|
||||
- **Review policy facet**: Shared review policy (`builtins/{lang}/policies/review.md`) for consistent review criteria across pieces
|
||||
|
||||
### Changed
|
||||
|
||||
- **BREAKING:** Removed all Hybrid Codex pieces (`*-hybrid-codex`) — replaced by `persona_providers` config which achieves the same result without duplicating piece files
|
||||
- Removed `tools/generate-hybrid-codex.mjs` (no longer needed with `persona_providers`)
|
||||
- Improved parallel execution output: movement-level prefix now includes task context and iteration info in concurrent runs
|
||||
- Codex client now detects stream hangs (10-minute idle timeout) and distinguishes timeout vs external abort in error messages
|
||||
- Parallel task execution (`takt run`) now polls for newly added tasks during execution instead of only checking between task completions
|
||||
- Parallel task execution no longer enforces per-task time limits (previously had a timeout)
|
||||
- Issue references now routed through interactive mode (as initial input) instead of skipping interactive mode entirely
|
||||
- Builtin `config.yaml` updated to document all GlobalConfig fields
|
||||
- Extracted `conversationLoop.ts` for shared conversation logic across interactive mode variants
|
||||
- Line editor improvements: additional key bindings and edge case fixes
|
||||
|
||||
### Fixed
|
||||
|
||||
- Codex processes hanging indefinitely when stream becomes idle — now aborted after 10 minutes of inactivity, releasing worker pool slots
|
||||
|
||||
### Internal
|
||||
|
||||
- New test coverage: engine-persona-providers, interactive-mode (532 lines), task-prefix-writer, workerPool expansion, pieceResolver expansion, lineEditor expansion, parallel-logger expansion, globalConfig-defaults expansion, pieceExecution-debug-prompts expansion, it-piece-loader expansion, runAllTasks-concurrency expansion, engine-parallel
|
||||
- Extracted `TaskPrefixWriter` for task-level parallel output management
|
||||
- Extracted `modeSelection.ts`, `passthroughMode.ts`, `personaMode.ts`, `quietMode.ts` from interactive module
|
||||
- `InteractiveMode` type model added (`src/core/models/interactive-mode.ts`)
|
||||
- `PieceEngine` validates `taskPrefix`/`taskColorIndex` pair consistency at construction
|
||||
- Implementation notes document added (`docs/implements/retry-and-session.ja.md`)
|
||||
|
||||
## [0.9.0] - 2026-02-08
|
||||
|
||||
### Added
|
||||
|
||||
32
README.md
32
README.md
@ -92,13 +92,25 @@ takt
|
||||
takt hello
|
||||
```
|
||||
|
||||
**Note:** Issue references (`#6`) and `--task` / `--issue` options skip interactive mode and execute the task directly. All other inputs (including text with spaces) enter interactive mode for requirement refinement.
|
||||
**Note:** `--task` option skips interactive mode and executes the task directly. Issue references (`#6`, `--issue`) are used as initial input in interactive mode.
|
||||
|
||||
**Flow:**
|
||||
1. Select piece
|
||||
2. Refine task content through conversation with AI
|
||||
3. Finalize task instructions with `/go` (you can also add additional instructions like `/go additional instructions`), or use `/play <task>` to execute a task immediately
|
||||
4. Execute (create worktree, run piece, create PR)
|
||||
2. Select interactive mode (assistant / persona / quiet / passthrough)
|
||||
3. Refine task content through conversation with AI
|
||||
4. Finalize task instructions with `/go` (you can also add additional instructions like `/go additional instructions`), or use `/play <task>` to execute a task immediately
|
||||
5. Execute (create worktree, run piece, create PR)
|
||||
|
||||
#### Interactive Mode Variants
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `assistant` | Default. AI asks clarifying questions before generating task instructions. |
|
||||
| `persona` | Conversation with the first movement's persona (uses its system prompt and tools). |
|
||||
| `quiet` | Generates task instructions without asking questions (best-effort). |
|
||||
| `passthrough` | Passes user input directly as task text without AI processing. |
|
||||
|
||||
Pieces can set a default mode via the `interactive_mode` field in YAML.
|
||||
|
||||
#### Execution Example
|
||||
|
||||
@ -451,8 +463,10 @@ TAKT includes multiple builtin pieces:
|
||||
| `passthrough` | Thinnest wrapper. Pass task directly to coder as-is. No review. |
|
||||
| `compound-eye` | Multi-model review: sends the same instruction to Claude and Codex simultaneously, then synthesizes both responses. |
|
||||
| `review-only` | Read-only code review piece that makes no changes. |
|
||||
| `structural-reform` | Full project review and structural reform: iterative codebase restructuring with staged file splits. |
|
||||
| `unit-test` | Unit test focused piece: test analysis → test implementation → review → fix. |
|
||||
|
||||
**Hybrid Codex variants** (`*-hybrid-codex`): Each major piece has a Codex variant where the coder agent runs on Codex while reviewers use Claude. Available for: default, minimal, expert, expert-cqrs, passthrough, review-fix-minimal, coding.
|
||||
**Per-persona provider overrides:** Use `persona_providers` in config to route specific personas to different providers (e.g., coder on Codex, reviewers on Claude) without duplicating pieces.
|
||||
|
||||
Use `takt switch` to switch pieces.
|
||||
|
||||
@ -475,6 +489,7 @@ Use `takt switch` to switch pieces.
|
||||
| **research-planner** | Research task planning and scope definition |
|
||||
| **research-digger** | Deep investigation and information gathering |
|
||||
| **research-supervisor** | Research quality validation and completeness assessment |
|
||||
| **test-planner** | Test strategy analysis and comprehensive test planning |
|
||||
| **pr-commenter** | Posts review findings as GitHub PR comments |
|
||||
|
||||
## Custom Personas
|
||||
@ -543,8 +558,15 @@ branch_name_strategy: romaji # Branch name generation: 'romaji' (fast) or 'ai'
|
||||
prevent_sleep: false # Prevent macOS idle sleep during execution (caffeinate)
|
||||
notification_sound: true # Enable/disable notification sounds
|
||||
concurrency: 1 # Parallel task count for takt run (1-10, default: 1 = sequential)
|
||||
task_poll_interval_ms: 500 # Polling interval for new tasks during takt run (100-5000, default: 500)
|
||||
interactive_preview_movements: 3 # Movement previews in interactive mode (0-10, default: 3)
|
||||
|
||||
# Per-persona provider overrides (optional)
|
||||
# Route specific personas to different providers without duplicating pieces
|
||||
# persona_providers:
|
||||
# coder: codex # Run coder on Codex
|
||||
# ai-antipattern-reviewer: claude # Keep reviewers on Claude
|
||||
|
||||
# API Key configuration (optional)
|
||||
# Can be overridden by environment variables TAKT_ANTHROPIC_API_KEY / TAKT_OPENAI_API_KEY
|
||||
anthropic_api_key: sk-ant-... # For Claude (Anthropic)
|
||||
|
||||
@ -1,34 +1,85 @@
|
||||
# TAKT Global Configuration
|
||||
# This file contains default settings for takt.
|
||||
# Location: ~/.takt/config.yaml
|
||||
|
||||
# Language setting (en or ja)
|
||||
# ── Basic ──
|
||||
|
||||
# Language (en | ja)
|
||||
language: en
|
||||
|
||||
# Default piece to use when no piece is specified
|
||||
# Default piece when no piece is specified
|
||||
default_piece: default
|
||||
|
||||
# Log level: debug, info, warn, error
|
||||
# Log level (debug | info | warn | error)
|
||||
log_level: info
|
||||
|
||||
# Provider runtime: claude or codex
|
||||
# ── Provider & Model ──
|
||||
|
||||
# Provider runtime (claude | codex)
|
||||
provider: claude
|
||||
|
||||
# Builtin pieces (resources/global/{lang}/pieces)
|
||||
# enable_builtin_pieces: true
|
||||
|
||||
# Default model (optional)
|
||||
# Claude: opus, sonnet, haiku, opusplan, default, or full model name
|
||||
# Claude: opus, sonnet, haiku
|
||||
# Codex: gpt-5.2-codex, gpt-5.1-codex, etc.
|
||||
# model: sonnet
|
||||
|
||||
# Anthropic API key (optional, overridden by TAKT_ANTHROPIC_API_KEY env var)
|
||||
# anthropic_api_key: ""
|
||||
# Per-persona provider override (optional)
|
||||
# Override provider for specific personas. Others use the global provider.
|
||||
# persona_providers:
|
||||
# coder: codex
|
||||
|
||||
# OpenAI API key (optional, overridden by TAKT_OPENAI_API_KEY env var)
|
||||
# ── API Keys ──
|
||||
# Optional. Environment variables take priority:
|
||||
# TAKT_ANTHROPIC_API_KEY, TAKT_OPENAI_API_KEY
|
||||
|
||||
# anthropic_api_key: ""
|
||||
# openai_api_key: ""
|
||||
|
||||
# Pipeline execution settings (optional)
|
||||
# Customize branch naming, commit messages, and PR body for pipeline mode (--task).
|
||||
# ── Execution ──
|
||||
|
||||
# Worktree (shared clone) directory (default: ../{clone-name} relative to project)
|
||||
# worktree_dir: ~/takt-worktrees
|
||||
|
||||
# Auto-create PR after worktree execution (default: prompt in interactive mode)
|
||||
# auto_pr: false
|
||||
|
||||
# Prevent macOS idle sleep during execution using caffeinate (default: false)
|
||||
# prevent_sleep: false
|
||||
|
||||
# ── Parallel Execution (takt run) ──
|
||||
|
||||
# Number of tasks to run concurrently (1 = sequential, max: 10)
|
||||
# concurrency: 1
|
||||
|
||||
# Polling interval in ms for picking up new tasks (100-5000, default: 500)
|
||||
# task_poll_interval_ms: 500
|
||||
|
||||
# ── Interactive Mode ──
|
||||
|
||||
# Number of movement previews shown in interactive mode (0 to disable, max: 10)
|
||||
# interactive_preview_movements: 3
|
||||
|
||||
# Branch name generation strategy (romaji: fast default | ai: slow)
|
||||
# branch_name_strategy: romaji
|
||||
|
||||
# ── Output ──
|
||||
|
||||
# Notification sounds (default: true)
|
||||
# notification_sound: true
|
||||
|
||||
# Minimal output for CI - suppress AI output (default: false)
|
||||
# minimal_output: false
|
||||
|
||||
# ── Builtin Pieces ──
|
||||
|
||||
# Enable builtin pieces (default: true)
|
||||
# enable_builtin_pieces: true
|
||||
|
||||
# Exclude specific builtins from loading
|
||||
# disabled_builtins:
|
||||
# - magi
|
||||
|
||||
# ── Pipeline Mode (--pipeline) ──
|
||||
|
||||
# pipeline:
|
||||
# default_branch_prefix: "takt/"
|
||||
# commit_message_template: "feat: {title} (#{issue})"
|
||||
@ -37,10 +88,14 @@ provider: claude
|
||||
# {issue_body}
|
||||
# Closes #{issue}
|
||||
|
||||
# Notification sounds (true: enabled, false: disabled, default: true)
|
||||
# notification_sound: true
|
||||
# ── Preferences ──
|
||||
|
||||
# Custom paths for preference files
|
||||
# bookmarks_file: ~/.takt/preferences/bookmarks.yaml
|
||||
# piece_categories_file: ~/.takt/preferences/piece-categories.yaml
|
||||
|
||||
# ── Debug ──
|
||||
|
||||
# Debug settings (optional)
|
||||
# debug:
|
||||
# enabled: false
|
||||
# log_file: ~/.takt/logs/debug.log
|
||||
|
||||
52
builtins/en/instructions/implement-test.md
Normal file
52
builtins/en/instructions/implement-test.md
Normal file
@ -0,0 +1,52 @@
|
||||
Implement unit tests according to the test plan.
|
||||
Refer only to files within the Report Directory shown in the Piece Context. Do not search or reference other report directories.
|
||||
|
||||
**Important: Do NOT modify production code. Only test files may be edited.**
|
||||
|
||||
**Actions:**
|
||||
1. Review the test plan report
|
||||
2. Implement the planned test cases
|
||||
3. Run tests and verify all pass
|
||||
4. Confirm existing tests are not broken
|
||||
|
||||
**Test implementation constraints:**
|
||||
- Follow the project's existing test patterns (naming conventions, directory structure, helpers)
|
||||
- Write tests in Given-When-Then structure
|
||||
- One concept per test. Do not mix multiple concerns in a single test
|
||||
|
||||
**Scope output contract (create at the start of implementation):**
|
||||
```markdown
|
||||
# Change Scope Declaration
|
||||
|
||||
## Task
|
||||
{One-line task summary}
|
||||
|
||||
## Planned changes
|
||||
| Type | File |
|
||||
|------|------|
|
||||
| Create | `src/__tests__/example.test.ts` |
|
||||
|
||||
## Estimated size
|
||||
Small / Medium / Large
|
||||
|
||||
## Impact area
|
||||
- {Affected modules or features}
|
||||
```
|
||||
|
||||
**Decisions output contract (at implementation completion, only if decisions were made):**
|
||||
```markdown
|
||||
# Decision Log
|
||||
|
||||
## 1. {Decision}
|
||||
- **Context**: {Why the decision was needed}
|
||||
- **Options considered**: {List of options}
|
||||
- **Rationale**: {Reason for the choice}
|
||||
```
|
||||
|
||||
**Required output (include headings)**
|
||||
## Work results
|
||||
- {Summary of actions taken}
|
||||
## Changes made
|
||||
- {Summary of changes}
|
||||
## Test results
|
||||
- {Command executed and results}
|
||||
11
builtins/en/instructions/plan-test.md
Normal file
11
builtins/en/instructions/plan-test.md
Normal file
@ -0,0 +1,11 @@
|
||||
Analyze the target code and identify missing unit tests.
|
||||
|
||||
**Note:** If a Previous Response exists, this is a replan due to rejection.
|
||||
Revise the test plan taking that feedback into account.
|
||||
|
||||
**Actions:**
|
||||
1. Read the target module source code and understand its behavior, branches, and state transitions
|
||||
2. Read existing tests and identify what is already covered
|
||||
3. Identify missing test cases (happy path, error cases, boundary values, edge cases)
|
||||
4. Determine test strategy (mock approach, existing test helper usage, fixture design)
|
||||
5. Provide concrete guidelines for the test implementer
|
||||
@ -9,8 +9,14 @@ Do not review AI-specific issues (already covered by the ai_review movement).
|
||||
- Dead code
|
||||
- Call chain verification
|
||||
|
||||
**Previous finding tracking (required):**
|
||||
- First, extract open findings from "Previous Response"
|
||||
- Assign `finding_id` to each finding and classify current status as `new / persists / resolved`
|
||||
- If status is `persists`, provide concrete unresolved evidence (file/line)
|
||||
|
||||
## Judgment Procedure
|
||||
|
||||
1. Review the change diff and detect issues based on the architecture and design criteria above
|
||||
2. For each detected issue, classify as blocking/non-blocking based on Policy's scope determination table and judgment rules
|
||||
3. If there is even one blocking issue, judge as REJECT
|
||||
1. First, extract previous open findings and preliminarily classify as `new / persists / resolved`
|
||||
2. Review the change diff and detect issues based on the architecture and design criteria above
|
||||
3. For each detected issue, classify as blocking/non-blocking based on Policy's scope determination table and judgment rules
|
||||
4. If there is even one blocking issue (`new` or `persists`), judge as REJECT
|
||||
|
||||
@ -7,8 +7,14 @@ Review the changes from a quality assurance perspective.
|
||||
- Logging and monitoring
|
||||
- Maintainability
|
||||
|
||||
**Previous finding tracking (required):**
|
||||
- First, extract open findings from "Previous Response"
|
||||
- Assign `finding_id` to each finding and classify current status as `new / persists / resolved`
|
||||
- If status is `persists`, provide concrete unresolved evidence (file/line)
|
||||
|
||||
## Judgment Procedure
|
||||
|
||||
1. Review the change diff and detect issues based on the quality assurance criteria above
|
||||
2. For each detected issue, classify as blocking/non-blocking based on Policy's scope determination table and judgment rules
|
||||
3. If there is even one blocking issue, judge as REJECT
|
||||
1. First, extract previous open findings and preliminarily classify as `new / persists / resolved`
|
||||
2. Review the change diff and detect issues based on the quality assurance criteria above
|
||||
3. For each detected issue, classify as blocking/non-blocking based on Policy's scope determination table and judgment rules
|
||||
4. If there is even one blocking issue (`new` or `persists`), judge as REJECT
|
||||
|
||||
14
builtins/en/instructions/review-test.md
Normal file
14
builtins/en/instructions/review-test.md
Normal file
@ -0,0 +1,14 @@
|
||||
Review the changes from a test quality perspective.
|
||||
|
||||
**Review criteria:**
|
||||
- Whether all test plan items are covered
|
||||
- Test quality (Given-When-Then structure, independence, reproducibility)
|
||||
- Test naming conventions
|
||||
- Completeness (unnecessary tests, missing cases)
|
||||
- Appropriateness of mocks and fixtures
|
||||
|
||||
## Judgment Procedure
|
||||
|
||||
1. Cross-reference the test plan report ({report:00-test-plan.md}) with the implemented tests
|
||||
2. For each detected issue, classify as blocking/non-blocking based on Policy's scope determination table and judgment rules
|
||||
3. If there is even one blocking issue, judge as REJECT
|
||||
@ -14,10 +14,15 @@
|
||||
- [x] Dead code
|
||||
- [x] Call chain verification
|
||||
|
||||
## Previous Open Findings
|
||||
| finding_id | Previous Status | Current Status (new/persists/resolved) | Evidence |
|
||||
|------------|-----------------|-----------------------------------------|----------|
|
||||
| ARCH-EXAMPLE-src-file-L42 | open | persists | `src/file.ts:42` |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Scope | Location | Issue | Fix Suggestion |
|
||||
|---|-------|----------|-------|----------------|
|
||||
| 1 | In-scope | `src/file.ts:42` | Issue description | Fix approach |
|
||||
| # | finding_id | Status (new/persists) | Scope | Location | Issue | Fix Suggestion |
|
||||
|---|------------|-----------------------|-------|----------|-------|----------------|
|
||||
| 1 | ARCH-EXAMPLE-src-file-L42 | new | In-scope | `src/file.ts:42` | Issue description | Fix approach |
|
||||
|
||||
Scope: "In-scope" (fixable in this change) / "Out-of-scope" (existing issue, non-blocking)
|
||||
|
||||
|
||||
@ -15,8 +15,13 @@
|
||||
| Documentation | ✅ | - |
|
||||
| Maintainability | ✅ | - |
|
||||
|
||||
## Previous Open Findings
|
||||
| finding_id | Previous Status | Current Status (new/persists/resolved) | Evidence |
|
||||
|------------|-----------------|-----------------------------------------|----------|
|
||||
| QA-EXAMPLE-src-file-L42 | open | persists | `src/file.ts:42` |
|
||||
|
||||
## Issues (if REJECT)
|
||||
| # | Category | Issue | Fix Suggestion |
|
||||
|---|----------|-------|----------------|
|
||||
| 1 | Testing | Issue description | Fix approach |
|
||||
| # | finding_id | Status (new/persists) | Category | Issue | Fix Suggestion |
|
||||
|---|------------|-----------------------|----------|-------|----------------|
|
||||
| 1 | QA-EXAMPLE-src-file-L42 | new | Testing | Issue description | Fix approach |
|
||||
```
|
||||
|
||||
24
builtins/en/output-contracts/test-plan.md
Normal file
24
builtins/en/output-contracts/test-plan.md
Normal file
@ -0,0 +1,24 @@
|
||||
```markdown
|
||||
# Test Plan
|
||||
|
||||
## Target Modules
|
||||
{List of modules to analyze}
|
||||
|
||||
## Existing Test Analysis
|
||||
| Module | Existing Tests | Coverage Status |
|
||||
|--------|---------------|-----------------|
|
||||
| `src/xxx.ts` | `xxx.test.ts` | {Coverage status} |
|
||||
|
||||
## Missing Test Cases
|
||||
| # | Target | Test Case | Priority | Reason |
|
||||
|---|--------|-----------|----------|--------|
|
||||
| 1 | `src/xxx.ts` | {Test case summary} | High/Medium/Low | {Reason} |
|
||||
|
||||
## Test Strategy
|
||||
- {Mock approach}
|
||||
- {Fixture design}
|
||||
- {Test helper usage}
|
||||
|
||||
## Implementation Guidelines
|
||||
- {Concrete instructions for the test implementer}
|
||||
```
|
||||
25
builtins/en/personas/test-planner.md
Normal file
25
builtins/en/personas/test-planner.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Test Planner
|
||||
|
||||
You are a **test analysis and planning specialist**. You understand the behavior of target code, analyze existing test coverage, and systematically identify missing test cases.
|
||||
|
||||
## Role Boundaries
|
||||
|
||||
**Do:**
|
||||
- Analyze target code behavior, branches, and state transitions
|
||||
- Analyze existing test coverage
|
||||
- Identify missing test cases (happy path, error cases, boundary values, edge cases)
|
||||
- Determine test strategy (mock approach, fixture design, test helper usage)
|
||||
- Provide concrete guidelines for test implementers
|
||||
|
||||
**Don't:**
|
||||
- Plan production code changes (Planner's job)
|
||||
- Implement test code (Coder's job)
|
||||
- Review code (Reviewer's job)
|
||||
|
||||
## Behavioral Principles
|
||||
|
||||
- Read the code before planning. Don't list test cases based on guesses
|
||||
- Always check existing tests. Don't duplicate already-covered scenarios
|
||||
- Prioritize tests: business logic and state transitions > edge cases > simple CRUD
|
||||
- Provide instructions at a granularity that prevents test implementers from hesitating
|
||||
- Follow the project's existing test patterns. Don't propose novel conventions
|
||||
@ -10,6 +10,7 @@ piece_categories:
|
||||
pieces:
|
||||
- review-fix-minimal
|
||||
- review-only
|
||||
- unit-test
|
||||
🎨 Frontend: {}
|
||||
⚙️ Backend: {}
|
||||
🔧 Expert:
|
||||
@ -17,20 +18,9 @@ piece_categories:
|
||||
pieces:
|
||||
- expert
|
||||
- expert-cqrs
|
||||
🔀 Hybrid (Codex Coding):
|
||||
🚀 Quick Start:
|
||||
Refactoring:
|
||||
pieces:
|
||||
- coding-hybrid-codex
|
||||
- default-hybrid-codex
|
||||
- minimal-hybrid-codex
|
||||
- passthrough-hybrid-codex
|
||||
🔧 Expert:
|
||||
pieces:
|
||||
- expert-cqrs-hybrid-codex
|
||||
- expert-hybrid-codex
|
||||
🔍 Review & Fix:
|
||||
pieces:
|
||||
- review-fix-minimal-hybrid-codex
|
||||
- structural-reform
|
||||
Others:
|
||||
pieces:
|
||||
- research
|
||||
|
||||
@ -1,158 +0,0 @@
|
||||
# Auto-generated from coding.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: coding-hybrid-codex
|
||||
description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete)
|
||||
max_iterations: 20
|
||||
knowledge:
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
fix: ../instructions/fix.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
initial_movement: plan
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: Requirements are clear and implementable
|
||||
next: implement
|
||||
- condition: User is asking a question (not an implementation task)
|
||||
next: COMPLETE
|
||||
- condition: Requirements are unclear, insufficient information
|
||||
next: ABORT
|
||||
instruction: plan
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Implementation complete
|
||||
next: reviewers
|
||||
- condition: Implementation not started (report only)
|
||||
next: reviewers
|
||||
- condition: Cannot determine, insufficient information
|
||||
next: reviewers
|
||||
- condition: User input required
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction: implement
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: No AI-specific issues
|
||||
- condition: AI-specific issues found
|
||||
instruction: ai-review
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-ai-review.md
|
||||
format: ai-review
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-architect-review.md
|
||||
format: architecture-review
|
||||
rules:
|
||||
- condition: all("No AI-specific issues", "approved")
|
||||
next: COMPLETE
|
||||
- condition: any("AI-specific issues found", "needs_fix")
|
||||
next: fix
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
- condition: Cannot determine, insufficient information
|
||||
next: ABORT
|
||||
instruction: fix
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
@ -1,341 +0,0 @@
|
||||
# Auto-generated from expert-cqrs.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: expert-cqrs-hybrid-codex
|
||||
description: CQRS+ES, Frontend, Security, QA Expert Review
|
||||
max_iterations: 30
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
backend: ../knowledge/backend.md
|
||||
cqrs-es: ../knowledge/cqrs-es.md
|
||||
security: ../knowledge/security.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
cqrs-es-reviewer: ../personas/cqrs-es-reviewer.md
|
||||
frontend-reviewer: ../personas/frontend-reviewer.md
|
||||
security-reviewer: ../personas/security-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
expert-supervisor: ../personas/expert-supervisor.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
arbitrate: ../instructions/arbitrate.md
|
||||
review-cqrs-es: ../instructions/review-cqrs-es.md
|
||||
review-frontend: ../instructions/review-frontend.md
|
||||
review-security: ../instructions/review-security.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
fix: ../instructions/fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
cqrs-es-review: ../output-contracts/cqrs-es-review.md
|
||||
frontend-review: ../output-contracts/frontend-review.md
|
||||
security-review: ../output-contracts/security-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
initial_movement: plan
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
persona: planner
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: plan
|
||||
rules:
|
||||
- condition: Task analysis and planning is complete
|
||||
next: implement
|
||||
- condition: Requirements are unclear and planning cannot proceed
|
||||
next: ABORT
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: Implementation is complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: ai_review
|
||||
- condition: Cannot proceed with implementation
|
||||
next: ai_review
|
||||
- condition: User input required
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-review
|
||||
rules:
|
||||
- condition: No AI-specific issues found
|
||||
next: reviewers
|
||||
- condition: AI-specific issues detected
|
||||
next: ai_fix
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-fix
|
||||
rules:
|
||||
- condition: AI Reviewer's issues have been fixed
|
||||
next: ai_review
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
next: ai_no_fix
|
||||
- condition: Unable to proceed with fixes
|
||||
next: ai_no_fix
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_review's findings are valid (fix required)
|
||||
next: ai_fix
|
||||
- condition: ai_fix's judgment is valid (no fix needed)
|
||||
next: reviewers
|
||||
instruction: arbitrate
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: cqrs-es-review
|
||||
edit: false
|
||||
persona: cqrs-es-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- cqrs-es
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-cqrs-es
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-cqrs-es-review.md
|
||||
format: cqrs-es-review
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
policy: review
|
||||
knowledge: frontend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-frontend
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
- name: security-review
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
policy: review
|
||||
knowledge: security
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-security
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-security-review.md
|
||||
format: security-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 07-qa-review.md
|
||||
format: qa-review
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: plan
|
||||
instruction: fix
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: expert-supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: All validations pass and ready to merge
|
||||
next: COMPLETE
|
||||
- condition: Issues detected during final review
|
||||
next: fix_supervisor
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: Supervisor's issues have been fixed
|
||||
next: supervise
|
||||
- condition: Unable to proceed with fixes
|
||||
next: plan
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
@ -1,335 +0,0 @@
|
||||
# Auto-generated from expert.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: expert-hybrid-codex
|
||||
description: Architecture, Frontend, Security, QA Expert Review
|
||||
max_iterations: 30
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
backend: ../knowledge/backend.md
|
||||
security: ../knowledge/security.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
frontend-reviewer: ../personas/frontend-reviewer.md
|
||||
security-reviewer: ../personas/security-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
expert-supervisor: ../personas/expert-supervisor.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
arbitrate: ../instructions/arbitrate.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
review-frontend: ../instructions/review-frontend.md
|
||||
review-security: ../instructions/review-security.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
fix: ../instructions/fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
frontend-review: ../output-contracts/frontend-review.md
|
||||
security-review: ../output-contracts/security-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
initial_movement: plan
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
persona: planner
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: plan
|
||||
rules:
|
||||
- condition: Task analysis and planning is complete
|
||||
next: implement
|
||||
- condition: Requirements are unclear and planning cannot proceed
|
||||
next: ABORT
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: Implementation is complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: ai_review
|
||||
- condition: Cannot proceed with implementation
|
||||
next: ai_review
|
||||
- condition: User input required
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-review
|
||||
rules:
|
||||
- condition: No AI-specific issues found
|
||||
next: reviewers
|
||||
- condition: AI-specific issues detected
|
||||
next: ai_fix
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-fix
|
||||
rules:
|
||||
- condition: AI Reviewer's issues have been fixed
|
||||
next: ai_review
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
next: ai_no_fix
|
||||
- condition: Unable to proceed with fixes
|
||||
next: ai_no_fix
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_review's findings are valid (fix required)
|
||||
next: ai_fix
|
||||
- condition: ai_fix's judgment is valid (no fix needed)
|
||||
next: reviewers
|
||||
instruction: arbitrate
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-architect-review.md
|
||||
format: architecture-review
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
policy: review
|
||||
knowledge: frontend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-frontend
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
- name: security-review
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
policy: review
|
||||
knowledge: security
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-security
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-security-review.md
|
||||
format: security-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 07-qa-review.md
|
||||
format: qa-review
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: plan
|
||||
instruction: fix
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: expert-supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: All validations pass and ready to merge
|
||||
next: COMPLETE
|
||||
- condition: Issues detected during final review
|
||||
next: fix_supervisor
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: Supervisor's issues have been fixed
|
||||
next: supervise
|
||||
- condition: Unable to proceed with fixes
|
||||
next: plan
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
@ -1,202 +0,0 @@
|
||||
# Auto-generated from minimal.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: minimal-hybrid-codex
|
||||
description: Minimal development piece (implement -> parallel review -> fix if needed -> complete)
|
||||
max_iterations: 20
|
||||
personas:
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
implement: ../instructions/implement.md
|
||||
review-ai: ../instructions/review-ai.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
initial_movement: implement
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: Implementation complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: ABORT
|
||||
- condition: User input required because there are items to confirm with the user
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: review-ai
|
||||
rules:
|
||||
- condition: No AI-specific issues
|
||||
- condition: AI-specific issues found
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: All checks passed
|
||||
- condition: Requirements unmet, tests failing
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 05-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
rules:
|
||||
- condition: all("No AI-specific issues", "All checks passed")
|
||||
next: COMPLETE
|
||||
- condition: all("AI-specific issues found", "Requirements unmet, tests failing")
|
||||
next: fix_both
|
||||
- condition: any("AI-specific issues found")
|
||||
next: ai_fix
|
||||
- condition: any("Requirements unmet, tests failing")
|
||||
next: supervise_fix
|
||||
- name: fix_both
|
||||
parallel:
|
||||
- name: ai_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI Reviewer's issues fixed
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
- condition: Cannot proceed, insufficient info
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Supervisor's issues fixed
|
||||
- condition: Cannot proceed, insufficient info
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: all("AI Reviewer's issues fixed", "Supervisor's issues fixed")
|
||||
next: reviewers
|
||||
- condition: any("No fix needed (verified target files/spec)", "Cannot proceed, insufficient info")
|
||||
next: implement
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI Reviewer's issues fixed
|
||||
next: reviewers
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
next: implement
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: implement
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Supervisor's issues fixed
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: implement
|
||||
instruction: fix-supervisor
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
@ -1,44 +0,0 @@
|
||||
# Auto-generated from passthrough.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: passthrough-hybrid-codex
|
||||
description: Single-agent thin wrapper. Pass task directly to coder as-is.
|
||||
max_iterations: 10
|
||||
personas:
|
||||
coder: ../personas/coder.md
|
||||
initial_movement: execute
|
||||
movements:
|
||||
- name: execute
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Task complete
|
||||
next: COMPLETE
|
||||
- condition: Cannot proceed
|
||||
next: ABORT
|
||||
- condition: User input required
|
||||
next: execute
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
Do the task.
|
||||
output_contracts:
|
||||
report:
|
||||
- Summary: summary.md
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
testing: ../policies/testing.md
|
||||
@ -1,202 +0,0 @@
|
||||
# Auto-generated from review-fix-minimal.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: review-fix-minimal-hybrid-codex
|
||||
description: Review and fix piece for existing code (starts with review, no implementation)
|
||||
max_iterations: 20
|
||||
personas:
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
implement: ../instructions/implement.md
|
||||
review-ai: ../instructions/review-ai.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
initial_movement: reviewers
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: Implementation complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: ABORT
|
||||
- condition: User input required because there are items to confirm with the user
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: review-ai
|
||||
rules:
|
||||
- condition: No AI-specific issues
|
||||
- condition: AI-specific issues found
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: All checks passed
|
||||
- condition: Requirements unmet, tests failing
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 05-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
rules:
|
||||
- condition: all("No AI-specific issues", "All checks passed")
|
||||
next: COMPLETE
|
||||
- condition: all("AI-specific issues found", "Requirements unmet, tests failing")
|
||||
next: fix_both
|
||||
- condition: any("AI-specific issues found")
|
||||
next: ai_fix
|
||||
- condition: any("Requirements unmet, tests failing")
|
||||
next: supervise_fix
|
||||
- name: fix_both
|
||||
parallel:
|
||||
- name: ai_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI Reviewer's issues fixed
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
- condition: Cannot proceed, insufficient info
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Supervisor's issues fixed
|
||||
- condition: Cannot proceed, insufficient info
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: all("AI Reviewer's issues fixed", "Supervisor's issues fixed")
|
||||
next: reviewers
|
||||
- condition: any("No fix needed (verified target files/spec)", "Cannot proceed, insufficient info")
|
||||
next: implement
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI Reviewer's issues fixed
|
||||
next: reviewers
|
||||
- condition: No fix needed (verified target files/spec)
|
||||
next: implement
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: implement
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Supervisor's issues fixed
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: implement
|
||||
instruction: fix-supervisor
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
455
builtins/en/pieces/structural-reform.yaml
Normal file
455
builtins/en/pieces/structural-reform.yaml
Normal file
@ -0,0 +1,455 @@
|
||||
name: structural-reform
|
||||
description: Full project review and structural reform - iterative codebase restructuring with staged file splits
|
||||
max_iterations: 50
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
qa: ../policies/qa.md
|
||||
knowledge:
|
||||
backend: ../knowledge/backend.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
implement: ../instructions/implement.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
fix: ../instructions/fix.md
|
||||
initial_movement: review
|
||||
loop_monitors:
|
||||
- cycle:
|
||||
- implement
|
||||
- fix
|
||||
threshold: 3
|
||||
judge:
|
||||
persona: supervisor
|
||||
instruction_template: |
|
||||
The implement -> reviewers -> fix loop has repeated {cycle_count} times for the current reform target.
|
||||
|
||||
Review the reports from each cycle and determine whether this loop
|
||||
is making progress or repeating the same issues.
|
||||
|
||||
**Reports to reference:**
|
||||
- Architect review: {report:04-architect-review.md}
|
||||
- QA review: {report:05-qa-review.md}
|
||||
|
||||
**Judgment criteria:**
|
||||
- Are review findings being addressed in each fix cycle?
|
||||
- Are the same issues recurring without resolution?
|
||||
- Is the implementation converging toward approval?
|
||||
rules:
|
||||
- condition: Healthy (making progress toward approval)
|
||||
next: implement
|
||||
- condition: Unproductive (same issues recurring, no convergence)
|
||||
next: next_target
|
||||
movements:
|
||||
- name: review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Piece Status
|
||||
- Iteration: {iteration}/{max_iterations} (piece-wide)
|
||||
- Movement Iteration: {movement_iteration} (times this movement has run)
|
||||
- Movement: review (full project review)
|
||||
|
||||
## User Request
|
||||
{task}
|
||||
|
||||
## Instructions
|
||||
Conduct a comprehensive structural review of the entire project codebase.
|
||||
|
||||
**Focus areas:**
|
||||
1. **God Classes/Functions**: Files exceeding 300 lines, classes with multiple responsibilities
|
||||
2. **Coupling**: Circular dependencies, tight coupling between modules
|
||||
3. **Cohesion**: Low-cohesion modules mixing unrelated concerns
|
||||
4. **Testability**: Untestable code due to tight coupling or side effects
|
||||
5. **Layer violations**: Wrong dependency directions, domain logic in adapters
|
||||
6. **DRY violations**: Duplicated logic across 3+ locations
|
||||
|
||||
**For each issue found, report:**
|
||||
- File path and line count
|
||||
- Problem category (God Class, Low Cohesion, etc.)
|
||||
- Severity (Critical / High / Medium)
|
||||
- Specific responsibilities that should be separated
|
||||
- Dependencies that would be affected by splitting
|
||||
|
||||
**Output format:**
|
||||
|
||||
```markdown
|
||||
# Full Project Structural Review
|
||||
|
||||
## Summary
|
||||
- Total files reviewed: N
|
||||
- Issues found: N (Critical: N, High: N, Medium: N)
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. {File path} ({line count} lines)
|
||||
- **Problem**: {category}
|
||||
- **Severity**: Critical
|
||||
- **Responsibilities found**:
|
||||
1. {responsibility 1}
|
||||
2. {responsibility 2}
|
||||
- **Proposed split**:
|
||||
- `{new-file-1}.ts`: {responsibility}
|
||||
- `{new-file-2}.ts`: {responsibility}
|
||||
- **Affected dependents**: {files that import this module}
|
||||
|
||||
## High Priority Issues
|
||||
...
|
||||
|
||||
## Medium Priority Issues
|
||||
...
|
||||
|
||||
## Dependency Graph Concerns
|
||||
- {circular dependencies, layering violations}
|
||||
|
||||
## Recommended Reform Order
|
||||
1. {file} - {reason for priority}
|
||||
2. {file} - {reason for priority}
|
||||
```
|
||||
rules:
|
||||
- condition: Full review is complete with findings
|
||||
next: plan_reform
|
||||
- condition: No structural issues found
|
||||
next: COMPLETE
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-full-review.md
|
||||
|
||||
- name: plan_reform
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Piece Status
|
||||
- Iteration: {iteration}/{max_iterations} (piece-wide)
|
||||
- Movement Iteration: {movement_iteration} (times this movement has run)
|
||||
- Movement: plan_reform (reform plan creation)
|
||||
|
||||
## User Request
|
||||
{task}
|
||||
|
||||
## Full Review Results
|
||||
{previous_response}
|
||||
|
||||
## Additional User Inputs
|
||||
{user_inputs}
|
||||
|
||||
## Instructions
|
||||
Based on the full review results, create a concrete reform execution plan.
|
||||
|
||||
**Planning principles:**
|
||||
- One file split per iteration (keep changes manageable)
|
||||
- Order by dependency: split leaf nodes first, then work inward
|
||||
- Each split must leave tests and build passing
|
||||
- No backward compatibility concerns (per user instruction)
|
||||
|
||||
**For each reform target, specify:**
|
||||
1. Target file and current line count
|
||||
2. Proposed new files with responsibilities
|
||||
3. Expected changes to imports in dependent files
|
||||
4. Test strategy (new tests needed, existing tests to update)
|
||||
5. Risk assessment (what could break)
|
||||
|
||||
**Output format:**
|
||||
|
||||
```markdown
|
||||
# Structural Reform Plan
|
||||
|
||||
## Reform Targets (ordered by execution priority)
|
||||
|
||||
### Target 1: {file path}
|
||||
- **Current state**: {line count} lines, {N} responsibilities
|
||||
- **Proposed split**:
|
||||
| New file | Responsibility | Estimated lines |
|
||||
|----------|---------------|-----------------|
|
||||
| `{path}` | {responsibility} | ~{N} |
|
||||
- **Dependent files**: {list of files that import this}
|
||||
- **Test plan**: {what tests to add/update}
|
||||
- **Risk**: {Low/Medium/High} - {description}
|
||||
|
||||
### Target 2: {file path}
|
||||
...
|
||||
|
||||
## Execution Order Rationale
|
||||
{Why this order minimizes risk and dependency conflicts}
|
||||
|
||||
## Success Criteria
|
||||
- All tests pass after each split
|
||||
- Build succeeds after each split
|
||||
- No file exceeds 300 lines
|
||||
- Each file has single responsibility
|
||||
```
|
||||
rules:
|
||||
- condition: Reform plan is complete and ready to execute
|
||||
next: implement
|
||||
- condition: No actionable reforms identified
|
||||
next: COMPLETE
|
||||
- condition: Requirements unclear, need user input
|
||||
next: ABORT
|
||||
appendix: |
|
||||
Clarifications needed:
|
||||
- {Question 1}
|
||||
- {Question 2}
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 01-reform-plan.md
|
||||
format: plan
|
||||
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- backend
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: Implementation complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: reviewers
|
||||
- condition: User input required
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-architect-review.md
|
||||
format: architecture-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-qa-review.md
|
||||
format: qa-review
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: verify
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- backend
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: plan_reform
|
||||
instruction: fix
|
||||
|
||||
- name: verify
|
||||
edit: false
|
||||
persona: supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Piece Status
|
||||
- Iteration: {iteration}/{max_iterations} (piece-wide)
|
||||
- Movement Iteration: {movement_iteration} (times this movement has run)
|
||||
- Movement: verify (build and test verification)
|
||||
|
||||
## Instructions
|
||||
Verify that the current reform step has been completed successfully.
|
||||
|
||||
**Verification checklist:**
|
||||
1. **Build**: Run the build command and confirm it passes
|
||||
2. **Tests**: Run the test suite and confirm all tests pass
|
||||
3. **File sizes**: Confirm no new file exceeds 300 lines
|
||||
4. **Single responsibility**: Confirm each new file has a clear, single purpose
|
||||
5. **Import consistency**: Confirm all imports are updated correctly
|
||||
|
||||
**Report format:**
|
||||
|
||||
```markdown
|
||||
# Verification Results
|
||||
|
||||
## Result: PASS / FAIL
|
||||
|
||||
| Check | Status | Details |
|
||||
|-------|--------|---------|
|
||||
| Build | PASS/FAIL | {output summary} |
|
||||
| Tests | PASS/FAIL | {N passed, N failed} |
|
||||
| File sizes | PASS/FAIL | {any file > 300 lines} |
|
||||
| Single responsibility | PASS/FAIL | {assessment} |
|
||||
| Import consistency | PASS/FAIL | {any broken imports} |
|
||||
|
||||
## Issues (if FAIL)
|
||||
1. {issue description}
|
||||
```
|
||||
rules:
|
||||
- condition: All verifications passed
|
||||
next: next_target
|
||||
- condition: Verification failed
|
||||
next: fix
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-verification.md
|
||||
format: validation
|
||||
|
||||
- name: next_target
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## Piece Status
|
||||
- Iteration: {iteration}/{max_iterations} (piece-wide)
|
||||
- Movement Iteration: {movement_iteration} (times this movement has run)
|
||||
- Movement: next_target (progress check and next target selection)
|
||||
|
||||
## Original Reform Plan
|
||||
{report:01-reform-plan.md}
|
||||
|
||||
## Latest Verification
|
||||
{previous_response}
|
||||
|
||||
## Instructions
|
||||
Assess the progress of the structural reform and determine the next action.
|
||||
|
||||
**Steps:**
|
||||
1. Review the reform plan and identify which targets have been completed
|
||||
2. Check the current codebase state against the plan
|
||||
3. Determine if there are remaining reform targets
|
||||
|
||||
**Output format:**
|
||||
|
||||
```markdown
|
||||
# Reform Progress
|
||||
|
||||
## Completed Targets
|
||||
| # | Target | Status |
|
||||
|---|--------|--------|
|
||||
| 1 | {file} | Completed |
|
||||
| 2 | {file} | Completed |
|
||||
|
||||
## Remaining Targets
|
||||
| # | Target | Priority |
|
||||
|---|--------|----------|
|
||||
| 3 | {file} | Next |
|
||||
| 4 | {file} | Pending |
|
||||
|
||||
## Next Action
|
||||
- **Target**: {next file to reform}
|
||||
- **Plan**: {brief description of the split}
|
||||
|
||||
## Overall Progress
|
||||
{N}/{total} targets completed. Estimated remaining iterations: {N}
|
||||
```
|
||||
rules:
|
||||
- condition: More reform targets remain
|
||||
next: implement
|
||||
- condition: All reform targets completed
|
||||
next: COMPLETE
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 07-progress.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
@ -1,37 +1,32 @@
|
||||
# Auto-generated from default.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: default-hybrid-codex
|
||||
description: Standard development piece with planning and specialized reviews
|
||||
max_iterations: 30
|
||||
name: unit-test
|
||||
description: Unit test focused piece (test analysis → test implementation → review → fix)
|
||||
max_iterations: 20
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
knowledge:
|
||||
backend: ../knowledge/backend.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
backend: ../knowledge/backend.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
test-planner: ../personas/test-planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
plan-test: ../instructions/plan-test.md
|
||||
implement-test: ../instructions/implement-test.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
arbitrate: ../instructions/arbitrate.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
review-test: ../instructions/review-test.md
|
||||
fix: ../instructions/fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
initial_movement: plan
|
||||
initial_movement: plan_test
|
||||
loop_monitors:
|
||||
- cycle:
|
||||
- ai_review
|
||||
@ -56,12 +51,15 @@ loop_monitors:
|
||||
- condition: Healthy (making progress)
|
||||
next: ai_review
|
||||
- condition: Unproductive (no improvement)
|
||||
next: reviewers
|
||||
next: review_test
|
||||
movements:
|
||||
- name: plan
|
||||
- name: plan_test
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
persona: test-planner
|
||||
policy: testing
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
@ -70,9 +68,9 @@ movements:
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: Requirements are clear and implementable
|
||||
next: implement
|
||||
- condition: User is asking a question (not an implementation task)
|
||||
- condition: Test plan complete
|
||||
next: implement_test
|
||||
- condition: User is asking a question (not a test task)
|
||||
next: COMPLETE
|
||||
- condition: Requirements unclear, insufficient info
|
||||
next: ABORT
|
||||
@ -80,15 +78,15 @@ movements:
|
||||
Clarifications needed:
|
||||
- {Question 1}
|
||||
- {Question 2}
|
||||
instruction: plan
|
||||
instruction: plan-test
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
- name: 00-test-plan.md
|
||||
format: test-plan
|
||||
|
||||
- name: implement_test
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
@ -107,21 +105,22 @@ movements:
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Implementation complete
|
||||
- condition: Test implementation complete
|
||||
next: ai_review
|
||||
- condition: No implementation (report only)
|
||||
next: ai_review
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: ai_review
|
||||
- condition: User input required
|
||||
next: implement
|
||||
next: implement_test
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction: implement
|
||||
instruction: implement-test
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
@ -136,7 +135,7 @@ movements:
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: No AI-specific issues
|
||||
next: reviewers
|
||||
next: review_test
|
||||
- condition: AI-specific issues found
|
||||
next: ai_fix
|
||||
instruction: ai-review
|
||||
@ -144,10 +143,10 @@ movements:
|
||||
report:
|
||||
- name: 04-ai-review.md
|
||||
format: ai-review
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
@ -173,6 +172,7 @@ movements:
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: ai_no_fix
|
||||
instruction: ai-fix
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
@ -185,63 +185,39 @@ movements:
|
||||
- condition: ai_review's findings are valid (fix required)
|
||||
next: ai_fix
|
||||
- condition: ai_fix's judgment is valid (no fix needed)
|
||||
next: reviewers
|
||||
next: review_test
|
||||
instruction: arbitrate
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-architect-review.md
|
||||
format: architecture-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-qa-review.md
|
||||
format: qa-review
|
||||
|
||||
- name: review_test
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
- condition: approved
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
- condition: needs_fix
|
||||
next: fix
|
||||
instruction: review-test
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-qa-review.md
|
||||
format: qa-review
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- backend
|
||||
- architecture
|
||||
@ -257,10 +233,11 @@ movements:
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: Fix complete
|
||||
next: reviewers
|
||||
next: review_test
|
||||
- condition: Cannot proceed, insufficient info
|
||||
next: plan
|
||||
next: plan_test
|
||||
instruction: fix
|
||||
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: supervisor
|
||||
@ -276,15 +253,15 @@ movements:
|
||||
- condition: All checks passed
|
||||
next: COMPLETE
|
||||
- condition: Requirements unmet, tests failing, build errors
|
||||
next: plan
|
||||
next: plan_test
|
||||
instruction: supervise
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 07-supervisor-validation.md
|
||||
- Validation: 06-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
report_formats:
|
||||
test-plan: ../output-contracts/test-plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
@ -86,6 +86,18 @@ Every issue raised must include the following.
|
||||
Extract into a shared function."
|
||||
```
|
||||
|
||||
## Finding ID Tracking (`finding_id`)
|
||||
|
||||
To prevent circular rejections, track findings by ID.
|
||||
|
||||
- Every issue raised in a REJECT must include a `finding_id`
|
||||
- If the same issue is raised again, reuse the same `finding_id`
|
||||
- For repeated issues, set status to `persists` and include concrete evidence (file/line) that it remains unresolved
|
||||
- New issues must use status `new`
|
||||
- Resolved issues must be listed with status `resolved`
|
||||
- Issues without `finding_id` are invalid (cannot be used as rejection grounds)
|
||||
- REJECT is allowed only when there is at least one `new` or `persists` issue
|
||||
|
||||
## Boy Scout Rule
|
||||
|
||||
Leave it better than you found it.
|
||||
|
||||
@ -1,34 +1,85 @@
|
||||
# TAKT グローバル設定
|
||||
# takt のデフォルト設定ファイルです。
|
||||
# 配置場所: ~/.takt/config.yaml
|
||||
|
||||
# 言語設定 (en または ja)
|
||||
# ── 基本設定 ──
|
||||
|
||||
# 言語 (en | ja)
|
||||
language: ja
|
||||
|
||||
# デフォルトのピース - 指定がない場合に使用します
|
||||
# デフォルトピース(指定なし時に使用)
|
||||
default_piece: default
|
||||
|
||||
# ログレベル: debug, info, warn, error
|
||||
# ログレベル (debug | info | warn | error)
|
||||
log_level: info
|
||||
|
||||
# プロバイダー: claude または codex
|
||||
# ── プロバイダー & モデル ──
|
||||
|
||||
# プロバイダー (claude | codex)
|
||||
provider: claude
|
||||
|
||||
# ビルトインピースの読み込み (resources/global/{lang}/pieces)
|
||||
# enable_builtin_pieces: true
|
||||
|
||||
# デフォルトモデル (オプション)
|
||||
# Claude: opus, sonnet, haiku, opusplan, default, またはフルモデル名
|
||||
# デフォルトモデル(オプション)
|
||||
# Claude: opus, sonnet, haiku
|
||||
# Codex: gpt-5.2-codex, gpt-5.1-codex など
|
||||
# model: sonnet
|
||||
|
||||
# Anthropic APIキー (オプション、環境変数 TAKT_ANTHROPIC_API_KEY で上書き可能)
|
||||
# anthropic_api_key: ""
|
||||
# ペルソナ単位のプロバイダー上書き(オプション)
|
||||
# 特定ペルソナだけプロバイダーを変更。未指定のペルソナはグローバル設定を使用。
|
||||
# persona_providers:
|
||||
# coder: codex
|
||||
|
||||
# OpenAI APIキー (オプション、環境変数 TAKT_OPENAI_API_KEY で上書き可能)
|
||||
# ── APIキー ──
|
||||
# オプション。環境変数が優先:
|
||||
# TAKT_ANTHROPIC_API_KEY, TAKT_OPENAI_API_KEY
|
||||
|
||||
# anthropic_api_key: ""
|
||||
# openai_api_key: ""
|
||||
|
||||
# パイプライン実行設定 (オプション)
|
||||
# パイプラインモード (--task) のブランチ名、コミットメッセージ、PRの本文をカスタマイズできます。
|
||||
# ── 実行設定 ──
|
||||
|
||||
# ワークツリー(shared clone)ディレクトリ(デフォルト: プロジェクトの ../{clone-name})
|
||||
# worktree_dir: ~/takt-worktrees
|
||||
|
||||
# ワークツリー実行後に自動PR作成(デフォルト: 対話モードで確認)
|
||||
# auto_pr: false
|
||||
|
||||
# macOS のアイドルスリープを防止(デフォルト: false)
|
||||
# prevent_sleep: false
|
||||
|
||||
# ── 並列実行 (takt run) ──
|
||||
|
||||
# タスクの同時実行数(1 = 逐次実行、最大: 10)
|
||||
# concurrency: 1
|
||||
|
||||
# 新規タスクのポーリング間隔 ms(100-5000、デフォルト: 500)
|
||||
# task_poll_interval_ms: 500
|
||||
|
||||
# ── 対話モード ──
|
||||
|
||||
# ムーブメントプレビューの表示数(0 で無効、最大: 10)
|
||||
# interactive_preview_movements: 3
|
||||
|
||||
# ブランチ名の生成方式(romaji: 高速デフォルト | ai: 低速)
|
||||
# branch_name_strategy: romaji
|
||||
|
||||
# ── 出力 ──
|
||||
|
||||
# 通知音(デフォルト: true)
|
||||
# notification_sound: true
|
||||
|
||||
# CI 向け最小出力 - AI 出力を抑制(デフォルト: false)
|
||||
# minimal_output: false
|
||||
|
||||
# ── ビルトインピース ──
|
||||
|
||||
# ビルトインピースの有効化(デフォルト: true)
|
||||
# enable_builtin_pieces: true
|
||||
|
||||
# 特定のビルトインを除外
|
||||
# disabled_builtins:
|
||||
# - magi
|
||||
|
||||
# ── パイプラインモード (--pipeline) ──
|
||||
|
||||
# pipeline:
|
||||
# default_branch_prefix: "takt/"
|
||||
# commit_message_template: "feat: {title} (#{issue})"
|
||||
@ -37,10 +88,14 @@ provider: claude
|
||||
# {issue_body}
|
||||
# Closes #{issue}
|
||||
|
||||
# 通知音 (true: 有効 / false: 無効、デフォルト: true)
|
||||
# notification_sound: true
|
||||
# ── プリファレンス ──
|
||||
|
||||
# プリファレンスファイルのカスタムパス
|
||||
# bookmarks_file: ~/.takt/preferences/bookmarks.yaml
|
||||
# piece_categories_file: ~/.takt/preferences/piece-categories.yaml
|
||||
|
||||
# ── デバッグ ──
|
||||
|
||||
# デバッグ設定 (オプション)
|
||||
# debug:
|
||||
# enabled: false
|
||||
# log_file: ~/.takt/logs/debug.log
|
||||
|
||||
52
builtins/ja/instructions/implement-test.md
Normal file
52
builtins/ja/instructions/implement-test.md
Normal file
@ -0,0 +1,52 @@
|
||||
テスト計画に従って単体テストを実装してください。
|
||||
Piece Contextに示されたReport Directory内のファイルのみ参照してください。他のレポートディレクトリは検索/参照しないでください。
|
||||
|
||||
**重要: プロダクションコードは変更しないでください。テストファイルのみ編集可能です。**
|
||||
|
||||
**やること:**
|
||||
1. テスト計画のレポートを確認する
|
||||
2. 計画されたテストケースを実装する
|
||||
3. テストを実行して全パスを確認する
|
||||
4. 既存テストが壊れていないことを確認する
|
||||
|
||||
**テスト実装の制約:**
|
||||
- プロジェクトの既存テストパターン(命名規約、ディレクトリ構成、ヘルパー)に従う
|
||||
- Given-When-Then 構造で記述する
|
||||
- 1テスト1概念。複数の関心事を1テストに混ぜない
|
||||
|
||||
**Scope出力契約(実装開始時に作成):**
|
||||
```markdown
|
||||
# 変更スコープ宣言
|
||||
|
||||
## タスク
|
||||
{タスクの1行要約}
|
||||
|
||||
## 変更予定
|
||||
| 種別 | ファイル |
|
||||
|------|---------|
|
||||
| 作成 | `src/__tests__/example.test.ts` |
|
||||
|
||||
## 推定規模
|
||||
Small / Medium / Large
|
||||
|
||||
## 影響範囲
|
||||
- {影響するモジュールや機能}
|
||||
```
|
||||
|
||||
**Decisions出力契約(実装完了時、決定がある場合のみ):**
|
||||
```markdown
|
||||
# 決定ログ
|
||||
|
||||
## 1. {決定内容}
|
||||
- **背景**: {なぜ決定が必要だったか}
|
||||
- **検討した選択肢**: {選択肢リスト}
|
||||
- **理由**: {選んだ理由}
|
||||
```
|
||||
|
||||
**必須出力(見出しを含める)**
|
||||
## 作業結果
|
||||
- {実施内容の要約}
|
||||
## 変更内容
|
||||
- {変更内容の要約}
|
||||
## テスト結果
|
||||
- {実行コマンドと結果}
|
||||
11
builtins/ja/instructions/plan-test.md
Normal file
11
builtins/ja/instructions/plan-test.md
Normal file
@ -0,0 +1,11 @@
|
||||
対象コードを分析し、不足している単体テストを洗い出してください。
|
||||
|
||||
**注意:** Previous Responseがある場合は差し戻しのため、
|
||||
その内容を踏まえてテスト計画を見直してください。
|
||||
|
||||
**やること:**
|
||||
1. 対象モジュールのソースコードを読み、振る舞い・分岐・状態遷移を理解する
|
||||
2. 既存テストを読み、カバーされている観点を把握する
|
||||
3. 不足しているテストケース(正常系・異常系・境界値・エッジケース)を洗い出す
|
||||
4. テスト方針(モック戦略、既存テストヘルパーの活用、フィクスチャ設計)を決める
|
||||
5. テスト実装者向けの具体的なガイドラインを出す
|
||||
@ -9,8 +9,14 @@ AI特有の問題はレビューしないでください(ai_reviewムーブメ
|
||||
- デッドコード
|
||||
- 呼び出しチェーン検証
|
||||
|
||||
**前回指摘の追跡(必須):**
|
||||
- まず「Previous Response」から前回の open findings を抽出する
|
||||
- 各 finding に `finding_id` を付け、今回の状態を `new / persists / resolved` で判定する
|
||||
- `persists` と判定する場合は、未解決である根拠(ファイル/行)を必ず示す
|
||||
|
||||
## 判定手順
|
||||
|
||||
1. 変更差分を確認し、構造・設計の観点に基づいて問題を検出する
|
||||
2. 検出した問題ごとに、Policyのスコープ判定表と判定ルールに基づいてブロッキング/非ブロッキングを分類する
|
||||
3. ブロッキング問題が1件でもあればREJECTと判定する
|
||||
1. まず前回open findingsを抽出し、`new / persists / resolved` を仮判定する
|
||||
2. 変更差分を確認し、構造・設計の観点に基づいて問題を検出する
|
||||
3. 検出した問題ごとに、Policyのスコープ判定表と判定ルールに基づいてブロッキング/非ブロッキングを分類する
|
||||
4. ブロッキング問題(`new` または `persists`)が1件でもあればREJECTと判定する
|
||||
|
||||
@ -7,8 +7,14 @@
|
||||
- ログとモニタリング
|
||||
- 保守性
|
||||
|
||||
**前回指摘の追跡(必須):**
|
||||
- まず「Previous Response」から前回の open findings を抽出する
|
||||
- 各 finding に `finding_id` を付け、今回の状態を `new / persists / resolved` で判定する
|
||||
- `persists` と判定する場合は、未解決である根拠(ファイル/行)を必ず示す
|
||||
|
||||
## 判定手順
|
||||
|
||||
1. 変更差分を確認し、品質保証の観点に基づいて問題を検出する
|
||||
2. 検出した問題ごとに、Policyのスコープ判定表と判定ルールに基づいてブロッキング/非ブロッキングを分類する
|
||||
3. ブロッキング問題が1件でもあればREJECTと判定する
|
||||
1. まず前回open findingsを抽出し、`new / persists / resolved` を仮判定する
|
||||
2. 変更差分を確認し、品質保証の観点に基づいて問題を検出する
|
||||
3. 検出した問題ごとに、Policyのスコープ判定表と判定ルールに基づいてブロッキング/非ブロッキングを分類する
|
||||
4. ブロッキング問題(`new` または `persists`)が1件でもあればREJECTと判定する
|
||||
|
||||
14
builtins/ja/instructions/review-test.md
Normal file
14
builtins/ja/instructions/review-test.md
Normal file
@ -0,0 +1,14 @@
|
||||
テスト品質の観点から変更をレビューしてください。
|
||||
|
||||
**レビュー観点:**
|
||||
- テスト計画の観点がすべてカバーされているか
|
||||
- テスト品質(Given-When-Then構造、独立性、再現性)
|
||||
- テスト命名規約
|
||||
- 過不足(不要なテスト、足りないケース)
|
||||
- モック・フィクスチャの適切さ
|
||||
|
||||
## 判定手順
|
||||
|
||||
1. テスト計画レポート({report:00-test-plan.md})と実装されたテストを突合する
|
||||
2. 検出した問題ごとに、Policyのスコープ判定表と判定ルールに基づいてブロッキング/非ブロッキングを分類する
|
||||
3. ブロッキング問題が1件でもあればREJECTと判定する
|
||||
@ -14,10 +14,15 @@
|
||||
- [x] デッドコード
|
||||
- [x] 呼び出しチェーン検証
|
||||
|
||||
## 前回Open Findings
|
||||
| finding_id | 前回状態 | 今回状態(new/persists/resolved) | 根拠 |
|
||||
|------------|----------|----------------------------------|------|
|
||||
| ARCH-EXAMPLE-src-file-L42 | open | persists | `src/file.ts:42` |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | スコープ | 場所 | 問題 | 修正案 |
|
||||
|---|---------|------|------|--------|
|
||||
| 1 | スコープ内 | `src/file.ts:42` | 問題の説明 | 修正方法 |
|
||||
| # | finding_id | 状態(new/persists) | スコープ | 場所 | 問題 | 修正案 |
|
||||
|---|------------|--------------------|---------|------|------|--------|
|
||||
| 1 | ARCH-EXAMPLE-src-file-L42 | new | スコープ内 | `src/file.ts:42` | 問題の説明 | 修正方法 |
|
||||
|
||||
スコープ: 「スコープ内」(今回修正可能)/ 「スコープ外」(既存問題・非ブロッキング)
|
||||
|
||||
|
||||
@ -15,8 +15,13 @@
|
||||
| ドキュメント | ✅ | - |
|
||||
| 保守性 | ✅ | - |
|
||||
|
||||
## 前回Open Findings
|
||||
| finding_id | 前回状態 | 今回状態(new/persists/resolved) | 根拠 |
|
||||
|------------|----------|----------------------------------|------|
|
||||
| QA-EXAMPLE-src-file-L42 | open | persists | `src/file.ts:42` |
|
||||
|
||||
## 問題点(REJECTの場合)
|
||||
| # | カテゴリ | 問題 | 修正案 |
|
||||
|---|---------|------|--------|
|
||||
| 1 | テスト | 問題の説明 | 修正方法 |
|
||||
| # | finding_id | 状態(new/persists) | カテゴリ | 問題 | 修正案 |
|
||||
|---|------------|--------------------|---------|------|--------|
|
||||
| 1 | QA-EXAMPLE-src-file-L42 | new | テスト | 問題の説明 | 修正方法 |
|
||||
```
|
||||
|
||||
24
builtins/ja/output-contracts/test-plan.md
Normal file
24
builtins/ja/output-contracts/test-plan.md
Normal file
@ -0,0 +1,24 @@
|
||||
```markdown
|
||||
# テスト計画
|
||||
|
||||
## 対象モジュール
|
||||
{分析対象のモジュール一覧}
|
||||
|
||||
## 既存テストの分析
|
||||
| モジュール | 既存テスト | カバレッジ状況 |
|
||||
|-----------|-----------|--------------|
|
||||
| `src/xxx.ts` | `xxx.test.ts` | {カバー状況} |
|
||||
|
||||
## 不足テストケース
|
||||
| # | 対象 | テストケース | 優先度 | 理由 |
|
||||
|---|------|------------|--------|------|
|
||||
| 1 | `src/xxx.ts` | {テストケース概要} | 高/中/低 | {理由} |
|
||||
|
||||
## テスト方針
|
||||
- {モック戦略}
|
||||
- {フィクスチャ設計}
|
||||
- {テストヘルパー活用}
|
||||
|
||||
## 実装ガイドライン
|
||||
- {テスト実装者向けの具体的指示}
|
||||
```
|
||||
25
builtins/ja/personas/test-planner.md
Normal file
25
builtins/ja/personas/test-planner.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Test Planner
|
||||
|
||||
あなたはテスト分析と計画の専門家です。対象コードの振る舞いを理解し、既存テストのカバレッジを分析して、不足しているテストケースを体系的に洗い出す。
|
||||
|
||||
## 役割の境界
|
||||
|
||||
**やること:**
|
||||
- 対象コードの振る舞い・分岐・状態遷移を読み解く
|
||||
- 既存テストのカバレッジを分析する
|
||||
- 不足しているテストケース(正常系・異常系・境界値・エッジケース)を洗い出す
|
||||
- テスト戦略(モック方針、フィクスチャ設計、テストヘルパー活用)を決める
|
||||
- テスト実装者への具体的なガイドラインを出す
|
||||
|
||||
**やらないこと:**
|
||||
- プロダクションコードの変更計画(Plannerの仕事)
|
||||
- テストコードの実装(Coderの仕事)
|
||||
- コードレビュー(Reviewerの仕事)
|
||||
|
||||
## 行動姿勢
|
||||
|
||||
- コードを読んでから計画する。推測でテストケースを列挙しない
|
||||
- 既存テストを必ず確認する。カバー済みの観点を重複して計画しない
|
||||
- テスト優先度を付ける。ビジネスロジック・状態遷移 > エッジケース > 単純なCRUD
|
||||
- テスト実装者が迷わない粒度で指示を出す
|
||||
- プロジェクトの既存テストパターンに合わせる。独自の書き方を提案しない
|
||||
@ -10,26 +10,17 @@ piece_categories:
|
||||
pieces:
|
||||
- review-fix-minimal
|
||||
- review-only
|
||||
- unit-test
|
||||
🎨 フロントエンド: {}
|
||||
⚙️ バックエンド: {}
|
||||
🔧 フルスタック:
|
||||
pieces:
|
||||
- expert
|
||||
- expert-cqrs
|
||||
🔀 ハイブリッド (Codex Coding):
|
||||
🚀 クイックスタート:
|
||||
🔧 エキスパート:
|
||||
フルスタック:
|
||||
pieces:
|
||||
- coding-hybrid-codex
|
||||
- default-hybrid-codex
|
||||
- minimal-hybrid-codex
|
||||
- passthrough-hybrid-codex
|
||||
🔧 フルスタック:
|
||||
- expert
|
||||
- expert-cqrs
|
||||
リファクタリング:
|
||||
pieces:
|
||||
- expert-cqrs-hybrid-codex
|
||||
- expert-hybrid-codex
|
||||
🔍 レビュー&修正:
|
||||
pieces:
|
||||
- review-fix-minimal-hybrid-codex
|
||||
- structural-reform
|
||||
その他:
|
||||
pieces:
|
||||
- research
|
||||
|
||||
@ -1,158 +0,0 @@
|
||||
# Auto-generated from coding.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: coding-hybrid-codex
|
||||
description: Lightweight development piece with planning and parallel reviews (plan -> implement -> parallel review -> complete)
|
||||
max_iterations: 20
|
||||
knowledge:
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
fix: ../instructions/fix.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
initial_movement: plan
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: 要件が明確で実装可能
|
||||
next: implement
|
||||
- condition: ユーザーが質問をしている(実装タスクではない)
|
||||
next: COMPLETE
|
||||
- condition: 要件が不明確、情報不足
|
||||
next: ABORT
|
||||
instruction: plan
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 実装完了
|
||||
next: reviewers
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: reviewers
|
||||
- condition: 判断できない、情報不足
|
||||
next: reviewers
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction: implement
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: AI特有の問題なし
|
||||
- condition: AI特有の問題あり
|
||||
instruction: ai-review
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-ai-review.md
|
||||
format: ai-review
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-architect-review.md
|
||||
format: architecture-review
|
||||
rules:
|
||||
- condition: all("AI特有の問題なし", "approved")
|
||||
next: COMPLETE
|
||||
- condition: any("AI特有の問題あり", "needs_fix")
|
||||
next: fix
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正完了
|
||||
next: reviewers
|
||||
- condition: 判断できない、情報不足
|
||||
next: ABORT
|
||||
instruction: fix
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
@ -1,341 +0,0 @@
|
||||
# Auto-generated from expert-cqrs.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: expert-cqrs-hybrid-codex
|
||||
description: CQRS+ES・フロントエンド・セキュリティ・QA専門家レビュー
|
||||
max_iterations: 30
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
backend: ../knowledge/backend.md
|
||||
cqrs-es: ../knowledge/cqrs-es.md
|
||||
security: ../knowledge/security.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
cqrs-es-reviewer: ../personas/cqrs-es-reviewer.md
|
||||
frontend-reviewer: ../personas/frontend-reviewer.md
|
||||
security-reviewer: ../personas/security-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
expert-supervisor: ../personas/expert-supervisor.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
arbitrate: ../instructions/arbitrate.md
|
||||
review-cqrs-es: ../instructions/review-cqrs-es.md
|
||||
review-frontend: ../instructions/review-frontend.md
|
||||
review-security: ../instructions/review-security.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
fix: ../instructions/fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
cqrs-es-review: ../output-contracts/cqrs-es-review.md
|
||||
frontend-review: ../output-contracts/frontend-review.md
|
||||
security-review: ../output-contracts/security-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
initial_movement: plan
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
persona: planner
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: plan
|
||||
rules:
|
||||
- condition: タスク分析と計画が完了した
|
||||
next: implement
|
||||
- condition: 要件が不明確で計画を立てられない
|
||||
next: ABORT
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: 実装が完了した
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: ai_review
|
||||
- condition: 実装を進行できない
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-review
|
||||
rules:
|
||||
- condition: AI特有の問題が見つからない
|
||||
next: reviewers
|
||||
- condition: AI特有の問題が検出された
|
||||
next: ai_fix
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-fix
|
||||
rules:
|
||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||
next: ai_review
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: ai_no_fix
|
||||
- condition: 修正を進行できない
|
||||
next: ai_no_fix
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_reviewの指摘が妥当(修正すべき)
|
||||
next: ai_fix
|
||||
- condition: ai_fixの判断が妥当(修正不要)
|
||||
next: reviewers
|
||||
instruction: arbitrate
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: cqrs-es-review
|
||||
edit: false
|
||||
persona: cqrs-es-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- cqrs-es
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-cqrs-es
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-cqrs-es-review.md
|
||||
format: cqrs-es-review
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
policy: review
|
||||
knowledge: frontend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-frontend
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
- name: security-review
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
policy: review
|
||||
knowledge: security
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-security
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-security-review.md
|
||||
format: security-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 07-qa-review.md
|
||||
format: qa-review
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
instruction: fix
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: expert-supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: すべての検証が完了し、マージ可能な状態である
|
||||
next: COMPLETE
|
||||
- condition: 問題が検出された
|
||||
next: fix_supervisor
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- cqrs-es
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: supervise
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
@ -1,335 +0,0 @@
|
||||
# Auto-generated from expert.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: expert-hybrid-codex
|
||||
description: アーキテクチャ・フロントエンド・セキュリティ・QA専門家レビュー
|
||||
max_iterations: 30
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
backend: ../knowledge/backend.md
|
||||
security: ../knowledge/security.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
frontend-reviewer: ../personas/frontend-reviewer.md
|
||||
security-reviewer: ../personas/security-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
expert-supervisor: ../personas/expert-supervisor.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
arbitrate: ../instructions/arbitrate.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
review-frontend: ../instructions/review-frontend.md
|
||||
review-security: ../instructions/review-security.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
fix: ../instructions/fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
frontend-review: ../output-contracts/frontend-review.md
|
||||
security-review: ../output-contracts/security-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
initial_movement: plan
|
||||
movements:
|
||||
- name: plan
|
||||
edit: false
|
||||
persona: planner
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: plan
|
||||
rules:
|
||||
- condition: タスク分析と計画が完了した
|
||||
next: implement
|
||||
- condition: 要件が不明確で計画を立てられない
|
||||
next: ABORT
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: 実装が完了した
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: ai_review
|
||||
- condition: 実装を進行できない
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-review
|
||||
rules:
|
||||
- condition: AI特有の問題が見つからない
|
||||
next: reviewers
|
||||
- condition: AI特有の問題が検出された
|
||||
next: ai_fix
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: ai-fix
|
||||
rules:
|
||||
- condition: AI Reviewerの指摘に対する修正が完了した
|
||||
next: ai_review
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: ai_no_fix
|
||||
- condition: 修正を進行できない
|
||||
next: ai_no_fix
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
rules:
|
||||
- condition: ai_reviewの指摘が妥当(修正すべき)
|
||||
next: ai_fix
|
||||
- condition: ai_fixの判断が妥当(修正不要)
|
||||
next: reviewers
|
||||
instruction: arbitrate
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-architect-review.md
|
||||
format: architecture-review
|
||||
- name: frontend-review
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
policy: review
|
||||
knowledge: frontend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-frontend
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
- name: security-review
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
policy: review
|
||||
knowledge: security
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-security
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-security-review.md
|
||||
format: security-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 07-qa-review.md
|
||||
format: qa-review
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
instruction: fix
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: expert-supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: すべての検証が完了し、マージ可能な状態である
|
||||
next: COMPLETE
|
||||
- condition: 問題が検出された
|
||||
next: fix_supervisor
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 08-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
- name: fix_supervisor
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- frontend
|
||||
- backend
|
||||
- security
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: supervise
|
||||
- condition: 修正を進行できない
|
||||
next: plan
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
@ -1,202 +0,0 @@
|
||||
# Auto-generated from minimal.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: minimal-hybrid-codex
|
||||
description: Minimal development piece (implement -> parallel review -> fix if needed -> complete)
|
||||
max_iterations: 20
|
||||
personas:
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
implement: ../instructions/implement.md
|
||||
review-ai: ../instructions/review-ai.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
initial_movement: implement
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: 実装が完了した
|
||||
next: reviewers
|
||||
- condition: 実装を進行できない
|
||||
next: ABORT
|
||||
- condition: ユーザーへの確認事項があるためユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: review-ai
|
||||
rules:
|
||||
- condition: AI特有の問題なし
|
||||
- condition: AI特有の問題あり
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: すべて問題なし
|
||||
- condition: 要求未達成、テスト失敗、ビルドエラー
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 05-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
rules:
|
||||
- condition: all("AI特有の問題なし", "すべて問題なし")
|
||||
next: COMPLETE
|
||||
- condition: all("AI特有の問題あり", "要求未達成、テスト失敗、ビルドエラー")
|
||||
next: fix_both
|
||||
- condition: any("AI特有の問題あり")
|
||||
next: ai_fix
|
||||
- condition: any("要求未達成、テスト失敗、ビルドエラー")
|
||||
next: supervise_fix
|
||||
- name: fix_both
|
||||
parallel:
|
||||
- name: ai_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
- condition: 判断できない、情報不足
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
- condition: 修正を進行できない
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: all("AI問題の修正完了", "監督者の指摘に対する修正が完了した")
|
||||
next: reviewers
|
||||
- condition: any("修正不要(指摘対象ファイル/仕様の確認済み)", "判断できない、情報不足", "修正を進行できない")
|
||||
next: implement
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
next: reviewers
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: implement
|
||||
- condition: 判断できない、情報不足
|
||||
next: implement
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: implement
|
||||
instruction: fix-supervisor
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
@ -1,44 +0,0 @@
|
||||
# Auto-generated from passthrough.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: passthrough-hybrid-codex
|
||||
description: Single-agent thin wrapper. Pass task directly to coder as-is.
|
||||
max_iterations: 10
|
||||
personas:
|
||||
coder: ../personas/coder.md
|
||||
initial_movement: execute
|
||||
movements:
|
||||
- name: execute
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: タスク完了
|
||||
next: COMPLETE
|
||||
- condition: 進行できない
|
||||
next: ABORT
|
||||
- condition: ユーザー入力が必要
|
||||
next: execute
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction_template: |
|
||||
タスクをこなしてください。
|
||||
output_contracts:
|
||||
report:
|
||||
- Summary: summary.md
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
testing: ../policies/testing.md
|
||||
@ -1,202 +0,0 @@
|
||||
# Auto-generated from review-fix-minimal.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: review-fix-minimal-hybrid-codex
|
||||
description: 既存コードのレビューと修正ピース(レビュー開始、実装なし)
|
||||
max_iterations: 20
|
||||
personas:
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
implement: ../instructions/implement.md
|
||||
review-ai: ../instructions/review-ai.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
fix-supervisor: ../instructions/fix-supervisor.md
|
||||
report_formats:
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
initial_movement: reviewers
|
||||
movements:
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: 実装が完了した
|
||||
next: reviewers
|
||||
- condition: 実装を進行できない
|
||||
next: ABORT
|
||||
- condition: ユーザーへの確認事項があるためユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 01-coder-scope.md
|
||||
- Decisions: 02-coder-decisions.md
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
policy:
|
||||
- review
|
||||
- ai-antipattern
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: review-ai
|
||||
rules:
|
||||
- condition: AI特有の問題なし
|
||||
- condition: AI特有の問題あり
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 03-ai-review.md
|
||||
format: ai-review
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction: supervise
|
||||
rules:
|
||||
- condition: すべて問題なし
|
||||
- condition: 要求未達成、テスト失敗、ビルドエラー
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 05-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
rules:
|
||||
- condition: all("AI特有の問題なし", "すべて問題なし")
|
||||
next: COMPLETE
|
||||
- condition: all("AI特有の問題あり", "要求未達成、テスト失敗、ビルドエラー")
|
||||
next: fix_both
|
||||
- condition: any("AI特有の問題あり")
|
||||
next: ai_fix
|
||||
- condition: any("要求未達成、テスト失敗、ビルドエラー")
|
||||
next: supervise_fix
|
||||
- name: fix_both
|
||||
parallel:
|
||||
- name: ai_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
- condition: 判断できない、情報不足
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix_parallel
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
- condition: 修正を進行できない
|
||||
instruction: fix-supervisor
|
||||
rules:
|
||||
- condition: all("AI問題の修正完了", "監督者の指摘に対する修正が完了した")
|
||||
next: reviewers
|
||||
- condition: any("修正不要(指摘対象ファイル/仕様の確認済み)", "判断できない、情報不足", "修正を進行できない")
|
||||
next: implement
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: AI問題の修正完了
|
||||
next: reviewers
|
||||
- condition: 修正不要(指摘対象ファイル/仕様の確認済み)
|
||||
next: implement
|
||||
- condition: 判断できない、情報不足
|
||||
next: implement
|
||||
instruction: ai-fix
|
||||
- name: supervise_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 監督者の指摘に対する修正が完了した
|
||||
next: reviewers
|
||||
- condition: 修正を進行できない
|
||||
next: implement
|
||||
instruction: fix-supervisor
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
455
builtins/ja/pieces/structural-reform.yaml
Normal file
455
builtins/ja/pieces/structural-reform.yaml
Normal file
@ -0,0 +1,455 @@
|
||||
name: structural-reform
|
||||
description: プロジェクト全体レビューと構造改革 - 段階的なファイル分割による反復的コードベース再構築
|
||||
max_iterations: 50
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
qa: ../policies/qa.md
|
||||
knowledge:
|
||||
backend: ../knowledge/backend.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
implement: ../instructions/implement.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
fix: ../instructions/fix.md
|
||||
initial_movement: review
|
||||
loop_monitors:
|
||||
- cycle:
|
||||
- implement
|
||||
- fix
|
||||
threshold: 3
|
||||
judge:
|
||||
persona: supervisor
|
||||
instruction_template: |
|
||||
implement → reviewers → fix のループが現在の改革ターゲットに対して {cycle_count} 回繰り返されました。
|
||||
|
||||
各サイクルのレポートを確認し、このループが進捗しているか、
|
||||
同じ問題を繰り返しているかを判断してください。
|
||||
|
||||
**参照するレポート:**
|
||||
- アーキテクチャレビュー: {report:04-architect-review.md}
|
||||
- QAレビュー: {report:05-qa-review.md}
|
||||
|
||||
**判断基準:**
|
||||
- 各修正サイクルでレビュー指摘が解消されているか
|
||||
- 同じ問題が解決されずに繰り返されていないか
|
||||
- 実装が承認に向かって収束しているか
|
||||
rules:
|
||||
- condition: 健全(承認に向けて進捗あり)
|
||||
next: implement
|
||||
- condition: 非生産的(同じ問題が繰り返され、収束なし)
|
||||
next: next_target
|
||||
movements:
|
||||
- name: review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## ピースステータス
|
||||
- イテレーション: {iteration}/{max_iterations}(ピース全体)
|
||||
- ムーブメントイテレーション: {movement_iteration}(このムーブメントの実行回数)
|
||||
- ムーブメント: review(プロジェクト全体レビュー)
|
||||
|
||||
## ユーザーリクエスト
|
||||
{task}
|
||||
|
||||
## 指示
|
||||
プロジェクトのコードベース全体に対して、包括的な構造レビューを実施してください。
|
||||
|
||||
**重点領域:**
|
||||
1. **God Class/Function**: 300行を超えるファイル、複数の責務を持つクラス
|
||||
2. **結合度**: 循環依存、モジュール間の密結合
|
||||
3. **凝集度**: 無関係な関心事が混在した低凝集モジュール
|
||||
4. **テスタビリティ**: 密結合や副作用によるテスト困難なコード
|
||||
5. **レイヤー違反**: 依存方向の誤り、アダプター層にドメインロジック
|
||||
6. **DRY違反**: 3箇所以上の重複ロジック
|
||||
|
||||
**発見した問題ごとに報告:**
|
||||
- ファイルパスと行数
|
||||
- 問題カテゴリ(God Class、低凝集など)
|
||||
- 深刻度(Critical / High / Medium)
|
||||
- 分離すべき具体的な責務
|
||||
- 分割により影響を受ける依存先
|
||||
|
||||
**出力フォーマット:**
|
||||
|
||||
```markdown
|
||||
# プロジェクト全体構造レビュー
|
||||
|
||||
## サマリー
|
||||
- レビュー対象ファイル数: N
|
||||
- 検出された問題: N(Critical: N, High: N, Medium: N)
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. {ファイルパス}({行数}行)
|
||||
- **問題**: {カテゴリ}
|
||||
- **深刻度**: Critical
|
||||
- **検出された責務**:
|
||||
1. {責務1}
|
||||
2. {責務2}
|
||||
- **分割提案**:
|
||||
- `{新ファイル1}.ts`: {責務}
|
||||
- `{新ファイル2}.ts`: {責務}
|
||||
- **影響を受ける依存先**: {このモジュールをインポートしているファイル}
|
||||
|
||||
## High Priority Issues
|
||||
...
|
||||
|
||||
## Medium Priority Issues
|
||||
...
|
||||
|
||||
## 依存グラフの懸念事項
|
||||
- {循環依存、レイヤー違反}
|
||||
|
||||
## 推奨改革順序
|
||||
1. {ファイル} - {優先理由}
|
||||
2. {ファイル} - {優先理由}
|
||||
```
|
||||
rules:
|
||||
- condition: 全体レビューが完了し、問題が検出された
|
||||
next: plan_reform
|
||||
- condition: 構造的な問題は見つからなかった
|
||||
next: COMPLETE
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-full-review.md
|
||||
|
||||
- name: plan_reform
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## ピースステータス
|
||||
- イテレーション: {iteration}/{max_iterations}(ピース全体)
|
||||
- ムーブメントイテレーション: {movement_iteration}(このムーブメントの実行回数)
|
||||
- ムーブメント: plan_reform(改革計画策定)
|
||||
|
||||
## ユーザーリクエスト
|
||||
{task}
|
||||
|
||||
## 全体レビュー結果
|
||||
{previous_response}
|
||||
|
||||
## ユーザー追加入力
|
||||
{user_inputs}
|
||||
|
||||
## 指示
|
||||
全体レビュー結果を基に、具体的な改革実行計画を策定してください。
|
||||
|
||||
**計画の原則:**
|
||||
- 1イテレーションにつき1ファイルの分割(変更を管理可能に保つ)
|
||||
- 依存順に実行:まずリーフノードを分割し、内側に向かって進む
|
||||
- 各分割後にテストとビルドが通ること
|
||||
- 後方互換は不要(ユーザー指示通り)
|
||||
|
||||
**各改革ターゲットについて指定:**
|
||||
1. 対象ファイルと現在の行数
|
||||
2. 提案する新ファイルと責務
|
||||
3. 依存先ファイルのインポート変更予定
|
||||
4. テスト戦略(新規テスト、既存テストの更新)
|
||||
5. リスク評価(何が壊れる可能性があるか)
|
||||
|
||||
**出力フォーマット:**
|
||||
|
||||
```markdown
|
||||
# 構造改革計画
|
||||
|
||||
## 改革ターゲット(実行優先順)
|
||||
|
||||
### ターゲット1: {ファイルパス}
|
||||
- **現状**: {行数}行、{N}個の責務
|
||||
- **分割提案**:
|
||||
| 新ファイル | 責務 | 推定行数 |
|
||||
|----------|------|---------|
|
||||
| `{パス}` | {責務} | ~{N} |
|
||||
- **依存先ファイル**: {このモジュールをインポートしているファイル一覧}
|
||||
- **テスト計画**: {追加・更新すべきテスト}
|
||||
- **リスク**: {Low/Medium/High} - {説明}
|
||||
|
||||
### ターゲット2: {ファイルパス}
|
||||
...
|
||||
|
||||
## 実行順序の根拠
|
||||
{この順序がリスクと依存関係の競合を最小化する理由}
|
||||
|
||||
## 成功基準
|
||||
- 各分割後に全テストが通る
|
||||
- 各分割後にビルドが成功する
|
||||
- 300行を超えるファイルがない
|
||||
- 各ファイルが単一責務を持つ
|
||||
```
|
||||
rules:
|
||||
- condition: 改革計画が完成し、実行可能な状態
|
||||
next: implement
|
||||
- condition: 実行可能な改革が特定されなかった
|
||||
next: COMPLETE
|
||||
- condition: 要件が不明確、ユーザー入力が必要
|
||||
next: ABORT
|
||||
appendix: |
|
||||
確認事項:
|
||||
- {質問1}
|
||||
- {質問2}
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 01-reform-plan.md
|
||||
format: plan
|
||||
|
||||
- name: implement
|
||||
edit: true
|
||||
persona: coder
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- backend
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
instruction: implement
|
||||
rules:
|
||||
- condition: 実装完了
|
||||
next: reviewers
|
||||
- condition: 判断できない、情報不足
|
||||
next: reviewers
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 04-architect-review.md
|
||||
format: architecture-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-qa-review.md
|
||||
format: qa-review
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
next: verify
|
||||
- condition: any("needs_fix")
|
||||
next: fix
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
knowledge:
|
||||
- backend
|
||||
- architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Edit
|
||||
- Write
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正完了
|
||||
next: reviewers
|
||||
- condition: 判断できない、情報不足
|
||||
next: plan_reform
|
||||
instruction: fix
|
||||
|
||||
- name: verify
|
||||
edit: false
|
||||
persona: supervisor
|
||||
policy: review
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## ピースステータス
|
||||
- イテレーション: {iteration}/{max_iterations}(ピース全体)
|
||||
- ムーブメントイテレーション: {movement_iteration}(このムーブメントの実行回数)
|
||||
- ムーブメント: verify(ビルド・テスト検証)
|
||||
|
||||
## 指示
|
||||
現在の改革ステップが正常に完了したことを検証してください。
|
||||
|
||||
**検証チェックリスト:**
|
||||
1. **ビルド**: ビルドコマンドを実行し、成功することを確認
|
||||
2. **テスト**: テストスイートを実行し、全テストが通ることを確認
|
||||
3. **ファイルサイズ**: 新しいファイルが300行を超えていないことを確認
|
||||
4. **単一責務**: 各新ファイルが明確な単一の目的を持つことを確認
|
||||
5. **インポート整合性**: すべてのインポートが正しく更新されていることを確認
|
||||
|
||||
**レポートフォーマット:**
|
||||
|
||||
```markdown
|
||||
# 検証結果
|
||||
|
||||
## 結果: PASS / FAIL
|
||||
|
||||
| チェック項目 | 状態 | 詳細 |
|
||||
|------------|------|------|
|
||||
| ビルド | PASS/FAIL | {出力サマリー} |
|
||||
| テスト | PASS/FAIL | {N passed, N failed} |
|
||||
| ファイルサイズ | PASS/FAIL | {300行超のファイルの有無} |
|
||||
| 単一責務 | PASS/FAIL | {評価} |
|
||||
| インポート整合性 | PASS/FAIL | {壊れたインポートの有無} |
|
||||
|
||||
## 問題点(FAILの場合)
|
||||
1. {問題の説明}
|
||||
```
|
||||
rules:
|
||||
- condition: すべての検証に合格
|
||||
next: next_target
|
||||
- condition: 検証に失敗
|
||||
next: fix
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-verification.md
|
||||
format: validation
|
||||
|
||||
- name: next_target
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
instruction_template: |
|
||||
## ピースステータス
|
||||
- イテレーション: {iteration}/{max_iterations}(ピース全体)
|
||||
- ムーブメントイテレーション: {movement_iteration}(このムーブメントの実行回数)
|
||||
- ムーブメント: next_target(進捗確認と次ターゲット選択)
|
||||
|
||||
## 改革計画
|
||||
{report:01-reform-plan.md}
|
||||
|
||||
## 最新の検証結果
|
||||
{previous_response}
|
||||
|
||||
## 指示
|
||||
構造改革の進捗を評価し、次のアクションを決定してください。
|
||||
|
||||
**手順:**
|
||||
1. 改革計画を確認し、完了したターゲットを特定
|
||||
2. 現在のコードベースの状態を計画と照合
|
||||
3. 残りの改革ターゲットがあるか判断
|
||||
|
||||
**出力フォーマット:**
|
||||
|
||||
```markdown
|
||||
# 改革進捗
|
||||
|
||||
## 完了ターゲット
|
||||
| # | ターゲット | 状態 |
|
||||
|---|----------|------|
|
||||
| 1 | {ファイル} | 完了 |
|
||||
| 2 | {ファイル} | 完了 |
|
||||
|
||||
## 残りターゲット
|
||||
| # | ターゲット | 優先度 |
|
||||
|---|----------|-------|
|
||||
| 3 | {ファイル} | 次 |
|
||||
| 4 | {ファイル} | 保留 |
|
||||
|
||||
## 次のアクション
|
||||
- **ターゲット**: {次に改革するファイル}
|
||||
- **計画**: {分割の概要}
|
||||
|
||||
## 全体進捗
|
||||
{N}/{合計}ターゲット完了。残りの推定イテレーション数: {N}
|
||||
```
|
||||
rules:
|
||||
- condition: まだ改革ターゲットが残っている
|
||||
next: implement
|
||||
- condition: すべての改革ターゲットが完了
|
||||
next: COMPLETE
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 07-progress.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
@ -1,37 +1,32 @@
|
||||
# Auto-generated from default.yaml by tools/generate-hybrid-codex.mjs
|
||||
# Do not edit manually. Edit the source piece and re-run the generator.
|
||||
|
||||
name: default-hybrid-codex
|
||||
description: Standard development piece with planning and specialized reviews
|
||||
max_iterations: 30
|
||||
name: unit-test
|
||||
description: 単体テスト追加に特化したピース(テスト分析→テスト実装→レビュー→修正)
|
||||
max_iterations: 20
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
knowledge:
|
||||
architecture: ../knowledge/architecture.md
|
||||
backend: ../knowledge/backend.md
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
test-planner: ../personas/test-planner.md
|
||||
coder: ../personas/coder.md
|
||||
ai-antipattern-reviewer: ../personas/ai-antipattern-reviewer.md
|
||||
architecture-reviewer: ../personas/architecture-reviewer.md
|
||||
qa-reviewer: ../personas/qa-reviewer.md
|
||||
supervisor: ../personas/supervisor.md
|
||||
instructions:
|
||||
plan: ../instructions/plan.md
|
||||
implement: ../instructions/implement.md
|
||||
plan-test: ../instructions/plan-test.md
|
||||
implement-test: ../instructions/implement-test.md
|
||||
ai-review: ../instructions/ai-review.md
|
||||
ai-fix: ../instructions/ai-fix.md
|
||||
arbitrate: ../instructions/arbitrate.md
|
||||
review-arch: ../instructions/review-arch.md
|
||||
review-qa: ../instructions/review-qa.md
|
||||
review-test: ../instructions/review-test.md
|
||||
fix: ../instructions/fix.md
|
||||
supervise: ../instructions/supervise.md
|
||||
report_formats:
|
||||
plan: ../output-contracts/plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
architecture-review: ../output-contracts/architecture-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
initial_movement: plan
|
||||
initial_movement: plan_test
|
||||
loop_monitors:
|
||||
- cycle:
|
||||
- ai_review
|
||||
@ -56,12 +51,15 @@ loop_monitors:
|
||||
- condition: 健全(進捗あり)
|
||||
next: ai_review
|
||||
- condition: 非生産的(改善なし)
|
||||
next: reviewers
|
||||
next: review_test
|
||||
movements:
|
||||
- name: plan
|
||||
- name: plan_test
|
||||
edit: false
|
||||
persona: planner
|
||||
knowledge: architecture
|
||||
persona: test-planner
|
||||
policy: testing
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
@ -70,9 +68,9 @@ movements:
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: 要件が明確で実装可能
|
||||
next: implement
|
||||
- condition: ユーザーが質問をしている(実装タスクではない)
|
||||
- condition: テスト計画が完了
|
||||
next: implement_test
|
||||
- condition: ユーザーが質問をしている(テスト追加タスクではない)
|
||||
next: COMPLETE
|
||||
- condition: 要件が不明確、情報不足
|
||||
next: ABORT
|
||||
@ -80,15 +78,15 @@ movements:
|
||||
確認事項:
|
||||
- {質問1}
|
||||
- {質問2}
|
||||
instruction: plan
|
||||
instruction: plan-test
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 00-plan.md
|
||||
format: plan
|
||||
- name: implement
|
||||
- name: 00-test-plan.md
|
||||
format: test-plan
|
||||
|
||||
- name: implement_test
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
@ -107,21 +105,22 @@ movements:
|
||||
- WebFetch
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 実装完了
|
||||
- condition: テスト実装完了
|
||||
next: ai_review
|
||||
- condition: 実装未着手(レポートのみ)
|
||||
next: ai_review
|
||||
- condition: 判断できない、情報不足
|
||||
next: ai_review
|
||||
- condition: ユーザー入力が必要
|
||||
next: implement
|
||||
next: implement_test
|
||||
requires_user_input: true
|
||||
interactive_only: true
|
||||
instruction: implement
|
||||
instruction: implement-test
|
||||
output_contracts:
|
||||
report:
|
||||
- Scope: 02-coder-scope.md
|
||||
- Decisions: 03-coder-decisions.md
|
||||
|
||||
- name: ai_review
|
||||
edit: false
|
||||
persona: ai-antipattern-reviewer
|
||||
@ -136,7 +135,7 @@ movements:
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: AI特有の問題なし
|
||||
next: reviewers
|
||||
next: review_test
|
||||
- condition: AI特有の問題あり
|
||||
next: ai_fix
|
||||
instruction: ai-review
|
||||
@ -144,10 +143,10 @@ movements:
|
||||
report:
|
||||
- name: 04-ai-review.md
|
||||
format: ai-review
|
||||
|
||||
- name: ai_fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
@ -173,6 +172,7 @@ movements:
|
||||
- condition: 判断できない、情報不足
|
||||
next: ai_no_fix
|
||||
instruction: ai-fix
|
||||
|
||||
- name: ai_no_fix
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
@ -185,63 +185,39 @@ movements:
|
||||
- condition: ai_reviewの指摘が妥当(修正すべき)
|
||||
next: ai_fix
|
||||
- condition: ai_fixの判断が妥当(修正不要)
|
||||
next: reviewers
|
||||
next: review_test
|
||||
instruction: arbitrate
|
||||
- name: reviewers
|
||||
parallel:
|
||||
- name: arch-review
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
policy: review
|
||||
knowledge:
|
||||
- architecture
|
||||
- backend
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-arch
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-architect-review.md
|
||||
format: architecture-review
|
||||
- name: qa-review
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: approved
|
||||
- condition: needs_fix
|
||||
instruction: review-qa
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 06-qa-review.md
|
||||
format: qa-review
|
||||
|
||||
- name: review_test
|
||||
edit: false
|
||||
persona: qa-reviewer
|
||||
policy:
|
||||
- review
|
||||
- qa
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
- Grep
|
||||
- WebSearch
|
||||
- WebFetch
|
||||
rules:
|
||||
- condition: all("approved")
|
||||
- condition: approved
|
||||
next: supervise
|
||||
- condition: any("needs_fix")
|
||||
- condition: needs_fix
|
||||
next: fix
|
||||
instruction: review-test
|
||||
output_contracts:
|
||||
report:
|
||||
- name: 05-qa-review.md
|
||||
format: qa-review
|
||||
|
||||
- name: fix
|
||||
edit: true
|
||||
persona: coder
|
||||
provider: codex
|
||||
policy:
|
||||
- coding
|
||||
- testing
|
||||
session: refresh
|
||||
knowledge:
|
||||
- backend
|
||||
- architecture
|
||||
@ -257,10 +233,11 @@ movements:
|
||||
permission_mode: edit
|
||||
rules:
|
||||
- condition: 修正完了
|
||||
next: reviewers
|
||||
next: review_test
|
||||
- condition: 判断できない、情報不足
|
||||
next: plan
|
||||
next: plan_test
|
||||
instruction: fix
|
||||
|
||||
- name: supervise
|
||||
edit: false
|
||||
persona: supervisor
|
||||
@ -276,15 +253,15 @@ movements:
|
||||
- condition: すべて問題なし
|
||||
next: COMPLETE
|
||||
- condition: 要求未達成、テスト失敗、ビルドエラー
|
||||
next: plan
|
||||
next: plan_test
|
||||
instruction: supervise
|
||||
output_contracts:
|
||||
report:
|
||||
- Validation: 07-supervisor-validation.md
|
||||
- Validation: 06-supervisor-validation.md
|
||||
- Summary: summary.md
|
||||
policies:
|
||||
coding: ../policies/coding.md
|
||||
review: ../policies/review.md
|
||||
testing: ../policies/testing.md
|
||||
ai-antipattern: ../policies/ai-antipattern.md
|
||||
qa: ../policies/qa.md
|
||||
report_formats:
|
||||
test-plan: ../output-contracts/test-plan.md
|
||||
ai-review: ../output-contracts/ai-review.md
|
||||
qa-review: ../output-contracts/qa-review.md
|
||||
validation: ../output-contracts/validation.md
|
||||
summary: ../output-contracts/summary.md
|
||||
@ -86,6 +86,18 @@
|
||||
共通関数に抽出してください」
|
||||
```
|
||||
|
||||
## 指摘ID管理(finding_id)
|
||||
|
||||
同じ指摘の堂々巡りを防ぐため、指摘をIDで追跡する。
|
||||
|
||||
- REJECT時に挙げる各問題には `finding_id` を必須で付ける
|
||||
- 同じ問題を再指摘する場合は、同じ `finding_id` を再利用する
|
||||
- 再指摘時は状態を `persists` とし、未解決である根拠(ファイル/行)を必ず示す
|
||||
- 新規指摘は状態 `new` とする
|
||||
- 解消済みは状態 `resolved` として一覧化する
|
||||
- `finding_id` のない指摘は無効(判定根拠として扱わない)
|
||||
- REJECTは `new` または `persists` の問題が1件以上ある場合のみ許可する
|
||||
|
||||
## ボーイスカウトルール
|
||||
|
||||
来たときよりも美しく。
|
||||
|
||||
@ -76,6 +76,10 @@ takt --pipeline --task "バグを修正して" --auto-pr
|
||||
|
||||
## 使い方
|
||||
|
||||
## 実装メモ
|
||||
|
||||
- failed タスクの retry とセッション再開: [`docs/implements/retry-and-session.ja.md`](./implements/retry-and-session.ja.md)
|
||||
|
||||
### 対話モード
|
||||
|
||||
AI との会話でタスク内容を詰めてから実行するモード。タスクの要件が曖昧な場合や、AI と相談しながら内容を整理したい場合に便利です。
|
||||
@ -88,13 +92,25 @@ takt
|
||||
takt hello
|
||||
```
|
||||
|
||||
**注意:** Issue 参照(`#6`)や `--task` / `--issue` オプションを指定すると、対話モードをスキップして直接タスク実行されます。それ以外の入力(スペースを含む文字列を含む)はすべて対話モードに入ります。
|
||||
**注意:** `--task` オプションを指定すると対話モードをスキップして直接タスク実行されます。Issue 参照(`#6`、`--issue`)は対話モードの初期入力として使用されます。
|
||||
|
||||
**フロー:**
|
||||
1. ピース選択
|
||||
2. AI との会話でタスク内容を整理
|
||||
3. `/go` でタスク指示を確定(`/go 追加の指示` のように指示を追加することも可能)、または `/play <タスク>` で即座に実行
|
||||
4. 実行(worktree 作成、ピース実行、PR 作成)
|
||||
2. 対話モード選択(assistant / persona / quiet / passthrough)
|
||||
3. AI との会話でタスク内容を整理
|
||||
4. `/go` でタスク指示を確定(`/go 追加の指示` のように指示を追加することも可能)、または `/play <タスク>` で即座に実行
|
||||
5. 実行(worktree 作成、ピース実行、PR 作成)
|
||||
|
||||
#### 対話モードの種類
|
||||
|
||||
| モード | 説明 |
|
||||
|--------|------|
|
||||
| `assistant` | デフォルト。AI が質問を通じてタスク要件を明確にしてから指示を生成。 |
|
||||
| `persona` | 最初のムーブメントのペルソナとの会話(ペルソナのシステムプロンプトとツールを使用)。 |
|
||||
| `quiet` | 質問なしでタスク指示を生成(ベストエフォート)。 |
|
||||
| `passthrough` | ユーザー入力をそのままタスクテキストとして使用。AI 処理なし。 |
|
||||
|
||||
ピースの `interactive_mode` フィールドでデフォルトモードを設定可能。
|
||||
|
||||
#### 実行例
|
||||
|
||||
@ -447,8 +463,10 @@ TAKTには複数のビルトインピースが同梱されています:
|
||||
| `passthrough` | 最小構成。タスクをそのまま coder に渡す薄いラッパー。レビューなし。 |
|
||||
| `compound-eye` | マルチモデルレビュー: Claude と Codex に同じ指示を同時送信し、両方の回答を統合。 |
|
||||
| `review-only` | 変更を加えない読み取り専用のコードレビューピース。 |
|
||||
| `structural-reform` | プロジェクト全体の構造改革: 段階的なファイル分割を伴う反復的なコードベース再構成。 |
|
||||
| `unit-test` | ユニットテスト重視ピース: テスト分析 → テスト実装 → レビュー → 修正。 |
|
||||
|
||||
**Hybrid Codex バリアント** (`*-hybrid-codex`): 主要ピースごとに、coder エージェントを Codex で実行しレビュアーは Claude を使うハイブリッド構成が用意されています。対象: default, minimal, expert, expert-cqrs, passthrough, review-fix-minimal, coding。
|
||||
**ペルソナ別プロバイダー設定:** 設定ファイルの `persona_providers` で、特定のペルソナを異なるプロバイダーにルーティングできます(例: coder は Codex、レビュアーは Claude)。ピースを複製する必要はありません。
|
||||
|
||||
`takt switch` でピースを切り替えられます。
|
||||
|
||||
@ -471,6 +489,7 @@ TAKTには複数のビルトインピースが同梱されています:
|
||||
| **research-planner** | リサーチタスクの計画・スコープ定義 |
|
||||
| **research-digger** | 深掘り調査と情報収集 |
|
||||
| **research-supervisor** | リサーチ品質の検証と網羅性の評価 |
|
||||
| **test-planner** | テスト戦略分析と包括的なテスト計画 |
|
||||
| **pr-commenter** | レビュー結果を GitHub PR にコメントとして投稿 |
|
||||
|
||||
## カスタムペルソナ
|
||||
@ -539,8 +558,15 @@ branch_name_strategy: romaji # ブランチ名生成: 'romaji'(高速)ま
|
||||
prevent_sleep: false # macOS の実行中スリープ防止(caffeinate)
|
||||
notification_sound: true # 通知音の有効/無効
|
||||
concurrency: 1 # takt run の並列タスク数(1-10、デフォルト: 1 = 逐次実行)
|
||||
task_poll_interval_ms: 500 # takt run 中の新タスク検出ポーリング間隔(100-5000、デフォルト: 500)
|
||||
interactive_preview_movements: 3 # 対話モードでのムーブメントプレビュー数(0-10、デフォルト: 3)
|
||||
|
||||
# ペルソナ別プロバイダー設定(オプション)
|
||||
# ピースを複製せずに特定のペルソナを異なるプロバイダーにルーティング
|
||||
# persona_providers:
|
||||
# coder: codex # coder を Codex で実行
|
||||
# ai-antipattern-reviewer: claude # レビュアーは Claude のまま
|
||||
|
||||
# API Key 設定(オプション)
|
||||
# 環境変数 TAKT_ANTHROPIC_API_KEY / TAKT_OPENAI_API_KEY で上書き可能
|
||||
anthropic_api_key: sk-ant-... # Claude (Anthropic) を使う場合
|
||||
|
||||
58
docs/implements/retry-and-session.ja.md
Normal file
58
docs/implements/retry-and-session.ja.md
Normal file
@ -0,0 +1,58 @@
|
||||
# failed -> retry とセッション再開の動作原理
|
||||
|
||||
このドキュメントは、`takt list` の `failed` タスクを `Retry` したときの挙動と、AI セッション再開の実際の仕組みを整理した実装メモです。
|
||||
|
||||
## 問題の要約
|
||||
|
||||
- `failed -> retry` で「前回のセッションがそのまま復活するか」が分かりづらい。
|
||||
- 実際には「タスク固有のセッション再開」ではなく「保存済みの persona セッション利用」で再開される。
|
||||
|
||||
## 解決方針(実装の見方)
|
||||
|
||||
- `Retry` が何を再投入するかを確認する。
|
||||
- 再実行時にどのセッションIDを読み込むかを確認する。
|
||||
- `session: refresh` や worktree 条件など、再開しないケースを明記する。
|
||||
|
||||
## 実装の流れ
|
||||
|
||||
1. `takt list` で `failed` タスクに `Retry` を選ぶ。
|
||||
2. `retryFailedTask()` が `TaskRunner.requeueFailedTask()` を呼ぶ。
|
||||
3. 失敗タスク配下の元タスクファイルを `.takt/tasks/` にコピーする。
|
||||
4. YAML タスクの場合のみ、必要に応じて `start_movement` と `retry_note` を書き換える。
|
||||
|
||||
参照:
|
||||
- `src/features/tasks/list/taskRetryActions.ts`
|
||||
- `src/infra/task/runner.ts`
|
||||
|
||||
## 重要: Retry で復元される情報
|
||||
|
||||
`requeueFailedTask()` はタスクファイルに `sessionId` を書き戻さない。
|
||||
復元対象は `start_movement` と `retry_note` のみ。
|
||||
|
||||
つまり、`failed` タスクを `Retry` しても「その failed ディレクトリに紐づくセッションID」を直接復元する実装にはなっていない。
|
||||
|
||||
## セッション再開の実際
|
||||
|
||||
再実行時の `pieceExecution` で、プロジェクト保存済みセッションをロードして `initialSessions` に渡す。
|
||||
|
||||
- 通常実行: `loadPersonaSessions(projectCwd, provider)`
|
||||
- worktree 実行: `loadWorktreeSessions(projectCwd, cwd, provider)`
|
||||
|
||||
その後、各 movement の Phase 1 実行で `OptionsBuilder.buildAgentOptions()` が `sessionId` を組み立てる。
|
||||
|
||||
参照:
|
||||
- `src/features/tasks/execute/pieceExecution.ts`
|
||||
- `src/core/piece/engine/OptionsBuilder.ts`
|
||||
|
||||
## セッション再開しない条件
|
||||
|
||||
- movement 側が `session: refresh` の場合。
|
||||
- `cwd !== projectCwd` など、セッション再開を抑止する条件に当たる場合。
|
||||
|
||||
このため、`Retry` で常に同じ会話が厳密再開されるわけではない。
|
||||
|
||||
## 運用上の注意
|
||||
|
||||
- `failed -> retry` は正しい復旧手順。
|
||||
- ただしセッションは「タスク単位」ではなく「persona の保存状態」基準で再開される。
|
||||
- 前回文脈を確実に残したい場合は、`retry_note` に再試行理由と前提を明記する。
|
||||
@ -11,6 +11,11 @@ export interface TestRepo {
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
export interface CreateTestRepoOptions {
|
||||
/** Skip creating a test branch (stay on default branch). Use for pipeline tests. */
|
||||
skipBranch?: boolean;
|
||||
}
|
||||
|
||||
function getGitHubUser(): string {
|
||||
const user = execFileSync('gh', ['api', 'user', '--jq', '.login'], {
|
||||
encoding: 'utf-8',
|
||||
@ -33,7 +38,7 @@ function getGitHubUser(): string {
|
||||
* 2. Close any PRs created during the test
|
||||
* 3. Delete local directory
|
||||
*/
|
||||
export function createTestRepo(): TestRepo {
|
||||
export function createTestRepo(options?: CreateTestRepoOptions): TestRepo {
|
||||
const user = getGitHubUser();
|
||||
const repoName = `${user}/takt-testing`;
|
||||
|
||||
@ -56,49 +61,80 @@ export function createTestRepo(): TestRepo {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
// Create test branch
|
||||
const testBranch = `e2e-test-${Date.now()}`;
|
||||
execFileSync('git', ['checkout', '-b', testBranch], {
|
||||
cwd: repoPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
// Create test branch (unless skipped for pipeline tests)
|
||||
const testBranch = options?.skipBranch
|
||||
? undefined
|
||||
: `e2e-test-${Date.now()}`;
|
||||
if (testBranch) {
|
||||
execFileSync('git', ['checkout', '-b', testBranch], {
|
||||
cwd: repoPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
}
|
||||
|
||||
const currentBranch = testBranch
|
||||
?? execFileSync('git', ['branch', '--show-current'], {
|
||||
cwd: repoPath,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
|
||||
return {
|
||||
path: repoPath,
|
||||
repoName,
|
||||
branch: testBranch,
|
||||
branch: currentBranch,
|
||||
cleanup: () => {
|
||||
// 1. Delete remote branch (best-effort)
|
||||
try {
|
||||
execFileSync(
|
||||
'git',
|
||||
['push', 'origin', '--delete', testBranch],
|
||||
{ cwd: repoPath, stdio: 'pipe' },
|
||||
);
|
||||
} catch {
|
||||
// Branch may not have been pushed; ignore
|
||||
}
|
||||
|
||||
// 2. Close any PRs from this branch (best-effort)
|
||||
try {
|
||||
const prList = execFileSync(
|
||||
'gh',
|
||||
['pr', 'list', '--head', testBranch, '--repo', repoName, '--json', 'number', '--jq', '.[].number'],
|
||||
{ encoding: 'utf-8', stdio: 'pipe' },
|
||||
).trim();
|
||||
|
||||
for (const prNumber of prList.split('\n').filter(Boolean)) {
|
||||
if (testBranch) {
|
||||
// 1. Delete remote branch (best-effort)
|
||||
try {
|
||||
execFileSync(
|
||||
'gh',
|
||||
['pr', 'close', prNumber, '--repo', repoName, '--delete-branch'],
|
||||
{ stdio: 'pipe' },
|
||||
'git',
|
||||
['push', 'origin', '--delete', testBranch],
|
||||
{ cwd: repoPath, stdio: 'pipe' },
|
||||
);
|
||||
} catch {
|
||||
// Branch may not have been pushed; ignore
|
||||
}
|
||||
|
||||
// 2. Close any PRs from this branch (best-effort)
|
||||
try {
|
||||
const prList = execFileSync(
|
||||
'gh',
|
||||
['pr', 'list', '--head', testBranch, '--repo', repoName, '--json', 'number', '--jq', '.[].number'],
|
||||
{ encoding: 'utf-8', stdio: 'pipe' },
|
||||
).trim();
|
||||
|
||||
for (const prNumber of prList.split('\n').filter(Boolean)) {
|
||||
execFileSync(
|
||||
'gh',
|
||||
['pr', 'close', prNumber, '--repo', repoName, '--delete-branch'],
|
||||
{ stdio: 'pipe' },
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// No PRs or already closed; ignore
|
||||
}
|
||||
} else {
|
||||
// Pipeline mode: clean up takt-created PRs (best-effort)
|
||||
try {
|
||||
const prNumbers = execFileSync(
|
||||
'gh',
|
||||
['pr', 'list', '--state', 'open', '--repo', repoName, '--json', 'number', '--jq', '.[].number'],
|
||||
{ encoding: 'utf-8', stdio: 'pipe' },
|
||||
).trim();
|
||||
|
||||
for (const prNumber of prNumbers.split('\n').filter(Boolean)) {
|
||||
execFileSync(
|
||||
'gh',
|
||||
['pr', 'close', prNumber, '--repo', repoName, '--delete-branch'],
|
||||
{ stdio: 'pipe' },
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} catch {
|
||||
// No PRs or already closed; ignore
|
||||
}
|
||||
|
||||
// 3. Delete local directory last
|
||||
// Delete local directory last
|
||||
try {
|
||||
rmSync(repoPath, { recursive: true, force: true });
|
||||
} catch {
|
||||
|
||||
@ -17,7 +17,7 @@ describe('E2E: GitHub Issue processing', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
testRepo = createTestRepo();
|
||||
testRepo = createTestRepo({ skipBranch: true });
|
||||
|
||||
// Create a test issue
|
||||
const createOutput = execFileSync(
|
||||
|
||||
@ -16,7 +16,7 @@ describe('E2E: Pipeline mode (--pipeline --auto-pr)', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
isolatedEnv = createIsolatedEnv();
|
||||
testRepo = createTestRepo();
|
||||
testRepo = createTestRepo({ skipBranch: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "takt",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "takt",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.37",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "takt",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"description": "TAKT: Task Agent Koordination Tool - AI Agent Piece Orchestration",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@ -45,6 +45,11 @@ vi.mock('../features/pipeline/index.js', () => ({
|
||||
|
||||
vi.mock('../features/interactive/index.js', () => ({
|
||||
interactiveMode: vi.fn(),
|
||||
selectInteractiveMode: vi.fn(() => 'assistant'),
|
||||
passthroughMode: vi.fn(),
|
||||
quietMode: vi.fn(),
|
||||
personaMode: vi.fn(),
|
||||
resolveLanguage: vi.fn(() => 'en'),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/index.js', () => ({
|
||||
|
||||
@ -160,4 +160,71 @@ describe('PieceEngine Integration: Parallel Movement Aggregation', () => {
|
||||
expect(calledAgents).toContain('../personas/arch-review.md');
|
||||
expect(calledAgents).toContain('../personas/security-review.md');
|
||||
});
|
||||
|
||||
it('should output rich parallel prefix when taskPrefix/taskColorIndex are provided', async () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
||||
const parentOnStream = vi.fn();
|
||||
|
||||
const responsesByPersona = new Map<string, ReturnType<typeof makeResponse>>([
|
||||
['../personas/plan.md', makeResponse({ persona: 'plan', content: 'Plan done' })],
|
||||
['../personas/implement.md', makeResponse({ persona: 'implement', content: 'Impl done' })],
|
||||
['../personas/ai_review.md', makeResponse({ persona: 'ai_review', content: 'OK' })],
|
||||
['../personas/arch-review.md', makeResponse({ persona: 'arch-review', content: 'Architecture review content' })],
|
||||
['../personas/security-review.md', makeResponse({ persona: 'security-review', content: 'Security review content' })],
|
||||
['../personas/supervise.md', makeResponse({ persona: 'supervise', content: 'All passed' })],
|
||||
]);
|
||||
|
||||
vi.mocked(runAgent).mockImplementation(async (persona, _task, options) => {
|
||||
const response = responsesByPersona.get(persona ?? '');
|
||||
if (!response) {
|
||||
throw new Error(`Unexpected persona: ${persona}`);
|
||||
}
|
||||
|
||||
if (persona === '../personas/arch-review.md') {
|
||||
options.onStream?.({ type: 'text', data: { text: 'arch stream line\n' } });
|
||||
}
|
||||
if (persona === '../personas/security-review.md') {
|
||||
options.onStream?.({ type: 'text', data: { text: 'security stream line\n' } });
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'aggregate' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
]);
|
||||
|
||||
const engine = new PieceEngine(config, tmpDir, 'test task', {
|
||||
projectCwd: tmpDir,
|
||||
onStream: parentOnStream,
|
||||
taskPrefix: 'override-persona-provider',
|
||||
taskColorIndex: 0,
|
||||
});
|
||||
|
||||
try {
|
||||
const state = await engine.run();
|
||||
expect(state.status).toBe('completed');
|
||||
|
||||
const output = stdoutSpy.mock.calls.map((call) => String(call[0])).join('');
|
||||
expect(output).toContain('[over]');
|
||||
expect(output).toContain('[reviewers][arch-review](4/30)(1) arch stream line');
|
||||
expect(output).toContain('[reviewers][security-review](4/30)(1) security stream line');
|
||||
} finally {
|
||||
stdoutSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail fast when taskPrefix is provided without taskColorIndex', () => {
|
||||
const config = buildDefaultPieceConfig();
|
||||
expect(
|
||||
() => new PieceEngine(config, tmpDir, 'test task', { projectCwd: tmpDir, taskPrefix: 'override-persona-provider' })
|
||||
).toThrow('taskPrefix and taskColorIndex must be provided together');
|
||||
});
|
||||
});
|
||||
|
||||
204
src/__tests__/engine-persona-providers.test.ts
Normal file
204
src/__tests__/engine-persona-providers.test.ts
Normal file
@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Tests for persona_providers config-level provider override.
|
||||
*
|
||||
* Verifies the provider resolution priority:
|
||||
* 1. Movement YAML provider (highest)
|
||||
* 2. persona_providers[personaDisplayName]
|
||||
* 3. CLI/global provider (lowest)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../agents/runner.js', () => ({
|
||||
runAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../core/piece/evaluation/index.js', () => ({
|
||||
detectMatchedRule: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../core/piece/phase-runner.js', () => ({
|
||||
needsStatusJudgmentPhase: vi.fn(),
|
||||
runReportPhase: vi.fn(),
|
||||
runStatusJudgmentPhase: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
generateReportDir: vi.fn().mockReturnValue('test-report-dir'),
|
||||
}));
|
||||
|
||||
import { PieceEngine } from '../core/piece/index.js';
|
||||
import { runAgent } from '../agents/runner.js';
|
||||
import type { PieceConfig } from '../core/models/index.js';
|
||||
import {
|
||||
makeResponse,
|
||||
makeRule,
|
||||
makeMovement,
|
||||
mockRunAgentSequence,
|
||||
mockDetectMatchedRuleSequence,
|
||||
applyDefaultMocks,
|
||||
} from './engine-test-helpers.js';
|
||||
|
||||
describe('PieceEngine persona_providers override', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
applyDefaultMocks();
|
||||
});
|
||||
|
||||
it('should use persona_providers when movement has no provider and persona matches', async () => {
|
||||
const movement = makeMovement('implement', {
|
||||
personaDisplayName: 'coder',
|
||||
rules: [makeRule('done', 'COMPLETE')],
|
||||
});
|
||||
const config: PieceConfig = {
|
||||
name: 'persona-provider-test',
|
||||
movements: [movement],
|
||||
initialMovement: 'implement',
|
||||
maxIterations: 1,
|
||||
};
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ persona: movement.persona, content: 'done' }),
|
||||
]);
|
||||
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||
|
||||
const engine = new PieceEngine(config, '/tmp/project', 'test task', {
|
||||
projectCwd: '/tmp/project',
|
||||
provider: 'claude',
|
||||
personaProviders: { coder: 'codex' },
|
||||
});
|
||||
|
||||
await engine.run();
|
||||
|
||||
const options = vi.mocked(runAgent).mock.calls[0][2];
|
||||
expect(options.provider).toBe('codex');
|
||||
});
|
||||
|
||||
it('should use global provider when persona is not in persona_providers', async () => {
|
||||
const movement = makeMovement('plan', {
|
||||
personaDisplayName: 'planner',
|
||||
rules: [makeRule('done', 'COMPLETE')],
|
||||
});
|
||||
const config: PieceConfig = {
|
||||
name: 'persona-provider-nomatch',
|
||||
movements: [movement],
|
||||
initialMovement: 'plan',
|
||||
maxIterations: 1,
|
||||
};
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ persona: movement.persona, content: 'done' }),
|
||||
]);
|
||||
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||
|
||||
const engine = new PieceEngine(config, '/tmp/project', 'test task', {
|
||||
projectCwd: '/tmp/project',
|
||||
provider: 'claude',
|
||||
personaProviders: { coder: 'codex' },
|
||||
});
|
||||
|
||||
await engine.run();
|
||||
|
||||
const options = vi.mocked(runAgent).mock.calls[0][2];
|
||||
expect(options.provider).toBe('claude');
|
||||
});
|
||||
|
||||
it('should prioritize movement provider over persona_providers', async () => {
|
||||
const movement = makeMovement('implement', {
|
||||
personaDisplayName: 'coder',
|
||||
provider: 'claude',
|
||||
rules: [makeRule('done', 'COMPLETE')],
|
||||
});
|
||||
const config: PieceConfig = {
|
||||
name: 'movement-over-persona',
|
||||
movements: [movement],
|
||||
initialMovement: 'implement',
|
||||
maxIterations: 1,
|
||||
};
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ persona: movement.persona, content: 'done' }),
|
||||
]);
|
||||
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||
|
||||
const engine = new PieceEngine(config, '/tmp/project', 'test task', {
|
||||
projectCwd: '/tmp/project',
|
||||
provider: 'mock',
|
||||
personaProviders: { coder: 'codex' },
|
||||
});
|
||||
|
||||
await engine.run();
|
||||
|
||||
const options = vi.mocked(runAgent).mock.calls[0][2];
|
||||
expect(options.provider).toBe('claude');
|
||||
});
|
||||
|
||||
it('should work without persona_providers (undefined)', async () => {
|
||||
const movement = makeMovement('plan', {
|
||||
personaDisplayName: 'planner',
|
||||
rules: [makeRule('done', 'COMPLETE')],
|
||||
});
|
||||
const config: PieceConfig = {
|
||||
name: 'no-persona-providers',
|
||||
movements: [movement],
|
||||
initialMovement: 'plan',
|
||||
maxIterations: 1,
|
||||
};
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ persona: movement.persona, content: 'done' }),
|
||||
]);
|
||||
mockDetectMatchedRuleSequence([{ index: 0, method: 'phase1_tag' }]);
|
||||
|
||||
const engine = new PieceEngine(config, '/tmp/project', 'test task', {
|
||||
projectCwd: '/tmp/project',
|
||||
provider: 'claude',
|
||||
});
|
||||
|
||||
await engine.run();
|
||||
|
||||
const options = vi.mocked(runAgent).mock.calls[0][2];
|
||||
expect(options.provider).toBe('claude');
|
||||
});
|
||||
|
||||
it('should apply different providers to different personas in a multi-movement piece', async () => {
|
||||
const planMovement = makeMovement('plan', {
|
||||
personaDisplayName: 'planner',
|
||||
rules: [makeRule('done', 'implement')],
|
||||
});
|
||||
const implementMovement = makeMovement('implement', {
|
||||
personaDisplayName: 'coder',
|
||||
rules: [makeRule('done', 'COMPLETE')],
|
||||
});
|
||||
const config: PieceConfig = {
|
||||
name: 'multi-persona-providers',
|
||||
movements: [planMovement, implementMovement],
|
||||
initialMovement: 'plan',
|
||||
maxIterations: 3,
|
||||
};
|
||||
|
||||
mockRunAgentSequence([
|
||||
makeResponse({ persona: planMovement.persona, content: 'done' }),
|
||||
makeResponse({ persona: implementMovement.persona, content: 'done' }),
|
||||
]);
|
||||
mockDetectMatchedRuleSequence([
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
{ index: 0, method: 'phase1_tag' },
|
||||
]);
|
||||
|
||||
const engine = new PieceEngine(config, '/tmp/project', 'test task', {
|
||||
projectCwd: '/tmp/project',
|
||||
provider: 'claude',
|
||||
personaProviders: { coder: 'codex' },
|
||||
});
|
||||
|
||||
await engine.run();
|
||||
|
||||
const calls = vi.mocked(runAgent).mock.calls;
|
||||
// Plan movement: planner not in persona_providers → claude
|
||||
expect(calls[0][2].provider).toBe('claude');
|
||||
// Implement movement: coder in persona_providers → codex
|
||||
expect(calls[1][2].provider).toBe('codex');
|
||||
});
|
||||
});
|
||||
@ -158,6 +158,21 @@ describe('resolveRefToContent with layer resolution', () => {
|
||||
// No context, no file — returns the spec as-is (inline content behavior)
|
||||
expect(content).toBe('some-name');
|
||||
});
|
||||
|
||||
it('should fall back to resolveResourceContent when facet not found with context', () => {
|
||||
// Given: facetType and context provided, but no matching facet file exists
|
||||
// When: resolveRefToContent is called with a name that has no facet file
|
||||
const content = resolveRefToContent(
|
||||
'nonexistent-facet-xyz',
|
||||
undefined,
|
||||
tempDir,
|
||||
'policies',
|
||||
context,
|
||||
);
|
||||
|
||||
// Then: falls back to resolveResourceContent, which returns the ref as inline content
|
||||
expect(content).toBe('nonexistent-facet-xyz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRefList with layer resolution', () => {
|
||||
|
||||
@ -336,6 +336,63 @@ describe('loadGlobalConfig', () => {
|
||||
expect(config.interactivePreviewMovements).toBe(0);
|
||||
});
|
||||
|
||||
describe('persona_providers', () => {
|
||||
it('should load persona_providers from config.yaml', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(
|
||||
getGlobalConfigPath(),
|
||||
[
|
||||
'language: en',
|
||||
'persona_providers:',
|
||||
' coder: codex',
|
||||
' reviewer: claude',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
|
||||
expect(config.personaProviders).toEqual({
|
||||
coder: 'codex',
|
||||
reviewer: 'claude',
|
||||
});
|
||||
});
|
||||
|
||||
it('should save and reload persona_providers', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8');
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
config.personaProviders = { coder: 'codex' };
|
||||
saveGlobalConfig(config);
|
||||
invalidateGlobalConfigCache();
|
||||
|
||||
const reloaded = loadGlobalConfig();
|
||||
expect(reloaded.personaProviders).toEqual({ coder: 'codex' });
|
||||
});
|
||||
|
||||
it('should have undefined personaProviders by default', () => {
|
||||
const config = loadGlobalConfig();
|
||||
expect(config.personaProviders).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not save persona_providers when empty', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
mkdirSync(taktDir, { recursive: true });
|
||||
writeFileSync(getGlobalConfigPath(), 'language: en\n', 'utf-8');
|
||||
|
||||
const config = loadGlobalConfig();
|
||||
config.personaProviders = {};
|
||||
saveGlobalConfig(config);
|
||||
invalidateGlobalConfigCache();
|
||||
|
||||
const reloaded = loadGlobalConfig();
|
||||
expect(reloaded.personaProviders).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider/model compatibility validation', () => {
|
||||
it('should throw when provider is codex but model is a Claude alias (opus)', () => {
|
||||
const taktDir = join(testHomeDir, '.takt');
|
||||
|
||||
532
src/__tests__/interactive-mode.test.ts
Normal file
532
src/__tests__/interactive-mode.test.ts
Normal file
@ -0,0 +1,532 @@
|
||||
/**
|
||||
* Tests for interactive mode variants (assistant, persona, quiet, passthrough)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// ── Mocks ──────────────────────────────────────────────
|
||||
|
||||
vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
loadGlobalConfig: vi.fn(() => ({ provider: 'mock', language: 'en' })),
|
||||
getBuiltinPiecesEnabled: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/providers/index.js', () => ({
|
||||
getProvider: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/context.js', () => ({
|
||||
isQuietMode: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
vi.mock('../infra/config/paths.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
loadPersonaSessions: vi.fn(() => ({})),
|
||||
updatePersonaSession: vi.fn(),
|
||||
getProjectConfigDir: vi.fn(() => '/tmp'),
|
||||
loadSessionState: vi.fn(() => null),
|
||||
clearSessionState: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/ui/index.js', () => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
blankLine: vi.fn(),
|
||||
StreamDisplay: vi.fn().mockImplementation(() => ({
|
||||
createHandler: vi.fn(() => vi.fn()),
|
||||
flush: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/prompt/index.js', () => ({
|
||||
selectOption: vi.fn(),
|
||||
selectOptionWithDefault: vi.fn(),
|
||||
}));
|
||||
|
||||
import { getProvider } from '../infra/providers/index.js';
|
||||
import { selectOptionWithDefault, selectOption } from '../shared/prompt/index.js';
|
||||
|
||||
const mockGetProvider = vi.mocked(getProvider);
|
||||
const mockSelectOptionWithDefault = vi.mocked(selectOptionWithDefault);
|
||||
const mockSelectOption = vi.mocked(selectOption);
|
||||
|
||||
// ── Stdin helpers (same pattern as interactive.test.ts) ──
|
||||
|
||||
let savedIsTTY: boolean | undefined;
|
||||
let savedIsRaw: boolean | undefined;
|
||||
let savedSetRawMode: typeof process.stdin.setRawMode | undefined;
|
||||
let savedStdoutWrite: typeof process.stdout.write;
|
||||
let savedStdinOn: typeof process.stdin.on;
|
||||
let savedStdinRemoveListener: typeof process.stdin.removeListener;
|
||||
let savedStdinResume: typeof process.stdin.resume;
|
||||
let savedStdinPause: typeof process.stdin.pause;
|
||||
|
||||
function setupRawStdin(rawInputs: string[]): void {
|
||||
savedIsTTY = process.stdin.isTTY;
|
||||
savedIsRaw = process.stdin.isRaw;
|
||||
savedSetRawMode = process.stdin.setRawMode;
|
||||
savedStdoutWrite = process.stdout.write;
|
||||
savedStdinOn = process.stdin.on;
|
||||
savedStdinRemoveListener = process.stdin.removeListener;
|
||||
savedStdinResume = process.stdin.resume;
|
||||
savedStdinPause = process.stdin.pause;
|
||||
|
||||
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdin, 'isRaw', { value: false, configurable: true, writable: true });
|
||||
process.stdin.setRawMode = vi.fn((mode: boolean) => {
|
||||
(process.stdin as unknown as { isRaw: boolean }).isRaw = mode;
|
||||
return process.stdin;
|
||||
}) as unknown as typeof process.stdin.setRawMode;
|
||||
process.stdout.write = vi.fn(() => true) as unknown as typeof process.stdout.write;
|
||||
process.stdin.resume = vi.fn(() => process.stdin) as unknown as typeof process.stdin.resume;
|
||||
process.stdin.pause = vi.fn(() => process.stdin) as unknown as typeof process.stdin.pause;
|
||||
|
||||
let currentHandler: ((data: Buffer) => void) | null = null;
|
||||
let inputIndex = 0;
|
||||
|
||||
process.stdin.on = vi.fn(((event: string, handler: (...args: unknown[]) => void) => {
|
||||
if (event === 'data') {
|
||||
currentHandler = handler as (data: Buffer) => void;
|
||||
if (inputIndex < rawInputs.length) {
|
||||
const data = rawInputs[inputIndex]!;
|
||||
inputIndex++;
|
||||
queueMicrotask(() => {
|
||||
if (currentHandler) {
|
||||
currentHandler(Buffer.from(data, 'utf-8'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return process.stdin;
|
||||
}) as typeof process.stdin.on);
|
||||
|
||||
process.stdin.removeListener = vi.fn(((event: string) => {
|
||||
if (event === 'data') {
|
||||
currentHandler = null;
|
||||
}
|
||||
return process.stdin;
|
||||
}) as typeof process.stdin.removeListener);
|
||||
}
|
||||
|
||||
function restoreStdin(): void {
|
||||
if (savedIsTTY !== undefined) {
|
||||
Object.defineProperty(process.stdin, 'isTTY', { value: savedIsTTY, configurable: true });
|
||||
}
|
||||
if (savedIsRaw !== undefined) {
|
||||
Object.defineProperty(process.stdin, 'isRaw', { value: savedIsRaw, configurable: true, writable: true });
|
||||
}
|
||||
if (savedSetRawMode) process.stdin.setRawMode = savedSetRawMode;
|
||||
if (savedStdoutWrite) process.stdout.write = savedStdoutWrite;
|
||||
if (savedStdinOn) process.stdin.on = savedStdinOn;
|
||||
if (savedStdinRemoveListener) process.stdin.removeListener = savedStdinRemoveListener;
|
||||
if (savedStdinResume) process.stdin.resume = savedStdinResume;
|
||||
if (savedStdinPause) process.stdin.pause = savedStdinPause;
|
||||
}
|
||||
|
||||
function toRawInputs(inputs: (string | null)[]): string[] {
|
||||
return inputs.map((input) => {
|
||||
if (input === null) return '\x04';
|
||||
return input + '\r';
|
||||
});
|
||||
}
|
||||
|
||||
function setupMockProvider(responses: string[]): void {
|
||||
let callIndex = 0;
|
||||
const mockCall = vi.fn(async () => {
|
||||
const content = callIndex < responses.length ? responses[callIndex] : 'AI response';
|
||||
callIndex++;
|
||||
return {
|
||||
persona: 'interactive',
|
||||
status: 'done' as const,
|
||||
content: content!,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
});
|
||||
const mockSetup = vi.fn(() => ({ call: mockCall }));
|
||||
const mockProvider = {
|
||||
setup: mockSetup,
|
||||
_call: mockCall,
|
||||
_setup: mockSetup,
|
||||
};
|
||||
mockGetProvider.mockReturnValue(mockProvider);
|
||||
}
|
||||
|
||||
// ── Imports (after mocks) ──
|
||||
|
||||
import { INTERACTIVE_MODES, DEFAULT_INTERACTIVE_MODE } from '../core/models/interactive-mode.js';
|
||||
import { selectInteractiveMode } from '../features/interactive/modeSelection.js';
|
||||
import { passthroughMode } from '../features/interactive/passthroughMode.js';
|
||||
import { quietMode } from '../features/interactive/quietMode.js';
|
||||
import { personaMode } from '../features/interactive/personaMode.js';
|
||||
import type { PieceContext } from '../features/interactive/interactive.js';
|
||||
import type { FirstMovementInfo } from '../infra/config/loaders/pieceResolver.js';
|
||||
|
||||
// ── Setup ──
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSelectOptionWithDefault.mockResolvedValue('assistant');
|
||||
mockSelectOption.mockResolvedValue('execute');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreStdin();
|
||||
});
|
||||
|
||||
// ── InteractiveMode type & constants tests ──
|
||||
|
||||
describe('InteractiveMode type', () => {
|
||||
it('should define all four modes', () => {
|
||||
expect(INTERACTIVE_MODES).toEqual(['assistant', 'persona', 'quiet', 'passthrough']);
|
||||
});
|
||||
|
||||
it('should have assistant as default mode', () => {
|
||||
expect(DEFAULT_INTERACTIVE_MODE).toBe('assistant');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Mode selection tests ──
|
||||
|
||||
describe('selectInteractiveMode', () => {
|
||||
it('should call selectOptionWithDefault with four mode options', async () => {
|
||||
// When
|
||||
await selectInteractiveMode('en');
|
||||
|
||||
// Then
|
||||
expect(mockSelectOptionWithDefault).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'assistant' }),
|
||||
expect.objectContaining({ value: 'persona' }),
|
||||
expect.objectContaining({ value: 'quiet' }),
|
||||
expect.objectContaining({ value: 'passthrough' }),
|
||||
]),
|
||||
'assistant',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use piece default when provided', async () => {
|
||||
// When
|
||||
await selectInteractiveMode('en', 'quiet');
|
||||
|
||||
// Then
|
||||
expect(mockSelectOptionWithDefault).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(Array),
|
||||
'quiet',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null when user cancels', async () => {
|
||||
// Given
|
||||
mockSelectOptionWithDefault.mockResolvedValue(null);
|
||||
|
||||
// When
|
||||
const result = await selectInteractiveMode('en');
|
||||
|
||||
// Then
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return selected mode value', async () => {
|
||||
// Given
|
||||
mockSelectOptionWithDefault.mockResolvedValue('persona');
|
||||
|
||||
// When
|
||||
const result = await selectInteractiveMode('ja');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('persona');
|
||||
});
|
||||
|
||||
it('should present options in correct order', async () => {
|
||||
// When
|
||||
await selectInteractiveMode('en');
|
||||
|
||||
// Then
|
||||
const options = mockSelectOptionWithDefault.mock.calls[0]?.[1] as Array<{ value: string }>;
|
||||
expect(options?.[0]?.value).toBe('assistant');
|
||||
expect(options?.[1]?.value).toBe('persona');
|
||||
expect(options?.[2]?.value).toBe('quiet');
|
||||
expect(options?.[3]?.value).toBe('passthrough');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Passthrough mode tests ──
|
||||
|
||||
describe('passthroughMode', () => {
|
||||
it('should return initialInput directly when provided', async () => {
|
||||
// When
|
||||
const result = await passthroughMode('en', 'my task text');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('my task text');
|
||||
});
|
||||
|
||||
it('should return cancel when user sends EOF', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs([null]));
|
||||
|
||||
// When
|
||||
const result = await passthroughMode('en');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('cancel');
|
||||
expect(result.task).toBe('');
|
||||
});
|
||||
|
||||
it('should return cancel when user enters empty input', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['']));
|
||||
|
||||
// When
|
||||
const result = await passthroughMode('en');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should return user input as task when entered', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['implement login feature']));
|
||||
|
||||
// When
|
||||
const result = await passthroughMode('en');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('implement login feature');
|
||||
});
|
||||
|
||||
it('should trim whitespace from user input', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs([' my task ']));
|
||||
|
||||
// When
|
||||
const result = await passthroughMode('en');
|
||||
|
||||
// Then
|
||||
expect(result.task).toBe('my task');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Quiet mode tests ──
|
||||
|
||||
describe('quietMode', () => {
|
||||
it('should generate instructions from initialInput without questions', async () => {
|
||||
// Given
|
||||
setupMockProvider(['Generated task instruction for login feature.']);
|
||||
mockSelectOption.mockResolvedValue('execute');
|
||||
|
||||
// When
|
||||
const result = await quietMode('/project', 'implement login feature');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Generated task instruction for login feature.');
|
||||
});
|
||||
|
||||
it('should return cancel when user sends EOF for input', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs([null]));
|
||||
setupMockProvider([]);
|
||||
|
||||
// When
|
||||
const result = await quietMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should return cancel when user enters empty input', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['']));
|
||||
setupMockProvider([]);
|
||||
|
||||
// When
|
||||
const result = await quietMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should prompt for input when no initialInput is provided', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['fix the bug']));
|
||||
setupMockProvider(['Fix the bug instruction.']);
|
||||
mockSelectOption.mockResolvedValue('execute');
|
||||
|
||||
// When
|
||||
const result = await quietMode('/project');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Fix the bug instruction.');
|
||||
});
|
||||
|
||||
it('should include piece context in summary generation', async () => {
|
||||
// Given
|
||||
const pieceContext: PieceContext = {
|
||||
name: 'test-piece',
|
||||
description: 'A test piece',
|
||||
pieceStructure: '1. implement\n2. review',
|
||||
movementPreviews: [],
|
||||
};
|
||||
setupMockProvider(['Instruction with piece context.']);
|
||||
mockSelectOption.mockResolvedValue('execute');
|
||||
|
||||
// When
|
||||
const result = await quietMode('/project', 'some task', pieceContext);
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Instruction with piece context.');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Persona mode tests ──
|
||||
|
||||
describe('personaMode', () => {
|
||||
const mockFirstMovement: FirstMovementInfo = {
|
||||
personaContent: 'You are a senior coder. Write clean, maintainable code.',
|
||||
personaDisplayName: 'Coder',
|
||||
allowedTools: ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'],
|
||||
};
|
||||
|
||||
it('should return cancel when user types /cancel', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['/cancel']));
|
||||
setupMockProvider([]);
|
||||
|
||||
// When
|
||||
const result = await personaMode('/project', mockFirstMovement);
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('cancel');
|
||||
expect(result.task).toBe('');
|
||||
});
|
||||
|
||||
it('should return cancel on EOF', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs([null]));
|
||||
setupMockProvider([]);
|
||||
|
||||
// When
|
||||
const result = await personaMode('/project', mockFirstMovement);
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should use first movement persona as system prompt', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['fix bug', '/cancel']));
|
||||
setupMockProvider(['I see the issue.']);
|
||||
|
||||
// When
|
||||
await personaMode('/project', mockFirstMovement);
|
||||
|
||||
// Then: the provider should be set up with persona content as system prompt
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _setup: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._setup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
systemPrompt: 'You are a senior coder. Write clean, maintainable code.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use first movement allowed tools', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['check the code', '/cancel']));
|
||||
setupMockProvider(['Looking at the code.']);
|
||||
|
||||
// When
|
||||
await personaMode('/project', mockFirstMovement);
|
||||
|
||||
// Then
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
allowedTools: ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should process initialInput as first message', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['/go']));
|
||||
setupMockProvider(['I analyzed the issue.', 'Task summary.']);
|
||||
mockSelectOption.mockResolvedValue('execute');
|
||||
|
||||
// When
|
||||
const result = await personaMode('/project', mockFirstMovement, 'fix the login');
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call).toHaveBeenCalledTimes(2);
|
||||
const firstPrompt = mockProvider._call.mock.calls[0]?.[0] as string;
|
||||
expect(firstPrompt).toBe('fix the login');
|
||||
});
|
||||
|
||||
it('should handle /play command', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['/play direct task text']));
|
||||
setupMockProvider([]);
|
||||
|
||||
// When
|
||||
const result = await personaMode('/project', mockFirstMovement);
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('direct task text');
|
||||
});
|
||||
|
||||
it('should fall back to default tools when first movement has none', async () => {
|
||||
// Given
|
||||
const noToolsMovement: FirstMovementInfo = {
|
||||
personaContent: 'Persona prompt',
|
||||
personaDisplayName: 'Agent',
|
||||
allowedTools: [],
|
||||
};
|
||||
setupRawStdin(toRawInputs(['test', '/cancel']));
|
||||
setupMockProvider(['response']);
|
||||
|
||||
// When
|
||||
await personaMode('/project', noToolsMovement);
|
||||
|
||||
// Then
|
||||
const mockProvider = mockGetProvider.mock.results[0]!.value as { _call: ReturnType<typeof vi.fn> };
|
||||
expect(mockProvider._call).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multi-turn conversation before /go', async () => {
|
||||
// Given
|
||||
setupRawStdin(toRawInputs(['first message', 'second message', '/go']));
|
||||
setupMockProvider(['reply 1', 'reply 2', 'Final summary.']);
|
||||
mockSelectOption.mockResolvedValue('execute');
|
||||
|
||||
// When
|
||||
const result = await personaMode('/project', mockFirstMovement);
|
||||
|
||||
// Then
|
||||
expect(result.action).toBe('execute');
|
||||
expect(result.task).toBe('Final summary.');
|
||||
});
|
||||
});
|
||||
@ -25,6 +25,7 @@ vi.mock('../infra/config/global/globalConfig.js', () => ({
|
||||
// --- Imports (after mocks) ---
|
||||
|
||||
import { loadPiece } from '../infra/config/index.js';
|
||||
import { listBuiltinPieceNames } from '../infra/config/loaders/pieceResolver.js';
|
||||
|
||||
// --- Test helpers ---
|
||||
|
||||
@ -45,7 +46,7 @@ describe('Piece Loader IT: builtin piece loading', () => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const builtinNames = ['default', 'minimal', 'expert', 'expert-cqrs', 'research', 'magi', 'review-only', 'review-fix-minimal'];
|
||||
const builtinNames = listBuiltinPieceNames({ includeDisabled: true });
|
||||
|
||||
for (const name of builtinNames) {
|
||||
it(`should load builtin piece: ${name}`, () => {
|
||||
@ -572,6 +573,139 @@ movements:
|
||||
});
|
||||
});
|
||||
|
||||
describe('Piece Loader IT: structural-reform piece', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = createTestDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should load structural-reform with 7 movements', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.name).toBe('structural-reform');
|
||||
expect(config!.movements.length).toBe(7);
|
||||
expect(config!.maxIterations).toBe(50);
|
||||
expect(config!.initialMovement).toBe('review');
|
||||
});
|
||||
|
||||
it('should have expected movement names in order', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
const movementNames = config!.movements.map((m) => m.name);
|
||||
expect(movementNames).toEqual([
|
||||
'review',
|
||||
'plan_reform',
|
||||
'implement',
|
||||
'reviewers',
|
||||
'fix',
|
||||
'verify',
|
||||
'next_target',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have review as read-only with instruction_template', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
const review = config!.movements.find((m) => m.name === 'review');
|
||||
expect(review).toBeDefined();
|
||||
expect(review!.edit).not.toBe(true);
|
||||
expect(review!.instructionTemplate).toBeDefined();
|
||||
expect(review!.instructionTemplate).toContain('{task}');
|
||||
});
|
||||
|
||||
it('should have implement with edit: true and session: refresh', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
const implement = config!.movements.find((m) => m.name === 'implement');
|
||||
expect(implement).toBeDefined();
|
||||
expect(implement!.edit).toBe(true);
|
||||
expect(implement!.session).toBe('refresh');
|
||||
});
|
||||
|
||||
it('should have 2 parallel reviewers (arch-review and qa-review)', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
const reviewers = config!.movements.find(
|
||||
(m) => m.parallel && m.parallel.length > 0,
|
||||
);
|
||||
expect(reviewers).toBeDefined();
|
||||
expect(reviewers!.parallel!.length).toBe(2);
|
||||
|
||||
const subNames = reviewers!.parallel!.map((s) => s.name);
|
||||
expect(subNames).toContain('arch-review');
|
||||
expect(subNames).toContain('qa-review');
|
||||
});
|
||||
|
||||
it('should have aggregate rules on reviewers movement', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
const reviewers = config!.movements.find(
|
||||
(m) => m.parallel && m.parallel.length > 0,
|
||||
);
|
||||
expect(reviewers).toBeDefined();
|
||||
|
||||
const allRule = reviewers!.rules?.find(
|
||||
(r) => r.isAggregateCondition && r.aggregateType === 'all',
|
||||
);
|
||||
expect(allRule).toBeDefined();
|
||||
expect(allRule!.aggregateConditionText).toBe('approved');
|
||||
expect(allRule!.next).toBe('verify');
|
||||
|
||||
const anyRule = reviewers!.rules?.find(
|
||||
(r) => r.isAggregateCondition && r.aggregateType === 'any',
|
||||
);
|
||||
expect(anyRule).toBeDefined();
|
||||
expect(anyRule!.aggregateConditionText).toBe('needs_fix');
|
||||
expect(anyRule!.next).toBe('fix');
|
||||
});
|
||||
|
||||
it('should have verify movement with instruction_template', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
const verify = config!.movements.find((m) => m.name === 'verify');
|
||||
expect(verify).toBeDefined();
|
||||
expect(verify!.edit).not.toBe(true);
|
||||
expect(verify!.instructionTemplate).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have next_target movement routing to implement or COMPLETE', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
const nextTarget = config!.movements.find((m) => m.name === 'next_target');
|
||||
expect(nextTarget).toBeDefined();
|
||||
expect(nextTarget!.edit).not.toBe(true);
|
||||
|
||||
const nextValues = nextTarget!.rules?.map((r) => r.next);
|
||||
expect(nextValues).toContain('implement');
|
||||
expect(nextValues).toContain('COMPLETE');
|
||||
});
|
||||
|
||||
it('should have loop_monitors for implement-fix cycle', () => {
|
||||
const config = loadPiece('structural-reform', testDir);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.loopMonitors).toBeDefined();
|
||||
expect(config!.loopMonitors!.length).toBe(1);
|
||||
|
||||
const monitor = config!.loopMonitors![0]!;
|
||||
expect(monitor.cycle).toEqual(['implement', 'fix']);
|
||||
expect(monitor.threshold).toBe(3);
|
||||
expect(monitor.judge).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Piece Loader IT: invalid YAML handling', () => {
|
||||
let testDir: string;
|
||||
|
||||
|
||||
@ -131,9 +131,11 @@ describe('readMultilineInput cursor navigation', () => {
|
||||
let savedStdinRemoveListener: typeof process.stdin.removeListener;
|
||||
let savedStdinResume: typeof process.stdin.resume;
|
||||
let savedStdinPause: typeof process.stdin.pause;
|
||||
let savedColumns: number | undefined;
|
||||
let columnsOverridden = false;
|
||||
let stdoutCalls: string[];
|
||||
|
||||
function setupRawStdin(rawInputs: string[]): void {
|
||||
function setupRawStdin(rawInputs: string[], termColumns?: number): void {
|
||||
savedIsTTY = process.stdin.isTTY;
|
||||
savedIsRaw = process.stdin.isRaw;
|
||||
savedSetRawMode = process.stdin.setRawMode;
|
||||
@ -142,6 +144,13 @@ describe('readMultilineInput cursor navigation', () => {
|
||||
savedStdinRemoveListener = process.stdin.removeListener;
|
||||
savedStdinResume = process.stdin.resume;
|
||||
savedStdinPause = process.stdin.pause;
|
||||
savedColumns = process.stdout.columns;
|
||||
columnsOverridden = false;
|
||||
|
||||
if (termColumns !== undefined) {
|
||||
Object.defineProperty(process.stdout, 'columns', { value: termColumns, configurable: true, writable: true });
|
||||
columnsOverridden = true;
|
||||
}
|
||||
|
||||
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
||||
Object.defineProperty(process.stdin, 'isRaw', { value: false, configurable: true, writable: true });
|
||||
@ -197,6 +206,10 @@ describe('readMultilineInput cursor navigation', () => {
|
||||
if (savedStdinRemoveListener) process.stdin.removeListener = savedStdinRemoveListener;
|
||||
if (savedStdinResume) process.stdin.resume = savedStdinResume;
|
||||
if (savedStdinPause) process.stdin.pause = savedStdinPause;
|
||||
if (columnsOverridden) {
|
||||
Object.defineProperty(process.stdout, 'columns', { value: savedColumns, configurable: true, writable: true });
|
||||
columnsOverridden = false;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@ -611,4 +624,338 @@ describe('readMultilineInput cursor navigation', () => {
|
||||
expect(result).toBe('abc\ndef\nghiX');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: arrow up within wrapped line', () => {
|
||||
it('should move to previous display row within same logical line', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), first display row = 18 chars, second = 20 chars
|
||||
// Type 30 chars "abcdefghijklmnopqrstuvwxyz1234" → wraps at pos 18
|
||||
// Display row 1: "abcdefghijklmnopqr" (18 chars, cols 3-20 with prompt)
|
||||
// Display row 2: "stuvwxyz1234" (12 chars, cols 1-12)
|
||||
// Cursor at end (pos 30, display col 12), press ↑ → display col 12 in row 1 → pos 12
|
||||
// Insert "X" → "abcdefghijklXmnopqrstuvwxyz1234"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[AX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklXmnopqrstuvwxyz1234');
|
||||
});
|
||||
|
||||
it('should do nothing when on first display row of first logical line', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type "abcdefghij" (10 chars, fits in first row of 18 cols)
|
||||
// Cursor at end (pos 10, first display row), press ↑ → no previous row, nothing happens
|
||||
// Insert "X" → "abcdefghijX"
|
||||
setupRawStdin([
|
||||
'abcdefghij\x1B[AX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijX');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: arrow down within wrapped line', () => {
|
||||
it('should move to next display row within same logical line', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), first row = 18 chars
|
||||
// Type 30 chars, Home → pos 0, then ↓ → display col 0 in row 2 → pos 18
|
||||
// Insert "X" → "abcdefghijklmnopqrXstuvwxyz1234"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[BX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklmnopqrXstuvwxyz1234');
|
||||
});
|
||||
|
||||
it('should do nothing when on last display row of last logical line', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars (wraps into 2 display rows)
|
||||
// Cursor at end (last display row), press ↓ → nothing happens
|
||||
// Insert "X" → "abcdefghijklmnopqrstuvwxyz1234X"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[BX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklmnopqrstuvwxyz1234X');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: Ctrl+A moves to display row start', () => {
|
||||
it('should move to display row start on wrapped second row', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
|
||||
// Cursor at end (pos 30), Ctrl+A → display row start (pos 18), insert "X"
|
||||
// Result: "abcdefghijklmnopqrXstuvwxyz1234"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x01X\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklmnopqrXstuvwxyz1234');
|
||||
});
|
||||
|
||||
it('should move to display row start on first row', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
|
||||
// Move cursor to middle of first display row (Home, Right*5 → pos 5)
|
||||
// Ctrl+A → pos 0, insert "X"
|
||||
// Result: "Xabcdefghijklmnopqrstuvwxyz1234"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[C\x1B[C\x1B[C\x1B[C\x1B[C\x01X\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('Xabcdefghijklmnopqrstuvwxyz1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: Ctrl+E moves to display row end', () => {
|
||||
it('should move to display row end on first row', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
|
||||
// Home → pos 0, Ctrl+E → end of first display row (pos 18), insert "X"
|
||||
// Result: "abcdefghijklmnopqrXstuvwxyz1234"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x05X\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklmnopqrXstuvwxyz1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: Home moves to logical line start', () => {
|
||||
it('should move from wrapped second row to logical line start', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), first row = 18 chars
|
||||
// Type 30 chars, cursor at end (pos 30, second display row)
|
||||
// Home → logical line start (pos 0), insert "X"
|
||||
// Result: "Xabcdefghijklmnopqrstuvwxyz1234"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[HX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('Xabcdefghijklmnopqrstuvwxyz1234');
|
||||
});
|
||||
|
||||
it('should emit cursor up sequence when crossing display rows', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars (wraps into 2 rows)
|
||||
// Cursor at end (second display row), Home → pos 0 (first display row)
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
await callReadMultilineInput('> ');
|
||||
|
||||
// Then: should contain \x1B[{n}A for moving up display rows
|
||||
const hasUpMove = stdoutCalls.some(c => /^\x1B\[\d+A$/.test(c));
|
||||
expect(hasUpMove).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: End moves to logical line end', () => {
|
||||
it('should move from first display row to logical line end', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), first row = 18 chars
|
||||
// Type 30 chars, Home → pos 0, End → logical line end (pos 30), insert "X"
|
||||
// Result: "abcdefghijklmnopqrstuvwxyz1234X"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[FX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklmnopqrstuvwxyz1234X');
|
||||
});
|
||||
|
||||
it('should emit cursor down sequence when crossing display rows', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars (wraps into 2 rows)
|
||||
// Home → pos 0 (first display row), End → pos 30 (second display row)
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[H\x1B[F\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
await callReadMultilineInput('> ');
|
||||
|
||||
// Then: should contain \x1B[{n}B for moving down display rows
|
||||
const hasDownMove = stdoutCalls.some(c => /^\x1B\[\d+B$/.test(c));
|
||||
expect(hasDownMove).toBe(true);
|
||||
});
|
||||
|
||||
it('should stay at end when already at logical line end on last display row', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols), type 30 chars
|
||||
// Cursor at end (pos 30, already at logical line end), End → nothing changes, insert "X"
|
||||
// Result: "abcdefghijklmnopqrstuvwxyz1234X"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwxyz1234\x1B[FX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklmnopqrstuvwxyz1234X');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: non-wrapped text retains original behavior', () => {
|
||||
it('should not affect arrow up on short single-line text', async () => {
|
||||
// Given: termWidth=80, short text "abc" (no wrap), ↑ does nothing
|
||||
setupRawStdin([
|
||||
'abc\x1B[AX\r',
|
||||
], 80);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcX');
|
||||
});
|
||||
|
||||
it('should not affect arrow down on short single-line text', async () => {
|
||||
// Given: termWidth=80, short text "abc" (no wrap), ↓ does nothing
|
||||
setupRawStdin([
|
||||
'abc\x1B[BX\r',
|
||||
], 80);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcX');
|
||||
});
|
||||
|
||||
it('should still navigate between logical lines with arrow up', async () => {
|
||||
// Given: termWidth=80, "abcde\nfgh" (no wrap), cursor at end of "fgh", ↑ → "abcde" at col 3
|
||||
setupRawStdin([
|
||||
'abcde\x1B[13;2ufgh\x1B[AX\r',
|
||||
], 80);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcXde\nfgh');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: full-width characters', () => {
|
||||
it('should calculate display row boundaries with full-width chars', async () => {
|
||||
// Given: termWidth=10, prompt "> " (2 cols), first row available = 8 cols
|
||||
// Type "あいうえ" (4 full-width chars = 8 display cols = fills first row exactly)
|
||||
// Then type "お" (2 cols, starts second row)
|
||||
// Cursor at end (after "お"), Ctrl+A → display row start (pos 4, start of "お")
|
||||
// Insert "X"
|
||||
// Result: "あいうえXお"
|
||||
setupRawStdin([
|
||||
'あいうえお\x01X\r',
|
||||
], 10);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('あいうえXお');
|
||||
});
|
||||
|
||||
it('should push full-width char to next row when only 1 column remains', async () => {
|
||||
// Given: termWidth=10, prompt "> " (2 cols), first row available = 8 cols
|
||||
// Type "abcdefg" (7 cols) then "あ" (2 cols) → 7+2=9 > 8, "あ" goes to row 2
|
||||
// Cursor at end (after "あ"), Ctrl+A → display row start at "あ" (pos 7)
|
||||
// Insert "X"
|
||||
// Result: "abcdefgXあ"
|
||||
setupRawStdin([
|
||||
'abcdefgあ\x01X\r',
|
||||
], 10);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefgXあ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: prompt width consideration', () => {
|
||||
it('should account for prompt width in first display row', async () => {
|
||||
// Given: termWidth=10, prompt "> " (2 cols), first row = 8 chars
|
||||
// Type "12345678" (8 chars = fills first row) then "9" (starts row 2)
|
||||
// Cursor at "9" (pos 9), ↑ → row 1 at display col 1, but only 8 chars available
|
||||
// Display col 1 → pos 1
|
||||
// Insert "X" → "1X234567890" ... wait, let me recalculate.
|
||||
// Actually: cursor at end of "123456789" (pos 9, display col 1 in row 2)
|
||||
// ↑ → display col 1 in row 1 → pos 1
|
||||
// Insert "X" → "1X23456789"
|
||||
setupRawStdin([
|
||||
'123456789\x1B[AX\r',
|
||||
], 10);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('1X23456789');
|
||||
});
|
||||
|
||||
it('should not add prompt offset for second logical line', async () => {
|
||||
// Given: termWidth=10, prompt "> " (2 cols)
|
||||
// Type "ab\n123456789" → second logical line "123456789" (9 chars, fits in 10 col row)
|
||||
// Cursor at end (pos 12), ↑ → "ab" at display col 9 → clamped to col 2 → pos 2 (end of "ab")
|
||||
// Insert "X" → "abX\n123456789"
|
||||
setupRawStdin([
|
||||
'ab\x1B[13;2u123456789\x1B[AX\r',
|
||||
], 10);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abX\n123456789');
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-wrap: cross logical line with display rows', () => {
|
||||
it('should move from wrapped logical line to previous logical line last display row', async () => {
|
||||
// Given: termWidth=20, prompt "> " (2 cols)
|
||||
// Line 1: "abcdefghijklmnopqrstuvwx" (24 chars) → wraps: row 1 (18 chars) + row 2 (6 chars)
|
||||
// Line 2: "123"
|
||||
// Cursor at end of "123" (display col 3), ↑ → last display row of line 1 (row 2: "uvwx", 6 chars)
|
||||
// Display col 3 → pos 21 ("v" position... let me calculate)
|
||||
// Row 2 of line 1 starts at pos 18 ("stuvwx"), display col 3 → pos 21
|
||||
// Insert "X" → "abcdefghijklmnopqrstuXvwx\n123"
|
||||
setupRawStdin([
|
||||
'abcdefghijklmnopqrstuvwx\x1B[13;2u123\x1B[AX\r',
|
||||
], 20);
|
||||
|
||||
// When
|
||||
const result = await callReadMultilineInput('> ');
|
||||
|
||||
// Then
|
||||
expect(result).toBe('abcdefghijklmnopqrstuXvwx\n123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -67,6 +67,29 @@ describe('ParallelLogger', () => {
|
||||
// No padding needed (0 spaces)
|
||||
expect(prefix).toMatch(/\x1b\[0m $/);
|
||||
});
|
||||
|
||||
it('should build rich prefix with task and parent movement for parallel task mode', () => {
|
||||
const logger = new ParallelLogger({
|
||||
subMovementNames: ['arch-review'],
|
||||
writeFn,
|
||||
progressInfo: {
|
||||
iteration: 4,
|
||||
maxIterations: 30,
|
||||
},
|
||||
taskLabel: 'override-persona-provider',
|
||||
taskColorIndex: 0,
|
||||
parentMovementName: 'reviewers',
|
||||
movementIteration: 1,
|
||||
});
|
||||
|
||||
const prefix = logger.buildPrefix('arch-review', 0);
|
||||
expect(prefix).toContain('\x1b[36m');
|
||||
expect(prefix).toContain('[over]');
|
||||
expect(prefix).toContain('[reviewers]');
|
||||
expect(prefix).toContain('[arch-review]');
|
||||
expect(prefix).toContain('(4/30)(1)');
|
||||
expect(prefix).not.toContain('step 1/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('text event line buffering', () => {
|
||||
|
||||
@ -14,10 +14,12 @@ const { mockIsDebugEnabled, mockWritePromptLog, MockPieceEngine } = vi.hoisted((
|
||||
|
||||
class MockPieceEngine extends EE {
|
||||
private config: PieceConfig;
|
||||
private task: string;
|
||||
|
||||
constructor(config: PieceConfig, _cwd: string, _task: string, _options: unknown) {
|
||||
constructor(config: PieceConfig, _cwd: string, task: string, _options: unknown) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
abort(): void {}
|
||||
@ -26,6 +28,7 @@ const { mockIsDebugEnabled, mockWritePromptLog, MockPieceEngine } = vi.hoisted((
|
||||
const step = this.config.movements[0]!;
|
||||
const timestamp = new Date('2026-02-07T00:00:00.000Z');
|
||||
|
||||
const shouldRepeatMovement = this.task === 'repeat-movement-task';
|
||||
this.emit('movement:start', step, 1, 'movement instruction');
|
||||
this.emit('phase:start', step, 1, 'execute', 'phase prompt');
|
||||
this.emit('phase:complete', step, 1, 'execute', 'phase response', 'done');
|
||||
@ -40,9 +43,23 @@ const { mockIsDebugEnabled, mockWritePromptLog, MockPieceEngine } = vi.hoisted((
|
||||
},
|
||||
'movement instruction'
|
||||
);
|
||||
if (shouldRepeatMovement) {
|
||||
this.emit('movement:start', step, 2, 'movement instruction repeat');
|
||||
this.emit(
|
||||
'movement:complete',
|
||||
step,
|
||||
{
|
||||
persona: step.personaDisplayName,
|
||||
status: 'done',
|
||||
content: 'movement response repeat',
|
||||
timestamp,
|
||||
},
|
||||
'movement instruction repeat'
|
||||
);
|
||||
}
|
||||
this.emit('piece:complete', { status: 'completed', iteration: 1 });
|
||||
|
||||
return { status: 'completed', iteration: 1 };
|
||||
return { status: 'completed', iteration: shouldRepeatMovement ? 2 : 1 };
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,4 +204,32 @@ describe('executePiece debug prompts logging', () => {
|
||||
|
||||
expect(mockWritePromptLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update movement prefix context on each movement:start event', async () => {
|
||||
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
||||
|
||||
try {
|
||||
await executePiece(makeConfig(), 'repeat-movement-task', '/tmp/project', {
|
||||
projectCwd: '/tmp/project',
|
||||
taskPrefix: 'override-persona-provider',
|
||||
taskColorIndex: 0,
|
||||
});
|
||||
|
||||
const output = stdoutSpy.mock.calls.map((call) => String(call[0])).join('');
|
||||
const normalizedOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
expect(normalizedOutput).toContain('[over][implement](1/5)(1) [INFO] [1/5] implement (coder)');
|
||||
expect(normalizedOutput).toContain('[over][implement](2/5)(2) [INFO] [2/5] implement (coder)');
|
||||
} finally {
|
||||
stdoutSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail fast when taskPrefix is provided without taskColorIndex', async () => {
|
||||
await expect(
|
||||
executePiece(makeConfig(), 'task', '/tmp/project', {
|
||||
projectCwd: '/tmp/project',
|
||||
taskPrefix: 'override-persona-provider',
|
||||
})
|
||||
).rejects.toThrow('taskPrefix and taskColorIndex must be provided together');
|
||||
});
|
||||
});
|
||||
|
||||
@ -563,3 +563,215 @@ movements:
|
||||
expect(result.movementPreviews[0].personaDisplayName).toBe('Custom Agent Name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPieceDescription interactiveMode field', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'takt-test-interactive-mode-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should return interactiveMode when piece defines interactive_mode', () => {
|
||||
const pieceYaml = `name: test-mode
|
||||
initial_movement: step1
|
||||
max_iterations: 1
|
||||
interactive_mode: quiet
|
||||
|
||||
movements:
|
||||
- name: step1
|
||||
persona: agent
|
||||
instruction: "Do something"
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, 'test-mode.yaml');
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.interactiveMode).toBe('quiet');
|
||||
});
|
||||
|
||||
it('should return undefined interactiveMode when piece omits interactive_mode', () => {
|
||||
const pieceYaml = `name: test-no-mode
|
||||
initial_movement: step1
|
||||
max_iterations: 1
|
||||
|
||||
movements:
|
||||
- name: step1
|
||||
persona: agent
|
||||
instruction: "Do something"
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, 'test-no-mode.yaml');
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.interactiveMode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return interactiveMode for each valid mode value', () => {
|
||||
for (const mode of ['assistant', 'persona', 'quiet', 'passthrough'] as const) {
|
||||
const pieceYaml = `name: test-${mode}
|
||||
initial_movement: step1
|
||||
max_iterations: 1
|
||||
interactive_mode: ${mode}
|
||||
|
||||
movements:
|
||||
- name: step1
|
||||
persona: agent
|
||||
instruction: "Do something"
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, `test-${mode}.yaml`);
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.interactiveMode).toBe(mode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPieceDescription firstMovement field', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'takt-test-first-movement-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should return firstMovement with inline persona content', () => {
|
||||
const pieceYaml = `name: test-first
|
||||
initial_movement: plan
|
||||
max_iterations: 1
|
||||
|
||||
movements:
|
||||
- name: plan
|
||||
persona: You are a planner.
|
||||
persona_name: Planner
|
||||
instruction: "Plan the task"
|
||||
allowed_tools:
|
||||
- Read
|
||||
- Glob
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, 'test-first.yaml');
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.firstMovement).toBeDefined();
|
||||
expect(result.firstMovement!.personaContent).toBe('You are a planner.');
|
||||
expect(result.firstMovement!.personaDisplayName).toBe('Planner');
|
||||
expect(result.firstMovement!.allowedTools).toEqual(['Read', 'Glob']);
|
||||
});
|
||||
|
||||
it('should return firstMovement with persona file content', () => {
|
||||
const personaContent = '# Expert Planner\nYou plan tasks with precision.';
|
||||
const personaPath = join(tempDir, 'planner-persona.md');
|
||||
writeFileSync(personaPath, personaContent);
|
||||
|
||||
const pieceYaml = `name: test-persona-file
|
||||
initial_movement: plan
|
||||
max_iterations: 1
|
||||
|
||||
personas:
|
||||
planner: ./planner-persona.md
|
||||
|
||||
movements:
|
||||
- name: plan
|
||||
persona: planner
|
||||
persona_name: Planner
|
||||
instruction: "Plan the task"
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, 'test-persona-file.yaml');
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.firstMovement).toBeDefined();
|
||||
expect(result.firstMovement!.personaContent).toBe(personaContent);
|
||||
});
|
||||
|
||||
it('should return undefined firstMovement when initialMovement not found', () => {
|
||||
const pieceYaml = `name: test-missing
|
||||
initial_movement: nonexistent
|
||||
max_iterations: 1
|
||||
|
||||
movements:
|
||||
- name: step1
|
||||
persona: agent
|
||||
instruction: "Do something"
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, 'test-missing.yaml');
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.firstMovement).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return empty allowedTools array when movement has no tools', () => {
|
||||
const pieceYaml = `name: test-no-tools
|
||||
initial_movement: step1
|
||||
max_iterations: 1
|
||||
|
||||
movements:
|
||||
- name: step1
|
||||
persona: agent
|
||||
persona_name: Agent
|
||||
instruction: "Do something"
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, 'test-no-tools.yaml');
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.firstMovement).toBeDefined();
|
||||
expect(result.firstMovement!.allowedTools).toEqual([]);
|
||||
});
|
||||
|
||||
it('should fallback to inline persona when personaPath is unreadable', () => {
|
||||
const personaPath = join(tempDir, 'unreadable.md');
|
||||
writeFileSync(personaPath, '# Persona');
|
||||
chmodSync(personaPath, 0o000);
|
||||
|
||||
const pieceYaml = `name: test-fallback
|
||||
initial_movement: step1
|
||||
max_iterations: 1
|
||||
|
||||
personas:
|
||||
myagent: ./unreadable.md
|
||||
|
||||
movements:
|
||||
- name: step1
|
||||
persona: myagent
|
||||
persona_name: Agent
|
||||
instruction: "Do something"
|
||||
`;
|
||||
|
||||
const piecePath = join(tempDir, 'test-fallback.yaml');
|
||||
writeFileSync(piecePath, pieceYaml);
|
||||
|
||||
try {
|
||||
const result = getPieceDescription(piecePath, tempDir);
|
||||
|
||||
expect(result.firstMovement).toBeDefined();
|
||||
// personaPath is unreadable, so fallback to empty (persona was resolved to a path)
|
||||
expect(result.firstMovement!.personaContent).toBe('');
|
||||
} finally {
|
||||
chmodSync(personaPath, 0o644);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,6 +14,7 @@ vi.mock('../infra/config/index.js', () => ({
|
||||
defaultPiece: 'default',
|
||||
logLevel: 'info',
|
||||
concurrency: 1,
|
||||
taskPollIntervalMs: 500,
|
||||
})),
|
||||
}));
|
||||
|
||||
@ -142,6 +143,7 @@ describe('runAllTasks concurrency', () => {
|
||||
defaultPiece: 'default',
|
||||
logLevel: 'info',
|
||||
concurrency: 1,
|
||||
taskPollIntervalMs: 500,
|
||||
});
|
||||
});
|
||||
|
||||
@ -182,6 +184,7 @@ describe('runAllTasks concurrency', () => {
|
||||
defaultPiece: 'default',
|
||||
logLevel: 'info',
|
||||
concurrency: 3,
|
||||
taskPollIntervalMs: 500,
|
||||
});
|
||||
});
|
||||
|
||||
@ -209,13 +212,25 @@ describe('runAllTasks concurrency', () => {
|
||||
.mockReturnValueOnce([task1, task2, task3])
|
||||
.mockReturnValueOnce([]);
|
||||
|
||||
// In parallel mode, task start messages go through TaskPrefixWriter → process.stdout.write
|
||||
const stdoutChunks: string[] = [];
|
||||
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
|
||||
stdoutChunks.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
|
||||
// When
|
||||
await runAllTasks('/project');
|
||||
writeSpy.mockRestore();
|
||||
|
||||
// Then: Task names displayed
|
||||
expect(mockInfo).toHaveBeenCalledWith('=== Task: task-1 ===');
|
||||
expect(mockInfo).toHaveBeenCalledWith('=== Task: task-2 ===');
|
||||
expect(mockInfo).toHaveBeenCalledWith('=== Task: task-3 ===');
|
||||
// Then: Task names displayed with prefix in stdout
|
||||
const allOutput = stdoutChunks.join('');
|
||||
expect(allOutput).toContain('[task]');
|
||||
expect(allOutput).toContain('=== Task: task-1 ===');
|
||||
expect(allOutput).toContain('[task]');
|
||||
expect(allOutput).toContain('=== Task: task-2 ===');
|
||||
expect(allOutput).toContain('[task]');
|
||||
expect(allOutput).toContain('=== Task: task-3 ===');
|
||||
expect(mockStatus).toHaveBeenCalledWith('Total', '3');
|
||||
});
|
||||
|
||||
@ -245,6 +260,7 @@ describe('runAllTasks concurrency', () => {
|
||||
defaultPiece: 'default',
|
||||
logLevel: 'info',
|
||||
concurrency: 1,
|
||||
taskPollIntervalMs: 500,
|
||||
});
|
||||
|
||||
const task1 = createTask('task-1');
|
||||
@ -277,6 +293,7 @@ describe('runAllTasks concurrency', () => {
|
||||
defaultPiece: 'default',
|
||||
logLevel: 'info',
|
||||
concurrency: 3,
|
||||
taskPollIntervalMs: 500,
|
||||
});
|
||||
// Return a valid piece config so executeTask reaches executePiece
|
||||
mockLoadPieceByIdentifier.mockReturnValue(fakePieceConfig as never);
|
||||
@ -323,6 +340,7 @@ describe('runAllTasks concurrency', () => {
|
||||
defaultPiece: 'default',
|
||||
logLevel: 'info',
|
||||
concurrency: 2,
|
||||
taskPollIntervalMs: 500,
|
||||
});
|
||||
|
||||
const task1 = createTask('fast');
|
||||
@ -412,6 +430,7 @@ describe('runAllTasks concurrency', () => {
|
||||
defaultPiece: 'default',
|
||||
logLevel: 'info',
|
||||
concurrency: 1,
|
||||
taskPollIntervalMs: 500,
|
||||
});
|
||||
|
||||
const task1 = createTask('sequential-task');
|
||||
|
||||
203
src/__tests__/task-prefix-writer.test.ts
Normal file
203
src/__tests__/task-prefix-writer.test.ts
Normal file
@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Tests for TaskPrefixWriter
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TaskPrefixWriter } from '../shared/ui/TaskPrefixWriter.js';
|
||||
|
||||
describe('TaskPrefixWriter', () => {
|
||||
let output: string[];
|
||||
let writeFn: (text: string) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
output = [];
|
||||
writeFn = (text: string) => output.push(text);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should cycle colors for different colorIndex values', () => {
|
||||
const writer0 = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
const writer4 = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 4, writeFn });
|
||||
|
||||
writer0.writeLine('hello');
|
||||
writer4.writeLine('hello');
|
||||
|
||||
// Both index 0 and 4 should use cyan (\x1b[36m)
|
||||
expect(output[0]).toContain('\x1b[36m');
|
||||
expect(output[1]).toContain('\x1b[36m');
|
||||
});
|
||||
|
||||
it('should assign correct colors in order', () => {
|
||||
const writers = [0, 1, 2, 3].map(
|
||||
(i) => new TaskPrefixWriter({ taskName: `t${i}`, colorIndex: i, writeFn }),
|
||||
);
|
||||
|
||||
writers.forEach((w) => w.writeLine('x'));
|
||||
|
||||
expect(output[0]).toContain('\x1b[36m'); // cyan
|
||||
expect(output[1]).toContain('\x1b[33m'); // yellow
|
||||
expect(output[2]).toContain('\x1b[35m'); // magenta
|
||||
expect(output[3]).toContain('\x1b[32m'); // green
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeLine', () => {
|
||||
it('should output single line with truncated task prefix', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeLine('Hello World');
|
||||
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toContain('[my-t]');
|
||||
expect(output[0]).toContain('Hello World');
|
||||
expect(output[0]).toMatch(/\n$/);
|
||||
});
|
||||
|
||||
it('should output empty line as bare newline', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeLine('');
|
||||
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toBe('\n');
|
||||
});
|
||||
|
||||
it('should split multi-line text and prefix each non-empty line', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeLine('Line 1\nLine 2\n\nLine 4');
|
||||
|
||||
expect(output).toHaveLength(4);
|
||||
expect(output[0]).toContain('Line 1');
|
||||
expect(output[1]).toContain('Line 2');
|
||||
expect(output[2]).toBe('\n'); // empty line
|
||||
expect(output[3]).toContain('Line 4');
|
||||
});
|
||||
|
||||
it('should strip ANSI codes from input text', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'my-task', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeLine('\x1b[31mRed Text\x1b[0m');
|
||||
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toContain('Red Text');
|
||||
expect(output[0]).not.toContain('\x1b[31m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeChunk (line buffering)', () => {
|
||||
it('should buffer partial line and output on newline', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('Hello');
|
||||
expect(output).toHaveLength(0);
|
||||
|
||||
writer.writeChunk(' World\n');
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toContain('[task]');
|
||||
expect(output[0]).toContain('Hello World');
|
||||
});
|
||||
|
||||
it('should handle multiple lines in single chunk', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('Line 1\nLine 2\n');
|
||||
|
||||
expect(output).toHaveLength(2);
|
||||
expect(output[0]).toContain('Line 1');
|
||||
expect(output[1]).toContain('Line 2');
|
||||
});
|
||||
|
||||
it('should output empty line without prefix', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('Hello\n\nWorld\n');
|
||||
|
||||
expect(output).toHaveLength(3);
|
||||
expect(output[0]).toContain('Hello');
|
||||
expect(output[1]).toBe('\n');
|
||||
expect(output[2]).toContain('World');
|
||||
});
|
||||
|
||||
it('should keep trailing partial in buffer', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('Complete\nPartial');
|
||||
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toContain('Complete');
|
||||
|
||||
writer.flush();
|
||||
expect(output).toHaveLength(2);
|
||||
expect(output[1]).toContain('Partial');
|
||||
});
|
||||
|
||||
it('should strip ANSI codes from streamed chunks', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('\x1b[31mHello');
|
||||
writer.writeChunk(' World\x1b[0m\n');
|
||||
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toContain('Hello World');
|
||||
expect(output[0]).not.toContain('\x1b[31m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flush', () => {
|
||||
it('should output remaining buffered content with prefix', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('partial content');
|
||||
expect(output).toHaveLength(0);
|
||||
|
||||
writer.flush();
|
||||
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toContain('[task]');
|
||||
expect(output[0]).toContain('partial content');
|
||||
expect(output[0]).toMatch(/\n$/);
|
||||
});
|
||||
|
||||
it('should not output anything when buffer is empty', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('complete line\n');
|
||||
output.length = 0;
|
||||
|
||||
writer.flush();
|
||||
expect(output).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should clear buffer after flush', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'task-a', colorIndex: 0, writeFn });
|
||||
|
||||
writer.writeChunk('content');
|
||||
writer.flush();
|
||||
output.length = 0;
|
||||
|
||||
writer.flush();
|
||||
expect(output).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMovementContext', () => {
|
||||
it('should include movement context in prefix after context update', () => {
|
||||
const writer = new TaskPrefixWriter({ taskName: 'override-persona-provider', colorIndex: 0, writeFn });
|
||||
|
||||
writer.setMovementContext({
|
||||
movementName: 'implement',
|
||||
iteration: 4,
|
||||
maxIterations: 30,
|
||||
movementIteration: 2,
|
||||
});
|
||||
writer.writeLine('content');
|
||||
|
||||
expect(output).toHaveLength(1);
|
||||
expect(output[0]).toContain('[over]');
|
||||
expect(output[0]).toContain('[implement](4/30)(2)');
|
||||
expect(output[0]).toContain('content');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@ -23,6 +23,15 @@ vi.mock('../shared/i18n/index.js', () => ({
|
||||
getLabel: vi.fn((key: string) => key),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/utils/index.js', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockExecuteAndCompleteTask = vi.fn();
|
||||
|
||||
vi.mock('../features/tasks/execute/taskExecution.js', () => ({
|
||||
@ -34,6 +43,8 @@ import { info } from '../shared/ui/index.js';
|
||||
|
||||
const mockInfo = vi.mocked(info);
|
||||
|
||||
const TEST_POLL_INTERVAL_MS = 50;
|
||||
|
||||
function createTask(name: string): TaskInfo {
|
||||
return {
|
||||
name,
|
||||
@ -68,7 +79,7 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When
|
||||
const result = await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default');
|
||||
const result = await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({ success: 2, fail: 0 });
|
||||
@ -85,23 +96,32 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When
|
||||
const result = await runWithWorkerPool(runner as never, tasks, 3, '/cwd', 'default');
|
||||
const result = await runWithWorkerPool(runner as never, tasks, 3, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({ success: 2, fail: 1 });
|
||||
});
|
||||
|
||||
it('should display task name for each task', async () => {
|
||||
it('should display task name for each task via prefix writer in parallel mode', async () => {
|
||||
// Given
|
||||
const tasks = [createTask('alpha'), createTask('beta')];
|
||||
const runner = createMockTaskRunner([]);
|
||||
const stdoutChunks: string[] = [];
|
||||
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation((chunk: unknown) => {
|
||||
stdoutChunks.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
|
||||
// When
|
||||
await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default');
|
||||
await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then
|
||||
expect(mockInfo).toHaveBeenCalledWith('=== Task: alpha ===');
|
||||
expect(mockInfo).toHaveBeenCalledWith('=== Task: beta ===');
|
||||
// Then: Task names appear in prefixed stdout output
|
||||
writeSpy.mockRestore();
|
||||
const allOutput = stdoutChunks.join('');
|
||||
expect(allOutput).toContain('[alph]');
|
||||
expect(allOutput).toContain('=== Task: alpha ===');
|
||||
expect(allOutput).toContain('[beta]');
|
||||
expect(allOutput).toContain('=== Task: beta ===');
|
||||
});
|
||||
|
||||
it('should pass taskPrefix for parallel execution (concurrency > 1)', async () => {
|
||||
@ -110,7 +130,7 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When
|
||||
await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default');
|
||||
await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then
|
||||
expect(mockExecuteAndCompleteTask).toHaveBeenCalledTimes(1);
|
||||
@ -118,6 +138,7 @@ describe('runWithWorkerPool', () => {
|
||||
expect(parallelOpts).toEqual({
|
||||
abortSignal: expect.any(AbortSignal),
|
||||
taskPrefix: 'my-task',
|
||||
taskColorIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@ -127,7 +148,7 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When
|
||||
await runWithWorkerPool(runner as never, tasks, 1, '/cwd', 'default');
|
||||
await runWithWorkerPool(runner as never, tasks, 1, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then
|
||||
expect(mockExecuteAndCompleteTask).toHaveBeenCalledTimes(1);
|
||||
@ -135,6 +156,7 @@ describe('runWithWorkerPool', () => {
|
||||
expect(parallelOpts).toEqual({
|
||||
abortSignal: undefined,
|
||||
taskPrefix: undefined,
|
||||
taskColorIndex: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@ -145,7 +167,7 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([[task2]]);
|
||||
|
||||
// When
|
||||
await runWithWorkerPool(runner as never, [task1], 2, '/cwd', 'default');
|
||||
await runWithWorkerPool(runner as never, [task1], 2, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then
|
||||
expect(mockExecuteAndCompleteTask).toHaveBeenCalledTimes(2);
|
||||
@ -173,7 +195,7 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When
|
||||
await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default');
|
||||
await runWithWorkerPool(runner as never, tasks, 2, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then: Never exceeded concurrency of 2
|
||||
expect(maxActive).toBeLessThanOrEqual(2);
|
||||
@ -192,7 +214,7 @@ describe('runWithWorkerPool', () => {
|
||||
});
|
||||
|
||||
// When
|
||||
await runWithWorkerPool(runner as never, tasks, 3, '/cwd', 'default');
|
||||
await runWithWorkerPool(runner as never, tasks, 3, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then: All tasks received the same AbortSignal
|
||||
expect(receivedSignals).toHaveLength(3);
|
||||
@ -208,7 +230,7 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When
|
||||
const result = await runWithWorkerPool(runner as never, [], 2, '/cwd', 'default');
|
||||
const result = await runWithWorkerPool(runner as never, [], 2, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({ success: 0, fail: 0 });
|
||||
@ -222,9 +244,107 @@ describe('runWithWorkerPool', () => {
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When
|
||||
const result = await runWithWorkerPool(runner as never, tasks, 1, '/cwd', 'default');
|
||||
const result = await runWithWorkerPool(runner as never, tasks, 1, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS);
|
||||
|
||||
// Then: Treated as failure
|
||||
expect(result).toEqual({ success: 0, fail: 1 });
|
||||
});
|
||||
|
||||
describe('polling', () => {
|
||||
it('should pick up tasks added during execution via polling', async () => {
|
||||
// Given: 1 initial task running with concurrency=2, a second task appears via poll
|
||||
const task1 = createTask('initial');
|
||||
const task2 = createTask('added-later');
|
||||
|
||||
const executionOrder: string[] = [];
|
||||
|
||||
mockExecuteAndCompleteTask.mockImplementation((task: TaskInfo) => {
|
||||
executionOrder.push(`start:${task.name}`);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
executionOrder.push(`end:${task.name}`);
|
||||
resolve(true);
|
||||
}, 80);
|
||||
});
|
||||
});
|
||||
|
||||
let claimCallCount = 0;
|
||||
const runner = {
|
||||
getNextTask: vi.fn(() => null),
|
||||
claimNextTasks: vi.fn(() => {
|
||||
claimCallCount++;
|
||||
// Return the new task on the second call (triggered by polling)
|
||||
if (claimCallCount === 2) return [task2];
|
||||
return [];
|
||||
}),
|
||||
completeTask: vi.fn(),
|
||||
failTask: vi.fn(),
|
||||
};
|
||||
|
||||
// When: pollIntervalMs=30 so polling fires before task1 completes (80ms)
|
||||
const result = await runWithWorkerPool(
|
||||
runner as never, [task1], 2, '/cwd', 'default', undefined, 30,
|
||||
);
|
||||
|
||||
// Then: Both tasks were executed
|
||||
expect(result).toEqual({ success: 2, fail: 0 });
|
||||
expect(executionOrder).toContain('start:initial');
|
||||
expect(executionOrder).toContain('start:added-later');
|
||||
// task2 started before task1 ended (picked up by polling, not by task completion)
|
||||
const task2Start = executionOrder.indexOf('start:added-later');
|
||||
const task1End = executionOrder.indexOf('end:initial');
|
||||
expect(task2Start).toBeLessThan(task1End);
|
||||
});
|
||||
|
||||
it('should work correctly with concurrency=1 (sequential behavior preserved)', async () => {
|
||||
// Given: concurrency=1, tasks claimed sequentially
|
||||
const task1 = createTask('seq-1');
|
||||
const task2 = createTask('seq-2');
|
||||
|
||||
const executionOrder: string[] = [];
|
||||
mockExecuteAndCompleteTask.mockImplementation((task: TaskInfo) => {
|
||||
executionOrder.push(`start:${task.name}`);
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
executionOrder.push(`end:${task.name}`);
|
||||
resolve(true);
|
||||
}, 20);
|
||||
});
|
||||
});
|
||||
|
||||
const runner = createMockTaskRunner([[task2]]);
|
||||
|
||||
// When
|
||||
const result = await runWithWorkerPool(
|
||||
runner as never, [task1], 1, '/cwd', 'default', undefined, TEST_POLL_INTERVAL_MS,
|
||||
);
|
||||
|
||||
// Then: Tasks executed sequentially — task2 starts after task1 ends
|
||||
expect(result).toEqual({ success: 2, fail: 0 });
|
||||
const task2Start = executionOrder.indexOf('start:seq-2');
|
||||
const task1End = executionOrder.indexOf('end:seq-1');
|
||||
expect(task2Start).toBeGreaterThan(task1End);
|
||||
});
|
||||
|
||||
it('should not leak poll timer when task completes before poll fires', async () => {
|
||||
// Given: A task that completes in 200ms, poll interval is 5000ms
|
||||
const task1 = createTask('fast-task');
|
||||
|
||||
mockExecuteAndCompleteTask.mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(true), 200);
|
||||
});
|
||||
});
|
||||
|
||||
const runner = createMockTaskRunner([]);
|
||||
|
||||
// When: Task completes before poll timer fires; cancel() cleans up timer
|
||||
const result = await runWithWorkerPool(
|
||||
runner as never, [task1], 1, '/cwd', 'default', undefined, 5000,
|
||||
);
|
||||
|
||||
// Then: Result is returned without hanging (timer was cleaned up by cancel())
|
||||
expect(result).toEqual({ success: 1, fail: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -100,6 +100,7 @@ export class AgentRunner {
|
||||
): ProviderCallOptions {
|
||||
return {
|
||||
cwd: options.cwd,
|
||||
abortSignal: options.abortSignal,
|
||||
sessionId: options.sessionId,
|
||||
allowedTools: options.allowedTools ?? agentConfig?.allowedTools,
|
||||
mcpServers: options.mcpServers,
|
||||
|
||||
@ -10,6 +10,7 @@ export type { StreamCallback };
|
||||
/** Common options for running agents */
|
||||
export interface RunAgentOptions {
|
||||
cwd: string;
|
||||
abortSignal?: AbortSignal;
|
||||
sessionId?: string;
|
||||
model?: string;
|
||||
provider?: 'claude' | 'codex' | 'mock';
|
||||
|
||||
@ -7,10 +7,19 @@
|
||||
|
||||
import { info, error } from '../../shared/ui/index.js';
|
||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||
import { getLabel } from '../../shared/i18n/index.js';
|
||||
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
|
||||
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||
import { executePipeline } from '../../features/pipeline/index.js';
|
||||
import { interactiveMode } from '../../features/interactive/index.js';
|
||||
import {
|
||||
interactiveMode,
|
||||
selectInteractiveMode,
|
||||
passthroughMode,
|
||||
quietMode,
|
||||
personaMode,
|
||||
resolveLanguage,
|
||||
type InteractiveModeResult,
|
||||
} from '../../features/interactive/index.js';
|
||||
import { getPieceDescription, loadGlobalConfig } from '../../infra/config/index.js';
|
||||
import { DEFAULT_PIECE_NAME } from '../../shared/constants.js';
|
||||
import { program, resolvedCwd, pipelineMode } from './program.js';
|
||||
@ -118,16 +127,57 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
||||
}
|
||||
|
||||
// All paths below go through interactive mode
|
||||
const globalConfig = loadGlobalConfig();
|
||||
const lang = resolveLanguage(globalConfig.language);
|
||||
|
||||
const pieceId = await determinePiece(resolvedCwd, selectOptions.piece);
|
||||
if (pieceId === null) {
|
||||
info('Cancelled');
|
||||
info(getLabel('interactive.ui.cancelled', lang));
|
||||
return;
|
||||
}
|
||||
|
||||
const globalConfig = loadGlobalConfig();
|
||||
const previewCount = globalConfig.interactivePreviewMovements;
|
||||
const pieceContext = getPieceDescription(pieceId, resolvedCwd, previewCount);
|
||||
const result = await interactiveMode(resolvedCwd, initialInput, pieceContext);
|
||||
const pieceDesc = getPieceDescription(pieceId, resolvedCwd, previewCount);
|
||||
|
||||
// Mode selection after piece selection
|
||||
const selectedMode = await selectInteractiveMode(lang, pieceDesc.interactiveMode);
|
||||
if (selectedMode === null) {
|
||||
info(getLabel('interactive.ui.cancelled', lang));
|
||||
return;
|
||||
}
|
||||
|
||||
const pieceContext = {
|
||||
name: pieceDesc.name,
|
||||
description: pieceDesc.description,
|
||||
pieceStructure: pieceDesc.pieceStructure,
|
||||
movementPreviews: pieceDesc.movementPreviews,
|
||||
};
|
||||
|
||||
let result: InteractiveModeResult;
|
||||
|
||||
switch (selectedMode) {
|
||||
case 'assistant':
|
||||
result = await interactiveMode(resolvedCwd, initialInput, pieceContext);
|
||||
break;
|
||||
|
||||
case 'passthrough':
|
||||
result = await passthroughMode(lang, initialInput);
|
||||
break;
|
||||
|
||||
case 'quiet':
|
||||
result = await quietMode(resolvedCwd, initialInput, pieceContext);
|
||||
break;
|
||||
|
||||
case 'persona': {
|
||||
if (!pieceDesc.firstMovement) {
|
||||
info(getLabel('interactive.ui.personaFallback', lang));
|
||||
result = await interactiveMode(resolvedCwd, initialInput, pieceContext);
|
||||
} else {
|
||||
result = await personaMode(resolvedCwd, pieceDesc.firstMovement, initialInput, pieceContext);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (result.action) {
|
||||
case 'execute':
|
||||
|
||||
@ -61,6 +61,8 @@ export interface GlobalConfig {
|
||||
bookmarksFile?: string;
|
||||
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
|
||||
pieceCategoriesFile?: string;
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
personaProviders?: Record<string, 'claude' | 'codex' | 'mock'>;
|
||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||
branchNameStrategy?: 'romaji' | 'ai';
|
||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||
@ -71,6 +73,8 @@ export interface GlobalConfig {
|
||||
interactivePreviewMovements?: number;
|
||||
/** Number of tasks to run concurrently in takt run (default: 1 = sequential) */
|
||||
concurrency: number;
|
||||
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
|
||||
taskPollIntervalMs: number;
|
||||
}
|
||||
|
||||
/** Project-level configuration */
|
||||
|
||||
@ -35,6 +35,9 @@ export * from './config.js';
|
||||
// Re-export from schemas.ts
|
||||
export * from './schemas.js';
|
||||
|
||||
// Re-export from interactive-mode.ts
|
||||
export { INTERACTIVE_MODES, DEFAULT_INTERACTIVE_MODE, type InteractiveMode } from './interactive-mode.js';
|
||||
|
||||
// Re-export from session.ts (functions only, not types)
|
||||
export {
|
||||
createSessionState,
|
||||
|
||||
18
src/core/models/interactive-mode.ts
Normal file
18
src/core/models/interactive-mode.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Interactive mode variants for conversational task input.
|
||||
*
|
||||
* Defines the four modes available when using interactive mode:
|
||||
* - assistant: Asks clarifying questions before generating instructions (default)
|
||||
* - persona: Uses the first movement's persona for conversation
|
||||
* - quiet: Generates instructions without asking questions (best-effort)
|
||||
* - passthrough: Passes user input directly as task text
|
||||
*/
|
||||
|
||||
/** Available interactive mode variants */
|
||||
export const INTERACTIVE_MODES = ['assistant', 'persona', 'quiet', 'passthrough'] as const;
|
||||
|
||||
/** Interactive mode type */
|
||||
export type InteractiveMode = typeof INTERACTIVE_MODES[number];
|
||||
|
||||
/** Default interactive mode */
|
||||
export const DEFAULT_INTERACTIVE_MODE: InteractiveMode = 'assistant';
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
import type { PermissionMode } from './status.js';
|
||||
import type { AgentResponse } from './response.js';
|
||||
import type { InteractiveMode } from './interactive-mode.js';
|
||||
|
||||
/** Rule-based transition configuration (unified format) */
|
||||
export interface PieceRule {
|
||||
@ -184,6 +185,8 @@ export interface PieceConfig {
|
||||
* instead of prompting the user interactively.
|
||||
*/
|
||||
answerAgent?: string;
|
||||
/** Default interactive mode for this piece (overrides user default) */
|
||||
interactiveMode?: InteractiveMode;
|
||||
}
|
||||
|
||||
/** Runtime state of a piece execution */
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
import { z } from 'zod/v4';
|
||||
import { DEFAULT_LANGUAGE } from '../../shared/constants.js';
|
||||
import { McpServersSchema } from './mcp-schemas.js';
|
||||
import { INTERACTIVE_MODES } from './interactive-mode.js';
|
||||
|
||||
export { McpServerConfigSchema, McpServersSchema } from './mcp-schemas.js';
|
||||
|
||||
@ -218,6 +219,9 @@ export const LoopMonitorSchema = z.object({
|
||||
judge: LoopMonitorJudgeSchema,
|
||||
});
|
||||
|
||||
/** Interactive mode schema for piece-level default */
|
||||
export const InteractiveModeSchema = z.enum(INTERACTIVE_MODES);
|
||||
|
||||
/** Piece configuration schema - raw YAML format */
|
||||
export const PieceConfigRawSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
@ -237,6 +241,8 @@ export const PieceConfigRawSchema = z.object({
|
||||
max_iterations: z.number().int().positive().optional().default(10),
|
||||
loop_monitors: z.array(LoopMonitorSchema).optional(),
|
||||
answer_agent: z.string().optional(),
|
||||
/** Default interactive mode for this piece (overrides user default) */
|
||||
interactive_mode: InteractiveModeSchema.optional(),
|
||||
});
|
||||
|
||||
/** Custom agent configuration schema */
|
||||
@ -312,6 +318,8 @@ export const GlobalConfigSchema = z.object({
|
||||
bookmarks_file: z.string().optional(),
|
||||
/** Path to piece categories file (default: ~/.takt/preferences/piece-categories.yaml) */
|
||||
piece_categories_file: z.string().optional(),
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
persona_providers: z.record(z.string(), z.enum(['claude', 'codex', 'mock'])).optional(),
|
||||
/** Branch name generation strategy: 'romaji' (fast, default) or 'ai' (slow) */
|
||||
branch_name_strategy: z.enum(['romaji', 'ai']).optional(),
|
||||
/** Prevent macOS idle sleep during takt execution using caffeinate (default: false) */
|
||||
@ -322,6 +330,8 @@ export const GlobalConfigSchema = z.object({
|
||||
interactive_preview_movements: z.number().int().min(0).max(10).optional().default(3),
|
||||
/** Number of tasks to run concurrently in takt run (default: 1 = sequential, max: 10) */
|
||||
concurrency: z.number().int().min(1).max(10).optional().default(1),
|
||||
/** Polling interval in ms for picking up new tasks during takt run (default: 500, range: 100-5000) */
|
||||
task_poll_interval_ms: z.number().int().min(100).max(5000).optional().default(500),
|
||||
});
|
||||
|
||||
/** Project config schema */
|
||||
|
||||
@ -33,8 +33,9 @@ export class OptionsBuilder {
|
||||
|
||||
return {
|
||||
cwd: this.getCwd(),
|
||||
abortSignal: this.engineOptions.abortSignal,
|
||||
personaPath: step.personaPath,
|
||||
provider: step.provider ?? this.engineOptions.provider,
|
||||
provider: step.provider ?? this.engineOptions.personaProviders?.[step.personaDisplayName] ?? this.engineOptions.provider,
|
||||
model: step.model ?? this.engineOptions.model,
|
||||
permissionMode: step.permissionMode,
|
||||
language: this.getLanguage(),
|
||||
|
||||
@ -20,6 +20,7 @@ import { buildSessionKey } from '../session-key.js';
|
||||
import type { OptionsBuilder } from './OptionsBuilder.js';
|
||||
import type { MovementExecutor } from './MovementExecutor.js';
|
||||
import type { PieceEngineOptions, PhaseName } from '../types.js';
|
||||
import type { ParallelLoggerOptions } from './parallel-logger.js';
|
||||
|
||||
const log = createLogger('parallel-runner');
|
||||
|
||||
@ -69,14 +70,7 @@ export class ParallelRunner {
|
||||
|
||||
// Create parallel logger for prefixed output (only when streaming is enabled)
|
||||
const parallelLogger = this.deps.engineOptions.onStream
|
||||
? new ParallelLogger({
|
||||
subMovementNames: subMovements.map((s) => s.name),
|
||||
parentOnStream: this.deps.engineOptions.onStream,
|
||||
progressInfo: {
|
||||
iteration: state.iteration,
|
||||
maxIterations,
|
||||
},
|
||||
})
|
||||
? new ParallelLogger(this.buildParallelLoggerOptions(step.name, movementIteration, subMovements.map((s) => s.name), state.iteration, maxIterations))
|
||||
: undefined;
|
||||
|
||||
const ruleCtx = {
|
||||
@ -202,4 +196,33 @@ export class ParallelRunner {
|
||||
return { response: aggregatedResponse, instruction: aggregatedInstruction };
|
||||
}
|
||||
|
||||
private buildParallelLoggerOptions(
|
||||
movementName: string,
|
||||
movementIteration: number,
|
||||
subMovementNames: string[],
|
||||
iteration: number,
|
||||
maxIterations: number,
|
||||
): ParallelLoggerOptions {
|
||||
const options: ParallelLoggerOptions = {
|
||||
subMovementNames,
|
||||
parentOnStream: this.deps.engineOptions.onStream,
|
||||
progressInfo: {
|
||||
iteration,
|
||||
maxIterations,
|
||||
},
|
||||
};
|
||||
|
||||
if (this.deps.engineOptions.taskPrefix != null && this.deps.engineOptions.taskColorIndex != null) {
|
||||
return {
|
||||
...options,
|
||||
taskLabel: this.deps.engineOptions.taskPrefix,
|
||||
taskColorIndex: this.deps.engineOptions.taskColorIndex,
|
||||
parentMovementName: movementName,
|
||||
movementIteration,
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -69,6 +69,7 @@ export class PieceEngine extends EventEmitter {
|
||||
|
||||
constructor(config: PieceConfig, cwd: string, task: string, options: PieceEngineOptions) {
|
||||
super();
|
||||
this.assertTaskPrefixPair(options.taskPrefix, options.taskColorIndex);
|
||||
this.config = config;
|
||||
this.projectCwd = options.projectCwd;
|
||||
this.cwd = cwd;
|
||||
@ -146,6 +147,14 @@ export class PieceEngine extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
private assertTaskPrefixPair(taskPrefix: string | undefined, taskColorIndex: number | undefined): void {
|
||||
const hasTaskPrefix = taskPrefix != null;
|
||||
const hasTaskColorIndex = taskColorIndex != null;
|
||||
if (hasTaskPrefix !== hasTaskColorIndex) {
|
||||
throw new Error('taskPrefix and taskColorIndex must be provided together');
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure report directory exists (in cwd, which is clone dir in worktree mode) */
|
||||
private ensureReportDirExists(): void {
|
||||
const reportDirPath = join(this.cwd, this.reportDir);
|
||||
|
||||
@ -30,6 +30,14 @@ export interface ParallelLoggerOptions {
|
||||
writeFn?: (text: string) => void;
|
||||
/** Progress information for display */
|
||||
progressInfo?: ParallelProgressInfo;
|
||||
/** Task label for rich parallel prefix display */
|
||||
taskLabel?: string;
|
||||
/** Task color index for rich parallel prefix display */
|
||||
taskColorIndex?: number;
|
||||
/** Parent movement name for rich parallel prefix display */
|
||||
parentMovementName?: string;
|
||||
/** Parent movement iteration count for rich parallel prefix display */
|
||||
movementIteration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,6 +55,10 @@ export class ParallelLogger {
|
||||
private readonly writeFn: (text: string) => void;
|
||||
private readonly progressInfo?: ParallelProgressInfo;
|
||||
private readonly totalSubMovements: number;
|
||||
private readonly taskLabel?: string;
|
||||
private readonly taskColorIndex?: number;
|
||||
private readonly parentMovementName?: string;
|
||||
private readonly movementIteration?: number;
|
||||
|
||||
constructor(options: ParallelLoggerOptions) {
|
||||
this.maxNameLength = Math.max(...options.subMovementNames.map((n) => n.length));
|
||||
@ -54,6 +66,10 @@ export class ParallelLogger {
|
||||
this.writeFn = options.writeFn ?? ((text: string) => process.stdout.write(text));
|
||||
this.progressInfo = options.progressInfo;
|
||||
this.totalSubMovements = options.subMovementNames.length;
|
||||
this.taskLabel = options.taskLabel ? options.taskLabel.slice(0, 4) : undefined;
|
||||
this.taskColorIndex = options.taskColorIndex;
|
||||
this.parentMovementName = options.parentMovementName;
|
||||
this.movementIteration = options.movementIteration;
|
||||
|
||||
for (const name of options.subMovementNames) {
|
||||
this.lineBuffers.set(name, '');
|
||||
@ -65,6 +81,12 @@ export class ParallelLogger {
|
||||
* Format: `\x1b[COLORm[name](iteration/max) step index/total\x1b[0m` + padding spaces
|
||||
*/
|
||||
buildPrefix(name: string, index: number): string {
|
||||
if (this.taskLabel && this.parentMovementName && this.progressInfo && this.movementIteration != null && this.taskColorIndex != null) {
|
||||
const taskColor = COLORS[this.taskColorIndex % COLORS.length];
|
||||
const { iteration, maxIterations } = this.progressInfo;
|
||||
return `${taskColor}[${this.taskLabel}]${RESET}[${this.parentMovementName}][${name}](${iteration}/${maxIterations})(${this.movementIteration}) `;
|
||||
}
|
||||
|
||||
const color = COLORS[index % COLORS.length];
|
||||
const padding = ' '.repeat(this.maxNameLength - name.length);
|
||||
|
||||
|
||||
@ -153,6 +153,7 @@ export type IterationLimitCallback = (request: IterationLimitRequest) => Promise
|
||||
|
||||
/** Options for piece engine */
|
||||
export interface PieceEngineOptions {
|
||||
abortSignal?: AbortSignal;
|
||||
/** Callback for streaming real-time output */
|
||||
onStream?: StreamCallback;
|
||||
/** Callback for requesting user input when an agent is blocked */
|
||||
@ -177,6 +178,8 @@ export interface PieceEngineOptions {
|
||||
language?: Language;
|
||||
provider?: ProviderType;
|
||||
model?: string;
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
personaProviders?: Record<string, ProviderType>;
|
||||
/** Enable interactive-only rules and user-input transitions */
|
||||
interactive?: boolean;
|
||||
/** Rule tag index detector (required for rules evaluation) */
|
||||
@ -187,6 +190,10 @@ export interface PieceEngineOptions {
|
||||
startMovement?: string;
|
||||
/** Retry note explaining why task is being retried */
|
||||
retryNote?: string;
|
||||
/** Task name prefix for parallel task execution output */
|
||||
taskPrefix?: string;
|
||||
/** Color index for task prefix (cycled across tasks) */
|
||||
taskColorIndex?: number;
|
||||
}
|
||||
|
||||
/** Loop detection result */
|
||||
|
||||
300
src/features/interactive/conversationLoop.ts
Normal file
300
src/features/interactive/conversationLoop.ts
Normal file
@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Shared conversation loop for interactive modes (assistant & persona).
|
||||
*
|
||||
* Extracts the common patterns:
|
||||
* - Provider/session initialization
|
||||
* - AI call with retry on stale session
|
||||
* - Session state display/clear
|
||||
* - Conversation loop (slash commands, AI messaging, /go summary)
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
loadGlobalConfig,
|
||||
loadPersonaSessions,
|
||||
updatePersonaSession,
|
||||
loadSessionState,
|
||||
clearSessionState,
|
||||
} from '../../infra/config/index.js';
|
||||
import { isQuietMode } from '../../shared/context.js';
|
||||
import { getProvider, type ProviderType } from '../../infra/providers/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
|
||||
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
|
||||
import { readMultilineInput } from './lineEditor.js';
|
||||
import {
|
||||
type PieceContext,
|
||||
type InteractiveModeResult,
|
||||
type InteractiveUIText,
|
||||
type ConversationMessage,
|
||||
resolveLanguage,
|
||||
buildSummaryPrompt,
|
||||
selectPostSummaryAction,
|
||||
formatSessionStatus,
|
||||
} from './interactive.js';
|
||||
|
||||
const log = createLogger('conversation-loop');
|
||||
|
||||
/** Result from a single AI call */
|
||||
export interface CallAIResult {
|
||||
content: string;
|
||||
sessionId?: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/** Initialized session context for conversation loops */
|
||||
export interface SessionContext {
|
||||
provider: ReturnType<typeof getProvider>;
|
||||
providerType: ProviderType;
|
||||
model: string | undefined;
|
||||
lang: 'en' | 'ja';
|
||||
personaName: string;
|
||||
sessionId: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize provider, session, and language for interactive conversation.
|
||||
*/
|
||||
export function initializeSession(cwd: string, personaName: string): SessionContext {
|
||||
const globalConfig = loadGlobalConfig();
|
||||
const lang = resolveLanguage(globalConfig.language);
|
||||
if (!globalConfig.provider) {
|
||||
throw new Error('Provider is not configured.');
|
||||
}
|
||||
const providerType = globalConfig.provider as ProviderType;
|
||||
const provider = getProvider(providerType);
|
||||
const model = globalConfig.model as string | undefined;
|
||||
const savedSessions = loadPersonaSessions(cwd, providerType);
|
||||
const sessionId: string | undefined = savedSessions[personaName];
|
||||
|
||||
return { provider, providerType, model, lang, personaName, sessionId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Display and clear previous session state if present.
|
||||
*/
|
||||
export function displayAndClearSessionState(cwd: string, lang: 'en' | 'ja'): void {
|
||||
const sessionState = loadSessionState(cwd);
|
||||
if (sessionState) {
|
||||
const statusLabel = formatSessionStatus(sessionState, lang);
|
||||
info(statusLabel);
|
||||
blankLine();
|
||||
clearSessionState(cwd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call AI with automatic retry on stale/invalid session.
|
||||
*
|
||||
* On session failure, clears sessionId and retries once without session.
|
||||
* Updates sessionId and persists it on success.
|
||||
*/
|
||||
export async function callAIWithRetry(
|
||||
prompt: string,
|
||||
systemPrompt: string,
|
||||
allowedTools: string[],
|
||||
cwd: string,
|
||||
ctx: SessionContext,
|
||||
): Promise<{ result: CallAIResult | null; sessionId: string | undefined }> {
|
||||
const display = new StreamDisplay('assistant', isQuietMode());
|
||||
let { sessionId } = ctx;
|
||||
|
||||
try {
|
||||
const agent = ctx.provider.setup({ name: ctx.personaName, systemPrompt });
|
||||
const response = await agent.call(prompt, {
|
||||
cwd,
|
||||
model: ctx.model,
|
||||
sessionId,
|
||||
allowedTools,
|
||||
onStream: display.createHandler(),
|
||||
});
|
||||
display.flush();
|
||||
const success = response.status !== 'blocked';
|
||||
|
||||
if (!success && sessionId) {
|
||||
log.info('Session invalid, retrying without session');
|
||||
sessionId = undefined;
|
||||
const retryDisplay = new StreamDisplay('assistant', isQuietMode());
|
||||
const retryAgent = ctx.provider.setup({ name: ctx.personaName, systemPrompt });
|
||||
const retry = await retryAgent.call(prompt, {
|
||||
cwd,
|
||||
model: ctx.model,
|
||||
sessionId: undefined,
|
||||
allowedTools,
|
||||
onStream: retryDisplay.createHandler(),
|
||||
});
|
||||
retryDisplay.flush();
|
||||
if (retry.sessionId) {
|
||||
sessionId = retry.sessionId;
|
||||
updatePersonaSession(cwd, ctx.personaName, sessionId, ctx.providerType);
|
||||
}
|
||||
return {
|
||||
result: { content: retry.content, sessionId: retry.sessionId, success: retry.status !== 'blocked' },
|
||||
sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.sessionId) {
|
||||
sessionId = response.sessionId;
|
||||
updatePersonaSession(cwd, ctx.personaName, sessionId, ctx.providerType);
|
||||
}
|
||||
return {
|
||||
result: { content: response.content, sessionId: response.sessionId, success },
|
||||
sessionId,
|
||||
};
|
||||
} catch (e) {
|
||||
const msg = getErrorMessage(e);
|
||||
log.error('AI call failed', { error: msg });
|
||||
error(msg);
|
||||
blankLine();
|
||||
return { result: null, sessionId };
|
||||
}
|
||||
}
|
||||
|
||||
/** Strategy for customizing conversation loop behavior */
|
||||
export interface ConversationStrategy {
|
||||
/** System prompt for AI calls */
|
||||
systemPrompt: string;
|
||||
/** Allowed tools for AI calls */
|
||||
allowedTools: string[];
|
||||
/** Transform user message before sending to AI (e.g., policy injection) */
|
||||
transformPrompt: (userMessage: string) => string;
|
||||
/** Intro message displayed at start */
|
||||
introMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the shared conversation loop.
|
||||
*
|
||||
* Handles: EOF, /play, /go (summary), /cancel, regular AI messaging.
|
||||
* The Strategy object controls system prompt, tool access, and prompt transformation.
|
||||
*/
|
||||
export async function runConversationLoop(
|
||||
cwd: string,
|
||||
ctx: SessionContext,
|
||||
strategy: ConversationStrategy,
|
||||
pieceContext: PieceContext | undefined,
|
||||
initialInput: string | undefined,
|
||||
): Promise<InteractiveModeResult> {
|
||||
const history: ConversationMessage[] = [];
|
||||
let sessionId = ctx.sessionId;
|
||||
const ui = getLabelObject<InteractiveUIText>('interactive.ui', ctx.lang);
|
||||
const conversationLabel = getLabel('interactive.conversationLabel', ctx.lang);
|
||||
const noTranscript = getLabel('interactive.noTranscript', ctx.lang);
|
||||
|
||||
info(strategy.introMessage);
|
||||
if (sessionId) {
|
||||
info(ui.resume);
|
||||
}
|
||||
blankLine();
|
||||
|
||||
/** Helper: call AI with current session and update session state */
|
||||
async function doCallAI(prompt: string, sysPrompt: string, tools: string[]): Promise<CallAIResult | null> {
|
||||
const { result, sessionId: newSessionId } = await callAIWithRetry(
|
||||
prompt, sysPrompt, tools, cwd, { ...ctx, sessionId },
|
||||
);
|
||||
sessionId = newSessionId;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (initialInput) {
|
||||
history.push({ role: 'user', content: initialInput });
|
||||
log.debug('Processing initial input', { initialInput, sessionId });
|
||||
|
||||
const promptWithTransform = strategy.transformPrompt(initialInput);
|
||||
const result = await doCallAI(promptWithTransform, strategy.systemPrompt, strategy.allowedTools);
|
||||
if (result) {
|
||||
if (!result.success) {
|
||||
error(result.content);
|
||||
blankLine();
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
history.push({ role: 'assistant', content: result.content });
|
||||
blankLine();
|
||||
} else {
|
||||
history.pop();
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const input = await readMultilineInput(chalk.green('> '));
|
||||
|
||||
if (input === null) {
|
||||
blankLine();
|
||||
info(ui.cancelled);
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('/play')) {
|
||||
const task = trimmed.slice(5).trim();
|
||||
if (!task) {
|
||||
info(ui.playNoTask);
|
||||
continue;
|
||||
}
|
||||
log.info('Play command', { task });
|
||||
return { action: 'execute', task };
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('/go')) {
|
||||
const userNote = trimmed.slice(3).trim();
|
||||
let summaryPrompt = buildSummaryPrompt(
|
||||
history, !!sessionId, ctx.lang, noTranscript, conversationLabel, pieceContext,
|
||||
);
|
||||
if (!summaryPrompt) {
|
||||
info(ui.noConversation);
|
||||
continue;
|
||||
}
|
||||
if (userNote) {
|
||||
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
|
||||
}
|
||||
const summaryResult = await doCallAI(summaryPrompt, summaryPrompt, strategy.allowedTools);
|
||||
if (!summaryResult) {
|
||||
info(ui.summarizeFailed);
|
||||
continue;
|
||||
}
|
||||
if (!summaryResult.success) {
|
||||
error(summaryResult.content);
|
||||
blankLine();
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
const task = summaryResult.content.trim();
|
||||
const selectedAction = await selectPostSummaryAction(task, ui.proposed, ui);
|
||||
if (selectedAction === 'continue' || selectedAction === null) {
|
||||
info(ui.continuePrompt);
|
||||
continue;
|
||||
}
|
||||
log.info('Conversation action selected', { action: selectedAction, messageCount: history.length });
|
||||
return { action: selectedAction, task };
|
||||
}
|
||||
|
||||
if (trimmed === '/cancel') {
|
||||
info(ui.cancelled);
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
history.push({ role: 'user', content: trimmed });
|
||||
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
||||
process.stdin.pause();
|
||||
|
||||
const promptWithTransform = strategy.transformPrompt(trimmed);
|
||||
const result = await doCallAI(promptWithTransform, strategy.systemPrompt, strategy.allowedTools);
|
||||
if (result) {
|
||||
if (!result.success) {
|
||||
error(result.content);
|
||||
blankLine();
|
||||
history.pop();
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
history.push({ role: 'assistant', content: result.content });
|
||||
blankLine();
|
||||
} else {
|
||||
history.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,17 @@
|
||||
|
||||
export {
|
||||
interactiveMode,
|
||||
resolveLanguage,
|
||||
buildSummaryPrompt,
|
||||
selectPostSummaryAction,
|
||||
formatMovementPreviews,
|
||||
formatSessionStatus,
|
||||
type PieceContext,
|
||||
type InteractiveModeResult,
|
||||
type InteractiveModeAction,
|
||||
} from './interactive.js';
|
||||
|
||||
export { selectInteractiveMode } from './modeSelection.js';
|
||||
export { passthroughMode } from './passthroughMode.js';
|
||||
export { quietMode } from './quietMode.js';
|
||||
export { personaMode } from './personaMode.js';
|
||||
|
||||
@ -10,29 +10,23 @@
|
||||
* /cancel - Cancel and exit
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import type { Language } from '../../core/models/index.js';
|
||||
import {
|
||||
loadGlobalConfig,
|
||||
loadPersonaSessions,
|
||||
updatePersonaSession,
|
||||
loadSessionState,
|
||||
clearSessionState,
|
||||
type SessionState,
|
||||
type MovementPreview,
|
||||
} from '../../infra/config/index.js';
|
||||
import { isQuietMode } from '../../shared/context.js';
|
||||
import { getProvider, type ProviderType } from '../../infra/providers/index.js';
|
||||
import { selectOption } from '../../shared/prompt/index.js';
|
||||
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
|
||||
import { info, error, blankLine, StreamDisplay } from '../../shared/ui/index.js';
|
||||
import { info, blankLine } from '../../shared/ui/index.js';
|
||||
import { loadTemplate } from '../../shared/prompts/index.js';
|
||||
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
|
||||
import { readMultilineInput } from './lineEditor.js';
|
||||
const log = createLogger('interactive');
|
||||
import {
|
||||
initializeSession,
|
||||
displayAndClearSessionState,
|
||||
runConversationLoop,
|
||||
} from './conversationLoop.js';
|
||||
|
||||
/** Shape of interactive UI text */
|
||||
interface InteractiveUIText {
|
||||
export interface InteractiveUIText {
|
||||
intro: string;
|
||||
resume: string;
|
||||
noConversation: string;
|
||||
@ -53,7 +47,7 @@ interface InteractiveUIText {
|
||||
/**
|
||||
* Format session state for display
|
||||
*/
|
||||
function formatSessionStatus(state: SessionState, lang: 'en' | 'ja'): string {
|
||||
export function formatSessionStatus(state: SessionState, lang: 'en' | 'ja'): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
// Status line
|
||||
@ -87,7 +81,7 @@ function formatSessionStatus(state: SessionState, lang: 'en' | 'ja'): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function resolveLanguage(lang?: Language): 'en' | 'ja' {
|
||||
export function resolveLanguage(lang?: Language): 'en' | 'ja' {
|
||||
return lang === 'ja' ? 'ja' : 'en';
|
||||
}
|
||||
|
||||
@ -122,37 +116,11 @@ export function formatMovementPreviews(previews: MovementPreview[], lang: 'en' |
|
||||
}).join('\n\n');
|
||||
}
|
||||
|
||||
function getInteractivePrompts(lang: 'en' | 'ja', pieceContext?: PieceContext) {
|
||||
const hasPreview = !!pieceContext?.movementPreviews?.length;
|
||||
const systemPrompt = loadTemplate('score_interactive_system_prompt', lang, {
|
||||
hasPiecePreview: hasPreview,
|
||||
pieceStructure: pieceContext?.pieceStructure ?? '',
|
||||
movementDetails: hasPreview ? formatMovementPreviews(pieceContext!.movementPreviews!, lang) : '',
|
||||
});
|
||||
const policyContent = loadTemplate('score_interactive_policy', lang, {});
|
||||
|
||||
return {
|
||||
systemPrompt,
|
||||
policyContent,
|
||||
lang,
|
||||
pieceContext,
|
||||
conversationLabel: getLabel('interactive.conversationLabel', lang),
|
||||
noTranscript: getLabel('interactive.noTranscript', lang),
|
||||
ui: getLabelObject<InteractiveUIText>('interactive.ui', lang),
|
||||
};
|
||||
}
|
||||
|
||||
interface ConversationMessage {
|
||||
export interface ConversationMessage {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface CallAIResult {
|
||||
content: string;
|
||||
sessionId?: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the final task description from conversation history for executeTask.
|
||||
*/
|
||||
@ -167,7 +135,7 @@ function buildTaskFromHistory(history: ConversationMessage[]): string {
|
||||
* Renders the complete score_summary_system_prompt template with conversation data.
|
||||
* Returns empty string if there is no conversation to summarize.
|
||||
*/
|
||||
function buildSummaryPrompt(
|
||||
export function buildSummaryPrompt(
|
||||
history: ConversationMessage[],
|
||||
hasSession: boolean,
|
||||
lang: 'en' | 'ja',
|
||||
@ -199,9 +167,9 @@ function buildSummaryPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
type PostSummaryAction = InteractiveModeAction | 'continue';
|
||||
export type PostSummaryAction = InteractiveModeAction | 'continue';
|
||||
|
||||
async function selectPostSummaryAction(
|
||||
export async function selectPostSummaryAction(
|
||||
task: string,
|
||||
proposedLabel: string,
|
||||
ui: InteractiveUIText,
|
||||
@ -218,34 +186,6 @@ async function selectPostSummaryAction(
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call AI with the same pattern as piece execution.
|
||||
* The key requirement is passing onStream — the Agent SDK requires
|
||||
* includePartialMessages to be true for the async iterator to yield.
|
||||
*/
|
||||
async function callAI(
|
||||
provider: ReturnType<typeof getProvider>,
|
||||
prompt: string,
|
||||
cwd: string,
|
||||
model: string | undefined,
|
||||
sessionId: string | undefined,
|
||||
display: StreamDisplay,
|
||||
systemPrompt: string,
|
||||
): Promise<CallAIResult> {
|
||||
const agent = provider.setup({ name: 'interactive', systemPrompt });
|
||||
const response = await agent.call(prompt, {
|
||||
cwd,
|
||||
model,
|
||||
sessionId,
|
||||
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
|
||||
onStream: display.createHandler(),
|
||||
});
|
||||
|
||||
display.flush();
|
||||
const success = response.status !== 'blocked';
|
||||
return { content: response.content, sessionId: response.sessionId, success };
|
||||
}
|
||||
|
||||
export type InteractiveModeAction = 'execute' | 'save_task' | 'create_issue' | 'cancel';
|
||||
|
||||
export interface InteractiveModeResult {
|
||||
@ -266,6 +206,8 @@ export interface PieceContext {
|
||||
movementPreviews?: MovementPreview[];
|
||||
}
|
||||
|
||||
export const DEFAULT_INTERACTIVE_TOOLS = ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
||||
|
||||
/**
|
||||
* Run the interactive task input mode.
|
||||
*
|
||||
@ -280,206 +222,37 @@ export async function interactiveMode(
|
||||
initialInput?: string,
|
||||
pieceContext?: PieceContext,
|
||||
): Promise<InteractiveModeResult> {
|
||||
const globalConfig = loadGlobalConfig();
|
||||
const lang = resolveLanguage(globalConfig.language);
|
||||
const prompts = getInteractivePrompts(lang, pieceContext);
|
||||
if (!globalConfig.provider) {
|
||||
throw new Error('Provider is not configured.');
|
||||
}
|
||||
const providerType = globalConfig.provider as ProviderType;
|
||||
const provider = getProvider(providerType);
|
||||
const model = (globalConfig.model as string | undefined);
|
||||
const ctx = initializeSession(cwd, 'interactive');
|
||||
|
||||
const history: ConversationMessage[] = [];
|
||||
const personaName = 'interactive';
|
||||
const savedSessions = loadPersonaSessions(cwd, providerType);
|
||||
let sessionId: string | undefined = savedSessions[personaName];
|
||||
displayAndClearSessionState(cwd, ctx.lang);
|
||||
|
||||
// Load and display previous task state
|
||||
const sessionState = loadSessionState(cwd);
|
||||
if (sessionState) {
|
||||
const statusLabel = formatSessionStatus(sessionState, lang);
|
||||
info(statusLabel);
|
||||
blankLine();
|
||||
clearSessionState(cwd);
|
||||
}
|
||||
|
||||
info(prompts.ui.intro);
|
||||
if (sessionId) {
|
||||
info(prompts.ui.resume);
|
||||
}
|
||||
blankLine();
|
||||
|
||||
/** Call AI with automatic retry on session error (stale/invalid session ID). */
|
||||
async function callAIWithRetry(prompt: string, systemPrompt: string): Promise<CallAIResult | null> {
|
||||
const display = new StreamDisplay('assistant', isQuietMode());
|
||||
try {
|
||||
const result = await callAI(
|
||||
provider,
|
||||
prompt,
|
||||
cwd,
|
||||
model,
|
||||
sessionId,
|
||||
display,
|
||||
systemPrompt,
|
||||
);
|
||||
// If session failed, clear it and retry without session
|
||||
if (!result.success && sessionId) {
|
||||
log.info('Session invalid, retrying without session');
|
||||
sessionId = undefined;
|
||||
const retryDisplay = new StreamDisplay('assistant', isQuietMode());
|
||||
const retry = await callAI(
|
||||
provider,
|
||||
prompt,
|
||||
cwd,
|
||||
model,
|
||||
undefined,
|
||||
retryDisplay,
|
||||
systemPrompt,
|
||||
);
|
||||
if (retry.sessionId) {
|
||||
sessionId = retry.sessionId;
|
||||
updatePersonaSession(cwd, personaName, sessionId, providerType);
|
||||
}
|
||||
return retry;
|
||||
}
|
||||
if (result.sessionId) {
|
||||
sessionId = result.sessionId;
|
||||
updatePersonaSession(cwd, personaName, sessionId, providerType);
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
const msg = getErrorMessage(e);
|
||||
log.error('AI call failed', { error: msg });
|
||||
error(msg);
|
||||
blankLine();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const hasPreview = !!pieceContext?.movementPreviews?.length;
|
||||
const systemPrompt = loadTemplate('score_interactive_system_prompt', ctx.lang, {
|
||||
hasPiecePreview: hasPreview,
|
||||
pieceStructure: pieceContext?.pieceStructure ?? '',
|
||||
movementDetails: hasPreview ? formatMovementPreviews(pieceContext!.movementPreviews!, ctx.lang) : '',
|
||||
});
|
||||
const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {});
|
||||
const ui = getLabelObject<InteractiveUIText>('interactive.ui', ctx.lang);
|
||||
|
||||
/**
|
||||
* Inject policy into user message for AI call.
|
||||
* Follows the same pattern as piece execution (perform_phase1_message.md).
|
||||
*/
|
||||
function injectPolicy(userMessage: string): string {
|
||||
const policyIntro = lang === 'ja'
|
||||
const policyIntro = ctx.lang === 'ja'
|
||||
? '以下のポリシーは行動規範です。必ず遵守してください。'
|
||||
: 'The following policy defines behavioral guidelines. Please follow them.';
|
||||
const reminderLabel = lang === 'ja'
|
||||
const reminderLabel = ctx.lang === 'ja'
|
||||
? '上記の Policy セクションで定義されたポリシー規範を遵守してください。'
|
||||
: 'Please follow the policy guidelines defined in the Policy section above.';
|
||||
return `## Policy\n${policyIntro}\n\n${prompts.policyContent}\n\n---\n\n${userMessage}\n\n---\n**Policy Reminder:** ${reminderLabel}`;
|
||||
return `## Policy\n${policyIntro}\n\n${policyContent}\n\n---\n\n${userMessage}\n\n---\n**Policy Reminder:** ${reminderLabel}`;
|
||||
}
|
||||
|
||||
// Process initial input if provided (e.g. from `takt a`)
|
||||
if (initialInput) {
|
||||
history.push({ role: 'user', content: initialInput });
|
||||
log.debug('Processing initial input', { initialInput, sessionId });
|
||||
|
||||
const promptWithPolicy = injectPolicy(initialInput);
|
||||
const result = await callAIWithRetry(promptWithPolicy, prompts.systemPrompt);
|
||||
if (result) {
|
||||
if (!result.success) {
|
||||
error(result.content);
|
||||
blankLine();
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
history.push({ role: 'assistant', content: result.content });
|
||||
blankLine();
|
||||
} else {
|
||||
history.pop();
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const input = await readMultilineInput(chalk.green('> '));
|
||||
|
||||
// EOF (Ctrl+D)
|
||||
if (input === null) {
|
||||
blankLine();
|
||||
info('Cancelled');
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
|
||||
// Empty input — skip
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle slash commands
|
||||
if (trimmed.startsWith('/play')) {
|
||||
const task = trimmed.slice(5).trim();
|
||||
if (!task) {
|
||||
info(prompts.ui.playNoTask);
|
||||
continue;
|
||||
}
|
||||
log.info('Play command', { task });
|
||||
return { action: 'execute', task };
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('/go')) {
|
||||
const userNote = trimmed.slice(3).trim();
|
||||
let summaryPrompt = buildSummaryPrompt(
|
||||
history,
|
||||
!!sessionId,
|
||||
prompts.lang,
|
||||
prompts.noTranscript,
|
||||
prompts.conversationLabel,
|
||||
prompts.pieceContext,
|
||||
);
|
||||
if (!summaryPrompt) {
|
||||
info(prompts.ui.noConversation);
|
||||
continue;
|
||||
}
|
||||
if (userNote) {
|
||||
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
|
||||
}
|
||||
const summaryResult = await callAIWithRetry(summaryPrompt, summaryPrompt);
|
||||
if (!summaryResult) {
|
||||
info(prompts.ui.summarizeFailed);
|
||||
continue;
|
||||
}
|
||||
if (!summaryResult.success) {
|
||||
error(summaryResult.content);
|
||||
blankLine();
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
const task = summaryResult.content.trim();
|
||||
const selectedAction = await selectPostSummaryAction(task, prompts.ui.proposed, prompts.ui);
|
||||
if (selectedAction === 'continue' || selectedAction === null) {
|
||||
info(prompts.ui.continuePrompt);
|
||||
continue;
|
||||
}
|
||||
log.info('Interactive mode action selected', { action: selectedAction, messageCount: history.length });
|
||||
return { action: selectedAction, task };
|
||||
}
|
||||
|
||||
if (trimmed === '/cancel') {
|
||||
info(prompts.ui.cancelled);
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
// Regular input — send to AI
|
||||
history.push({ role: 'user', content: trimmed });
|
||||
|
||||
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
||||
process.stdin.pause();
|
||||
|
||||
const promptWithPolicy = injectPolicy(trimmed);
|
||||
const result = await callAIWithRetry(promptWithPolicy, prompts.systemPrompt);
|
||||
if (result) {
|
||||
if (!result.success) {
|
||||
error(result.content);
|
||||
blankLine();
|
||||
history.pop();
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
history.push({ role: 'assistant', content: result.content });
|
||||
blankLine();
|
||||
} else {
|
||||
history.pop();
|
||||
}
|
||||
}
|
||||
return runConversationLoop(cwd, ctx, {
|
||||
systemPrompt,
|
||||
allowedTools: DEFAULT_INTERACTIVE_TOOLS,
|
||||
transformPrompt: injectPolicy,
|
||||
introMessage: ui.intro,
|
||||
}, pieceContext, initialInput);
|
||||
}
|
||||
|
||||
@ -250,46 +250,103 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
|
||||
|
||||
// --- Buffer position helpers ---
|
||||
|
||||
function getLineStart(): number {
|
||||
const lastNl = buffer.lastIndexOf('\n', cursorPos - 1);
|
||||
return lastNl + 1;
|
||||
}
|
||||
|
||||
function getLineEnd(): number {
|
||||
const nextNl = buffer.indexOf('\n', cursorPos);
|
||||
return nextNl >= 0 ? nextNl : buffer.length;
|
||||
}
|
||||
|
||||
function getLineStartAt(pos: number): number {
|
||||
const lastNl = buffer.lastIndexOf('\n', pos - 1);
|
||||
return lastNl + 1;
|
||||
}
|
||||
|
||||
function getLineStart(): number {
|
||||
return getLineStartAt(cursorPos);
|
||||
}
|
||||
|
||||
function getLineEndAt(pos: number): number {
|
||||
const nextNl = buffer.indexOf('\n', pos);
|
||||
return nextNl >= 0 ? nextNl : buffer.length;
|
||||
}
|
||||
|
||||
/** Display width from line start to cursor */
|
||||
function getDisplayColumn(): number {
|
||||
return getDisplayWidth(buffer.slice(getLineStart(), cursorPos));
|
||||
function getLineEnd(): number {
|
||||
return getLineEndAt(cursorPos);
|
||||
}
|
||||
|
||||
const promptWidth = getDisplayWidth(stripAnsi(prompt));
|
||||
|
||||
/** Terminal column (1-based) for a given buffer position */
|
||||
function getTerminalColumn(pos: number): number {
|
||||
const lineStart = getLineStartAt(pos);
|
||||
const col = getDisplayWidth(buffer.slice(lineStart, pos));
|
||||
const isFirstLine = lineStart === 0;
|
||||
return isFirstLine ? promptWidth + col + 1 : col + 1;
|
||||
// --- Display row helpers (soft-wrap awareness) ---
|
||||
|
||||
function getTermWidth(): number {
|
||||
return process.stdout.columns || 80;
|
||||
}
|
||||
|
||||
/** Find the buffer position in a line that matches a target display column */
|
||||
function findPositionByDisplayColumn(lineStart: number, lineEnd: number, targetDisplayCol: number): number {
|
||||
/** Buffer position of the display row start that contains `pos` */
|
||||
function getDisplayRowStart(pos: number): number {
|
||||
const logicalStart = getLineStartAt(pos);
|
||||
const termWidth = getTermWidth();
|
||||
const isFirstLogicalLine = logicalStart === 0;
|
||||
let firstRowWidth = isFirstLogicalLine ? termWidth - promptWidth : termWidth;
|
||||
if (firstRowWidth <= 0) firstRowWidth = 1;
|
||||
|
||||
let rowStart = logicalStart;
|
||||
let accumulated = 0;
|
||||
let available = firstRowWidth;
|
||||
let i = logicalStart;
|
||||
for (const ch of buffer.slice(logicalStart, pos)) {
|
||||
const w = getDisplayWidth(ch);
|
||||
if (accumulated + w > available) {
|
||||
rowStart = i;
|
||||
accumulated = w;
|
||||
available = termWidth;
|
||||
} else {
|
||||
accumulated += w;
|
||||
// Row exactly filled — next position starts a new display row
|
||||
if (accumulated === available) {
|
||||
rowStart = i + ch.length;
|
||||
accumulated = 0;
|
||||
available = termWidth;
|
||||
}
|
||||
}
|
||||
i += ch.length;
|
||||
}
|
||||
return rowStart;
|
||||
}
|
||||
|
||||
/** Buffer position of the display row end that contains `pos` */
|
||||
function getDisplayRowEnd(pos: number): number {
|
||||
const logicalEnd = getLineEndAt(pos);
|
||||
const rowStart = getDisplayRowStart(pos);
|
||||
const termWidth = getTermWidth();
|
||||
// The first display row of the first logical line has reduced width
|
||||
const isFirstDisplayRow = rowStart === 0;
|
||||
const available = isFirstDisplayRow ? termWidth - promptWidth : termWidth;
|
||||
|
||||
let accumulated = 0;
|
||||
let i = rowStart;
|
||||
for (const ch of buffer.slice(rowStart, logicalEnd)) {
|
||||
const w = getDisplayWidth(ch);
|
||||
if (accumulated + w > available) return i;
|
||||
accumulated += w;
|
||||
i += ch.length;
|
||||
}
|
||||
return logicalEnd;
|
||||
}
|
||||
|
||||
/** Display column (0-based) within the display row that contains `pos` */
|
||||
function getDisplayRowColumn(pos: number): number {
|
||||
return getDisplayWidth(buffer.slice(getDisplayRowStart(pos), pos));
|
||||
}
|
||||
|
||||
/** Terminal column (1-based) for a given buffer position */
|
||||
function getTerminalColumn(pos: number): number {
|
||||
const displayRowStart = getDisplayRowStart(pos);
|
||||
const col = getDisplayWidth(buffer.slice(displayRowStart, pos));
|
||||
// Only the first display row of the first logical line has the prompt offset
|
||||
const isFirstDisplayRow = displayRowStart === 0;
|
||||
return isFirstDisplayRow ? promptWidth + col + 1 : col + 1;
|
||||
}
|
||||
|
||||
/** Find the buffer position in a range that matches a target display column */
|
||||
function findPositionByDisplayColumn(rangeStart: number, rangeEnd: number, targetDisplayCol: number): number {
|
||||
let displayCol = 0;
|
||||
let pos = lineStart;
|
||||
for (const ch of buffer.slice(lineStart, lineEnd)) {
|
||||
let pos = rangeStart;
|
||||
for (const ch of buffer.slice(rangeStart, rangeEnd)) {
|
||||
const w = getDisplayWidth(ch);
|
||||
if (displayCol + w > targetDisplayCol) break;
|
||||
displayCol += w;
|
||||
@ -322,23 +379,77 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
|
||||
|
||||
// --- Cursor movement ---
|
||||
|
||||
function moveCursorToLineStart(): void {
|
||||
const displayOffset = getDisplayColumn();
|
||||
function moveCursorToDisplayRowStart(): void {
|
||||
const displayRowStart = getDisplayRowStart(cursorPos);
|
||||
const displayOffset = getDisplayRowColumn(cursorPos);
|
||||
if (displayOffset > 0) {
|
||||
cursorPos = getLineStart();
|
||||
cursorPos = displayRowStart;
|
||||
process.stdout.write(`\x1B[${displayOffset}D`);
|
||||
}
|
||||
}
|
||||
|
||||
function moveCursorToLineEnd(): void {
|
||||
const lineEnd = getLineEnd();
|
||||
const displayOffset = getDisplayWidth(buffer.slice(cursorPos, lineEnd));
|
||||
function moveCursorToDisplayRowEnd(): void {
|
||||
const displayRowEnd = getDisplayRowEnd(cursorPos);
|
||||
const displayOffset = getDisplayWidth(buffer.slice(cursorPos, displayRowEnd));
|
||||
if (displayOffset > 0) {
|
||||
cursorPos = lineEnd;
|
||||
cursorPos = displayRowEnd;
|
||||
process.stdout.write(`\x1B[${displayOffset}C`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Move cursor to a target display row, positioning at the given display column */
|
||||
function moveCursorToDisplayRow(
|
||||
targetRowStart: number,
|
||||
targetRowEnd: number,
|
||||
displayCol: number,
|
||||
direction: 'A' | 'B',
|
||||
): void {
|
||||
cursorPos = findPositionByDisplayColumn(targetRowStart, targetRowEnd, displayCol);
|
||||
const termCol = getTerminalColumn(cursorPos);
|
||||
process.stdout.write(`\x1B[${direction}`);
|
||||
process.stdout.write(`\x1B[${termCol}G`);
|
||||
}
|
||||
|
||||
/** Count how many display rows lie between two buffer positions in the same logical line */
|
||||
function countDisplayRowsBetween(from: number, to: number): number {
|
||||
if (from === to) return 0;
|
||||
const start = Math.min(from, to);
|
||||
const end = Math.max(from, to);
|
||||
let count = 0;
|
||||
let pos = start;
|
||||
while (pos < end) {
|
||||
const nextRowStart = getDisplayRowEnd(pos);
|
||||
if (nextRowStart >= end) break;
|
||||
pos = nextRowStart;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function moveCursorToLogicalLineStart(): void {
|
||||
const lineStart = getLineStart();
|
||||
if (cursorPos === lineStart) return;
|
||||
const rowDiff = countDisplayRowsBetween(lineStart, cursorPos);
|
||||
cursorPos = lineStart;
|
||||
if (rowDiff > 0) {
|
||||
process.stdout.write(`\x1B[${rowDiff}A`);
|
||||
}
|
||||
const termCol = getTerminalColumn(cursorPos);
|
||||
process.stdout.write(`\x1B[${termCol}G`);
|
||||
}
|
||||
|
||||
function moveCursorToLogicalLineEnd(): void {
|
||||
const lineEnd = getLineEnd();
|
||||
if (cursorPos === lineEnd) return;
|
||||
const rowDiff = countDisplayRowsBetween(cursorPos, lineEnd);
|
||||
cursorPos = lineEnd;
|
||||
if (rowDiff > 0) {
|
||||
process.stdout.write(`\x1B[${rowDiff}B`);
|
||||
}
|
||||
const termCol = getTerminalColumn(cursorPos);
|
||||
process.stdout.write(`\x1B[${termCol}G`);
|
||||
}
|
||||
|
||||
// --- Buffer editing ---
|
||||
|
||||
function insertAt(pos: number, text: string): void {
|
||||
@ -461,27 +572,40 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
|
||||
},
|
||||
onArrowUp() {
|
||||
if (state !== 'normal') return;
|
||||
const lineStart = getLineStart();
|
||||
if (lineStart === 0) return;
|
||||
const displayCol = getDisplayColumn();
|
||||
const prevLineStart = getLineStartAt(lineStart - 1);
|
||||
const prevLineEnd = lineStart - 1;
|
||||
cursorPos = findPositionByDisplayColumn(prevLineStart, prevLineEnd, displayCol);
|
||||
const termCol = getTerminalColumn(cursorPos);
|
||||
process.stdout.write('\x1B[A');
|
||||
process.stdout.write(`\x1B[${termCol}G`);
|
||||
const logicalLineStart = getLineStart();
|
||||
const displayRowStart = getDisplayRowStart(cursorPos);
|
||||
const displayCol = getDisplayRowColumn(cursorPos);
|
||||
|
||||
if (displayRowStart > logicalLineStart) {
|
||||
// Move to previous display row within the same logical line
|
||||
const prevRowStart = getDisplayRowStart(displayRowStart - 1);
|
||||
const prevRowEnd = getDisplayRowEnd(displayRowStart - 1);
|
||||
moveCursorToDisplayRow(prevRowStart, prevRowEnd, displayCol, 'A');
|
||||
} else if (logicalLineStart > 0) {
|
||||
// Move to the last display row of the previous logical line
|
||||
const prevLogicalLineEnd = logicalLineStart - 1;
|
||||
const prevRowStart = getDisplayRowStart(prevLogicalLineEnd);
|
||||
const prevRowEnd = getDisplayRowEnd(prevLogicalLineEnd);
|
||||
moveCursorToDisplayRow(prevRowStart, prevRowEnd, displayCol, 'A');
|
||||
}
|
||||
},
|
||||
onArrowDown() {
|
||||
if (state !== 'normal') return;
|
||||
const lineEnd = getLineEnd();
|
||||
if (lineEnd >= buffer.length) return;
|
||||
const displayCol = getDisplayColumn();
|
||||
const nextLineStart = lineEnd + 1;
|
||||
const nextLineEnd = getLineEndAt(nextLineStart);
|
||||
cursorPos = findPositionByDisplayColumn(nextLineStart, nextLineEnd, displayCol);
|
||||
const termCol = getTerminalColumn(cursorPos);
|
||||
process.stdout.write('\x1B[B');
|
||||
process.stdout.write(`\x1B[${termCol}G`);
|
||||
const logicalLineEnd = getLineEnd();
|
||||
const displayRowEnd = getDisplayRowEnd(cursorPos);
|
||||
const displayCol = getDisplayRowColumn(cursorPos);
|
||||
|
||||
if (displayRowEnd < logicalLineEnd) {
|
||||
// Move to next display row within the same logical line
|
||||
const nextRowStart = displayRowEnd;
|
||||
const nextRowEnd = getDisplayRowEnd(displayRowEnd);
|
||||
moveCursorToDisplayRow(nextRowStart, nextRowEnd, displayCol, 'B');
|
||||
} else if (logicalLineEnd < buffer.length) {
|
||||
// Move to the first display row of the next logical line
|
||||
const nextLineStart = logicalLineEnd + 1;
|
||||
const nextRowEnd = getDisplayRowEnd(nextLineStart);
|
||||
moveCursorToDisplayRow(nextLineStart, nextRowEnd, displayCol, 'B');
|
||||
}
|
||||
},
|
||||
onWordLeft() {
|
||||
if (state !== 'normal') return;
|
||||
@ -507,11 +631,11 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
|
||||
},
|
||||
onHome() {
|
||||
if (state !== 'normal') return;
|
||||
moveCursorToLineStart();
|
||||
moveCursorToLogicalLineStart();
|
||||
},
|
||||
onEnd() {
|
||||
if (state !== 'normal') return;
|
||||
moveCursorToLineEnd();
|
||||
moveCursorToLogicalLineEnd();
|
||||
},
|
||||
onChar(ch: string) {
|
||||
if (state === 'paste') {
|
||||
@ -543,8 +667,8 @@ export function readMultilineInput(prompt: string): Promise<string | null> {
|
||||
}
|
||||
// Editing
|
||||
if (ch === '\x7F' || ch === '\x08') { deleteCharBefore(); return; }
|
||||
if (ch === '\x01') { moveCursorToLineStart(); return; }
|
||||
if (ch === '\x05') { moveCursorToLineEnd(); return; }
|
||||
if (ch === '\x01') { moveCursorToDisplayRowStart(); return; }
|
||||
if (ch === '\x05') { moveCursorToDisplayRowEnd(); return; }
|
||||
if (ch === '\x0B') { deleteToLineEnd(); return; }
|
||||
if (ch === '\x15') { deleteToLineStart(); return; }
|
||||
if (ch === '\x17') { deleteWord(); return; }
|
||||
|
||||
35
src/features/interactive/modeSelection.ts
Normal file
35
src/features/interactive/modeSelection.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Interactive mode selection UI.
|
||||
*
|
||||
* Presents the four interactive mode options after piece selection
|
||||
* and returns the user's choice.
|
||||
*/
|
||||
|
||||
import type { InteractiveMode } from '../../core/models/index.js';
|
||||
import { DEFAULT_INTERACTIVE_MODE, INTERACTIVE_MODES } from '../../core/models/index.js';
|
||||
import { selectOptionWithDefault } from '../../shared/prompt/index.js';
|
||||
import { getLabel } from '../../shared/i18n/index.js';
|
||||
|
||||
/**
|
||||
* Prompt the user to select an interactive mode.
|
||||
*
|
||||
* @param lang - Display language
|
||||
* @param pieceDefault - Piece-level default mode (overrides user default)
|
||||
* @returns Selected mode, or null if cancelled
|
||||
*/
|
||||
export async function selectInteractiveMode(
|
||||
lang: 'en' | 'ja',
|
||||
pieceDefault?: InteractiveMode,
|
||||
): Promise<InteractiveMode | null> {
|
||||
const defaultMode = pieceDefault ?? DEFAULT_INTERACTIVE_MODE;
|
||||
|
||||
const options: { label: string; value: InteractiveMode; description: string }[] = INTERACTIVE_MODES.map((mode) => ({
|
||||
label: getLabel(`interactive.modeSelection.${mode}`, lang),
|
||||
value: mode,
|
||||
description: getLabel(`interactive.modeSelection.${mode}Description`, lang),
|
||||
}));
|
||||
|
||||
const prompt = getLabel('interactive.modeSelection.prompt', lang);
|
||||
|
||||
return selectOptionWithDefault<InteractiveMode>(prompt, options, defaultMode);
|
||||
}
|
||||
50
src/features/interactive/passthroughMode.ts
Normal file
50
src/features/interactive/passthroughMode.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Passthrough interactive mode.
|
||||
*
|
||||
* Passes user input directly as the task string without any
|
||||
* AI-assisted instruction generation or system prompt injection.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { info, blankLine } from '../../shared/ui/index.js';
|
||||
import { getLabel } from '../../shared/i18n/index.js';
|
||||
import { readMultilineInput } from './lineEditor.js';
|
||||
import type { InteractiveModeResult } from './interactive.js';
|
||||
|
||||
/**
|
||||
* Run passthrough mode: collect user input and return it as-is.
|
||||
*
|
||||
* If initialInput is provided, it is used directly as the task.
|
||||
* Otherwise, prompts the user for input.
|
||||
*
|
||||
* @param lang - Display language
|
||||
* @param initialInput - Pre-filled input (e.g., from issue reference)
|
||||
* @returns Result with the raw user input as task
|
||||
*/
|
||||
export async function passthroughMode(
|
||||
lang: 'en' | 'ja',
|
||||
initialInput?: string,
|
||||
): Promise<InteractiveModeResult> {
|
||||
if (initialInput) {
|
||||
return { action: 'execute', task: initialInput };
|
||||
}
|
||||
|
||||
info(getLabel('interactive.ui.intro', lang));
|
||||
blankLine();
|
||||
|
||||
const input = await readMultilineInput(chalk.green('> '));
|
||||
|
||||
if (input === null) {
|
||||
blankLine();
|
||||
info(getLabel('interactive.ui.cancelled', lang));
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
info(getLabel('interactive.ui.cancelled', lang));
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
return { action: 'execute', task: trimmed };
|
||||
}
|
||||
58
src/features/interactive/personaMode.ts
Normal file
58
src/features/interactive/personaMode.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Persona interactive mode.
|
||||
*
|
||||
* Uses the first movement's persona and tools for the interactive
|
||||
* conversation. The persona acts as the conversational agent,
|
||||
* performing code exploration and analysis while discussing the task.
|
||||
* The conversation result is passed as the task to the piece.
|
||||
*/
|
||||
|
||||
import type { FirstMovementInfo } from '../../infra/config/index.js';
|
||||
import { getLabel } from '../../shared/i18n/index.js';
|
||||
import {
|
||||
type PieceContext,
|
||||
type InteractiveModeResult,
|
||||
DEFAULT_INTERACTIVE_TOOLS,
|
||||
} from './interactive.js';
|
||||
import {
|
||||
initializeSession,
|
||||
displayAndClearSessionState,
|
||||
runConversationLoop,
|
||||
} from './conversationLoop.js';
|
||||
|
||||
/**
|
||||
* Run persona mode: converse as the first movement's persona.
|
||||
*
|
||||
* The persona's system prompt is used for all AI calls.
|
||||
* The first movement's allowed tools are made available.
|
||||
* After the conversation, the result is summarized as a task.
|
||||
*
|
||||
* @param cwd - Working directory
|
||||
* @param firstMovement - First movement's persona and tool info
|
||||
* @param initialInput - Pre-filled input
|
||||
* @param pieceContext - Piece context for summary generation
|
||||
* @returns Result with conversation-derived task
|
||||
*/
|
||||
export async function personaMode(
|
||||
cwd: string,
|
||||
firstMovement: FirstMovementInfo,
|
||||
initialInput?: string,
|
||||
pieceContext?: PieceContext,
|
||||
): Promise<InteractiveModeResult> {
|
||||
const ctx = initializeSession(cwd, 'persona-interactive');
|
||||
|
||||
displayAndClearSessionState(cwd, ctx.lang);
|
||||
|
||||
const allowedTools = firstMovement.allowedTools.length > 0
|
||||
? firstMovement.allowedTools
|
||||
: DEFAULT_INTERACTIVE_TOOLS;
|
||||
|
||||
const introMessage = `${getLabel('interactive.ui.intro', ctx.lang)} [${firstMovement.personaDisplayName}]`;
|
||||
|
||||
return runConversationLoop(cwd, ctx, {
|
||||
systemPrompt: firstMovement.personaContent,
|
||||
allowedTools,
|
||||
transformPrompt: (msg) => msg,
|
||||
introMessage,
|
||||
}, pieceContext, initialInput);
|
||||
}
|
||||
111
src/features/interactive/quietMode.ts
Normal file
111
src/features/interactive/quietMode.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Quiet interactive mode.
|
||||
*
|
||||
* Generates task instructions without asking clarifying questions.
|
||||
* Uses the same summarization logic as assistant mode but skips
|
||||
* the conversational loop — goes directly to summary generation.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { createLogger } from '../../shared/utils/index.js';
|
||||
import { info, error, blankLine } from '../../shared/ui/index.js';
|
||||
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
|
||||
import { readMultilineInput } from './lineEditor.js';
|
||||
import {
|
||||
type PieceContext,
|
||||
type InteractiveModeResult,
|
||||
type InteractiveUIText,
|
||||
type ConversationMessage,
|
||||
DEFAULT_INTERACTIVE_TOOLS,
|
||||
buildSummaryPrompt,
|
||||
selectPostSummaryAction,
|
||||
} from './interactive.js';
|
||||
import {
|
||||
initializeSession,
|
||||
callAIWithRetry,
|
||||
} from './conversationLoop.js';
|
||||
|
||||
const log = createLogger('quiet-mode');
|
||||
|
||||
/**
|
||||
* Run quiet mode: collect user input and generate instructions without questions.
|
||||
*
|
||||
* Flow:
|
||||
* 1. If initialInput is provided, use it; otherwise prompt for input
|
||||
* 2. Build summary prompt from the user input
|
||||
* 3. Call AI to generate task instructions (best-effort, no questions)
|
||||
* 4. Present the result and let user choose action
|
||||
*
|
||||
* @param cwd - Working directory
|
||||
* @param initialInput - Pre-filled input (e.g., from issue reference)
|
||||
* @param pieceContext - Piece context for template rendering
|
||||
* @returns Result with generated task instructions
|
||||
*/
|
||||
export async function quietMode(
|
||||
cwd: string,
|
||||
initialInput?: string,
|
||||
pieceContext?: PieceContext,
|
||||
): Promise<InteractiveModeResult> {
|
||||
const ctx = initializeSession(cwd, 'interactive');
|
||||
|
||||
let userInput = initialInput;
|
||||
|
||||
if (!userInput) {
|
||||
info(getLabel('interactive.ui.intro', ctx.lang));
|
||||
blankLine();
|
||||
|
||||
const input = await readMultilineInput(chalk.green('> '));
|
||||
if (input === null) {
|
||||
blankLine();
|
||||
info(getLabel('interactive.ui.cancelled', ctx.lang));
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
info(getLabel('interactive.ui.cancelled', ctx.lang));
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
userInput = trimmed;
|
||||
}
|
||||
|
||||
const history: ConversationMessage[] = [
|
||||
{ role: 'user', content: userInput },
|
||||
];
|
||||
|
||||
const conversationLabel = getLabel('interactive.conversationLabel', ctx.lang);
|
||||
const noTranscript = getLabel('interactive.noTranscript', ctx.lang);
|
||||
|
||||
const summaryPrompt = buildSummaryPrompt(
|
||||
history, !!ctx.sessionId, ctx.lang, noTranscript, conversationLabel, pieceContext,
|
||||
);
|
||||
|
||||
if (!summaryPrompt) {
|
||||
info(getLabel('interactive.ui.noConversation', ctx.lang));
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
const { result } = await callAIWithRetry(
|
||||
summaryPrompt, summaryPrompt, DEFAULT_INTERACTIVE_TOOLS, cwd, ctx,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
error(result.content);
|
||||
blankLine();
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
const task = result.content.trim();
|
||||
const ui = getLabelObject<InteractiveUIText>('interactive.ui', ctx.lang);
|
||||
|
||||
const selectedAction = await selectPostSummaryAction(task, ui.proposed, ui);
|
||||
if (selectedAction === 'continue' || selectedAction === null) {
|
||||
return { action: 'cancel', task: '' };
|
||||
}
|
||||
|
||||
log.info('Quiet mode action selected', { action: selectedAction });
|
||||
return { action: selectedAction, task };
|
||||
}
|
||||
@ -5,19 +5,75 @@
|
||||
* available task as soon as it finishes the current one, maximizing slot
|
||||
* utilization. Works for both sequential (concurrency=1) and parallel
|
||||
* (concurrency>1) execution through the same code path.
|
||||
*
|
||||
* Polls for newly added tasks at a configurable interval so that tasks
|
||||
* added to .takt/tasks/ during execution are picked up without waiting
|
||||
* for an active task to complete.
|
||||
*/
|
||||
|
||||
import type { TaskRunner, TaskInfo } from '../../../infra/task/index.js';
|
||||
import { info, blankLine } from '../../../shared/ui/index.js';
|
||||
import { TaskPrefixWriter } from '../../../shared/ui/TaskPrefixWriter.js';
|
||||
import { createLogger } from '../../../shared/utils/index.js';
|
||||
import { executeAndCompleteTask } from './taskExecution.js';
|
||||
import { installSigIntHandler } from './sigintHandler.js';
|
||||
import type { TaskExecutionOptions } from './types.js';
|
||||
|
||||
const log = createLogger('worker-pool');
|
||||
|
||||
export interface WorkerPoolResult {
|
||||
success: number;
|
||||
fail: number;
|
||||
}
|
||||
|
||||
type RaceResult =
|
||||
| { type: 'completion'; promise: Promise<boolean>; result: boolean }
|
||||
| { type: 'poll' };
|
||||
|
||||
interface PollTimer {
|
||||
promise: Promise<RaceResult>;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
function createPollTimer(intervalMs: number, signal: AbortSignal): PollTimer {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
let onAbort: (() => void) | undefined;
|
||||
|
||||
const promise = new Promise<RaceResult>((resolve) => {
|
||||
if (signal.aborted) {
|
||||
resolve({ type: 'poll' });
|
||||
return;
|
||||
}
|
||||
|
||||
onAbort = () => {
|
||||
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
||||
resolve({ type: 'poll' });
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
signal.removeEventListener('abort', onAbort!);
|
||||
onAbort = undefined;
|
||||
resolve({ type: 'poll' });
|
||||
}, intervalMs);
|
||||
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
cancel: () => {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = undefined;
|
||||
}
|
||||
if (onAbort) {
|
||||
signal.removeEventListener('abort', onAbort);
|
||||
onAbort = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run tasks using a worker pool with the given concurrency.
|
||||
*
|
||||
@ -25,9 +81,10 @@ export interface WorkerPoolResult {
|
||||
* 1. Create a shared AbortController
|
||||
* 2. Maintain a queue of pending tasks and a set of active promises
|
||||
* 3. Fill available slots from the queue
|
||||
* 4. Wait for any active task to complete (Promise.race)
|
||||
* 5. Record result, fill freed slot from queue
|
||||
* 6. Repeat until queue is empty and all active tasks complete
|
||||
* 4. Wait for any active task to complete OR a poll timer to fire (Promise.race)
|
||||
* 5. On task completion: record result
|
||||
* 6. On poll tick or completion: claim new tasks and fill freed slots
|
||||
* 7. Repeat until queue is empty and all active tasks complete
|
||||
*/
|
||||
export async function runWithWorkerPool(
|
||||
taskRunner: TaskRunner,
|
||||
@ -35,7 +92,8 @@ export async function runWithWorkerPool(
|
||||
concurrency: number,
|
||||
cwd: string,
|
||||
pieceName: string,
|
||||
options?: TaskExecutionOptions,
|
||||
options: TaskExecutionOptions | undefined,
|
||||
pollIntervalMs: number,
|
||||
): Promise<WorkerPoolResult> {
|
||||
const abortController = new AbortController();
|
||||
const { cleanup } = installSigIntHandler(() => abortController.abort());
|
||||
@ -45,6 +103,7 @@ export async function runWithWorkerPool(
|
||||
|
||||
const queue = [...initialTasks];
|
||||
const active = new Map<Promise<boolean>, TaskInfo>();
|
||||
const colorCounter = { value: 0 };
|
||||
|
||||
try {
|
||||
while (queue.length > 0 || active.size > 0) {
|
||||
@ -52,33 +111,50 @@ export async function runWithWorkerPool(
|
||||
break;
|
||||
}
|
||||
|
||||
fillSlots(queue, active, concurrency, taskRunner, cwd, pieceName, options, abortController);
|
||||
fillSlots(queue, active, concurrency, taskRunner, cwd, pieceName, options, abortController, colorCounter);
|
||||
|
||||
if (active.size === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const settled = await Promise.race(
|
||||
[...active.keys()].map((p) => p.then(
|
||||
(result) => ({ promise: p, result }),
|
||||
() => ({ promise: p, result: false }),
|
||||
)),
|
||||
const pollTimer = createPollTimer(pollIntervalMs, abortController.signal);
|
||||
|
||||
const completionPromises: Promise<RaceResult>[] = [...active.keys()].map((p) =>
|
||||
p.then(
|
||||
(result): RaceResult => ({ type: 'completion', promise: p, result }),
|
||||
(): RaceResult => ({ type: 'completion', promise: p, result: false }),
|
||||
),
|
||||
);
|
||||
|
||||
const task = active.get(settled.promise);
|
||||
active.delete(settled.promise);
|
||||
const settled = await Promise.race([...completionPromises, pollTimer.promise]);
|
||||
|
||||
if (task) {
|
||||
if (settled.result) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
pollTimer.cancel();
|
||||
|
||||
if (settled.type === 'completion') {
|
||||
const task = active.get(settled.promise);
|
||||
active.delete(settled.promise);
|
||||
|
||||
if (task) {
|
||||
if (settled.result) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!abortController.signal.aborted && queue.length === 0) {
|
||||
const nextTasks = taskRunner.claimNextTasks(concurrency - active.size);
|
||||
queue.push(...nextTasks);
|
||||
if (!abortController.signal.aborted) {
|
||||
const freeSlots = concurrency - active.size;
|
||||
if (freeSlots > 0) {
|
||||
const newTasks = taskRunner.claimNextTasks(freeSlots);
|
||||
log.debug('poll_tick', { active: active.size, queued: queue.length, freeSlots });
|
||||
if (newTasks.length > 0) {
|
||||
log.debug('poll_new_tasks', { count: newTasks.length });
|
||||
queue.push(...newTasks);
|
||||
} else {
|
||||
log.debug('no_new_tasks');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@ -97,17 +173,25 @@ function fillSlots(
|
||||
pieceName: string,
|
||||
options: TaskExecutionOptions | undefined,
|
||||
abortController: AbortController,
|
||||
colorCounter: { value: number },
|
||||
): void {
|
||||
while (active.size < concurrency && queue.length > 0) {
|
||||
const task = queue.shift()!;
|
||||
const isParallel = concurrency > 1;
|
||||
const colorIndex = colorCounter.value++;
|
||||
|
||||
blankLine();
|
||||
info(`=== Task: ${task.name} ===`);
|
||||
if (isParallel) {
|
||||
const writer = new TaskPrefixWriter({ taskName: task.name, colorIndex });
|
||||
writer.writeLine(`=== Task: ${task.name} ===`);
|
||||
} else {
|
||||
blankLine();
|
||||
info(`=== Task: ${task.name} ===`);
|
||||
}
|
||||
|
||||
const promise = executeAndCompleteTask(task, taskRunner, cwd, pieceName, options, {
|
||||
abortSignal: isParallel ? abortController.signal : undefined,
|
||||
taskPrefix: isParallel ? task.name : undefined,
|
||||
taskColorIndex: isParallel ? colorIndex : undefined,
|
||||
});
|
||||
active.set(promise, task);
|
||||
}
|
||||
|
||||
@ -21,15 +21,16 @@ import {
|
||||
} from '../../../infra/config/index.js';
|
||||
import { isQuietMode } from '../../../shared/context.js';
|
||||
import {
|
||||
header,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
success,
|
||||
status,
|
||||
blankLine,
|
||||
header as rawHeader,
|
||||
info as rawInfo,
|
||||
warn as rawWarn,
|
||||
error as rawError,
|
||||
success as rawSuccess,
|
||||
status as rawStatus,
|
||||
blankLine as rawBlankLine,
|
||||
StreamDisplay,
|
||||
} from '../../../shared/ui/index.js';
|
||||
import { TaskPrefixWriter } from '../../../shared/ui/TaskPrefixWriter.js';
|
||||
import {
|
||||
generateSessionId,
|
||||
createSessionLog,
|
||||
@ -62,6 +63,91 @@ import { installSigIntHandler } from './sigintHandler.js';
|
||||
|
||||
const log = createLogger('piece');
|
||||
|
||||
/**
|
||||
* Output facade — routes through TaskPrefixWriter when task prefix is active,
|
||||
* or falls through to the raw module functions for single-task execution.
|
||||
*/
|
||||
interface OutputFns {
|
||||
header: (title: string) => void;
|
||||
info: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
success: (message: string) => void;
|
||||
status: (label: string, value: string, color?: 'green' | 'yellow' | 'red') => void;
|
||||
blankLine: () => void;
|
||||
logLine: (text: string) => void;
|
||||
}
|
||||
|
||||
function assertTaskPrefixPair(
|
||||
taskPrefix: string | undefined,
|
||||
taskColorIndex: number | undefined
|
||||
): void {
|
||||
const hasTaskPrefix = taskPrefix != null;
|
||||
const hasTaskColorIndex = taskColorIndex != null;
|
||||
if (hasTaskPrefix !== hasTaskColorIndex) {
|
||||
throw new Error('taskPrefix and taskColorIndex must be provided together');
|
||||
}
|
||||
}
|
||||
|
||||
function createOutputFns(prefixWriter: TaskPrefixWriter | undefined): OutputFns {
|
||||
if (!prefixWriter) {
|
||||
return {
|
||||
header: rawHeader,
|
||||
info: rawInfo,
|
||||
warn: rawWarn,
|
||||
error: rawError,
|
||||
success: rawSuccess,
|
||||
status: rawStatus,
|
||||
blankLine: rawBlankLine,
|
||||
logLine: (text: string) => console.log(text),
|
||||
};
|
||||
}
|
||||
return {
|
||||
header: (title: string) => prefixWriter.writeLine(`=== ${title} ===`),
|
||||
info: (message: string) => prefixWriter.writeLine(`[INFO] ${message}`),
|
||||
warn: (message: string) => prefixWriter.writeLine(`[WARN] ${message}`),
|
||||
error: (message: string) => prefixWriter.writeLine(`[ERROR] ${message}`),
|
||||
success: (message: string) => prefixWriter.writeLine(message),
|
||||
status: (label: string, value: string) => prefixWriter.writeLine(`${label}: ${value}`),
|
||||
blankLine: () => prefixWriter.writeLine(''),
|
||||
logLine: (text: string) => prefixWriter.writeLine(text),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a stream handler that routes all stream events through TaskPrefixWriter.
|
||||
* Text and tool_output are line-buffered; block events are output per-line with prefix.
|
||||
*/
|
||||
function createPrefixedStreamHandler(
|
||||
writer: TaskPrefixWriter,
|
||||
): (event: Parameters<ReturnType<StreamDisplay['createHandler']>>[0]) => void {
|
||||
return (event) => {
|
||||
switch (event.type) {
|
||||
case 'text':
|
||||
writer.writeChunk(event.data.text);
|
||||
break;
|
||||
case 'tool_use':
|
||||
writer.writeLine(`[tool] ${event.data.tool}`);
|
||||
break;
|
||||
case 'tool_result': {
|
||||
const label = event.data.isError ? '✗' : '✓';
|
||||
writer.writeLine(` ${label} ${event.data.content}`);
|
||||
break;
|
||||
}
|
||||
case 'tool_output':
|
||||
writer.writeChunk(event.data.output);
|
||||
break;
|
||||
case 'thinking':
|
||||
writer.writeChunk(event.data.thinking);
|
||||
break;
|
||||
case 'init':
|
||||
case 'result':
|
||||
case 'error':
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate string to maximum length
|
||||
*/
|
||||
@ -106,11 +192,18 @@ export async function executePiece(
|
||||
|
||||
// projectCwd is where .takt/ lives (project root, not the clone)
|
||||
const projectCwd = options.projectCwd;
|
||||
assertTaskPrefixPair(options.taskPrefix, options.taskColorIndex);
|
||||
|
||||
// When taskPrefix is set (parallel execution), route all output through TaskPrefixWriter
|
||||
const prefixWriter = options.taskPrefix != null
|
||||
? new TaskPrefixWriter({ taskName: options.taskPrefix, colorIndex: options.taskColorIndex! })
|
||||
: undefined;
|
||||
const out = createOutputFns(prefixWriter);
|
||||
|
||||
// Always continue from previous sessions (use /clear to reset)
|
||||
log.debug('Continuing session (use /clear to reset)');
|
||||
|
||||
header(`${headerPrefix} ${pieceConfig.name}`);
|
||||
out.header(`${headerPrefix} ${pieceConfig.name}`);
|
||||
|
||||
const pieceSessionId = generateSessionId();
|
||||
let sessionLog = createSessionLog(task, projectCwd, pieceConfig.name);
|
||||
@ -139,14 +232,16 @@ export async function executePiece(
|
||||
// Track current display for streaming
|
||||
const displayRef: { current: StreamDisplay | null } = { current: null };
|
||||
|
||||
// Create stream handler that delegates to UI display
|
||||
const streamHandler = (
|
||||
event: Parameters<ReturnType<StreamDisplay['createHandler']>>[0]
|
||||
): void => {
|
||||
if (!displayRef.current) return;
|
||||
if (event.type === 'result') return;
|
||||
displayRef.current.createHandler()(event);
|
||||
};
|
||||
// Create stream handler — when prefixWriter is active, use it for line-buffered
|
||||
// output to prevent mid-line interleaving between concurrent tasks.
|
||||
// When not in parallel mode, delegate to StreamDisplay as before.
|
||||
const streamHandler = prefixWriter
|
||||
? createPrefixedStreamHandler(prefixWriter)
|
||||
: (event: Parameters<ReturnType<StreamDisplay['createHandler']>>[0]): void => {
|
||||
if (!displayRef.current) return;
|
||||
if (event.type === 'result') return;
|
||||
displayRef.current.createHandler()(event);
|
||||
};
|
||||
|
||||
// Load saved agent sessions for continuity (from project root or clone-specific storage)
|
||||
const isWorktree = cwd !== projectCwd;
|
||||
@ -180,14 +275,14 @@ export async function executePiece(
|
||||
displayRef.current = null;
|
||||
}
|
||||
|
||||
blankLine();
|
||||
warn(
|
||||
out.blankLine();
|
||||
out.warn(
|
||||
getLabel('piece.iterationLimit.maxReached', undefined, {
|
||||
currentIteration: String(request.currentIteration),
|
||||
maxIterations: String(request.maxIterations),
|
||||
})
|
||||
);
|
||||
info(getLabel('piece.iterationLimit.currentMovement', undefined, { currentMovement: request.currentMovement }));
|
||||
out.info(getLabel('piece.iterationLimit.currentMovement', undefined, { currentMovement: request.currentMovement }));
|
||||
|
||||
if (shouldNotify) {
|
||||
playWarningSound();
|
||||
@ -214,11 +309,11 @@ export async function executePiece(
|
||||
|
||||
const additionalIterations = Number.parseInt(input, 10);
|
||||
if (Number.isInteger(additionalIterations) && additionalIterations > 0) {
|
||||
pieceConfig.maxIterations += additionalIterations;
|
||||
pieceConfig.maxIterations = request.maxIterations + additionalIterations;
|
||||
return additionalIterations;
|
||||
}
|
||||
|
||||
warn(getLabel('piece.iterationLimit.invalidInput'));
|
||||
out.warn(getLabel('piece.iterationLimit.invalidInput'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -228,14 +323,15 @@ export async function executePiece(
|
||||
displayRef.current.flush();
|
||||
displayRef.current = null;
|
||||
}
|
||||
blankLine();
|
||||
info(request.prompt.trim());
|
||||
out.blankLine();
|
||||
out.info(request.prompt.trim());
|
||||
const input = await promptInput(getLabel('piece.iterationLimit.userInputPrompt'));
|
||||
return input && input.trim() ? input.trim() : null;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const engine = new PieceEngine(pieceConfig, cwd, task, {
|
||||
abortSignal: options.abortSignal,
|
||||
onStream: streamHandler,
|
||||
onUserInput,
|
||||
initialSessions: savedSessions,
|
||||
@ -245,11 +341,14 @@ export async function executePiece(
|
||||
language: options.language,
|
||||
provider: options.provider,
|
||||
model: options.model,
|
||||
personaProviders: options.personaProviders,
|
||||
interactive: interactiveUserInput,
|
||||
detectRuleIndex,
|
||||
callAiJudge,
|
||||
startMovement: options.startMovement,
|
||||
retryNote: options.retryNote,
|
||||
taskPrefix: options.taskPrefix,
|
||||
taskColorIndex: options.taskColorIndex,
|
||||
});
|
||||
|
||||
let abortReason: string | undefined;
|
||||
@ -257,6 +356,7 @@ export async function executePiece(
|
||||
let lastMovementName: string | undefined;
|
||||
let currentIteration = 0;
|
||||
const phasePrompts = new Map<string, string>();
|
||||
const movementIterations = new Map<string, number>();
|
||||
|
||||
engine.on('phase:start', (step, phase, phaseName, instruction) => {
|
||||
log.debug('Phase starting', { step: step.name, phase, phaseName });
|
||||
@ -311,7 +411,15 @@ export async function executePiece(
|
||||
engine.on('movement:start', (step, iteration, instruction) => {
|
||||
log.debug('Movement starting', { step: step.name, persona: step.personaDisplayName, iteration });
|
||||
currentIteration = iteration;
|
||||
info(`[${iteration}/${pieceConfig.maxIterations}] ${step.name} (${step.personaDisplayName})`);
|
||||
const movementIteration = (movementIterations.get(step.name) ?? 0) + 1;
|
||||
movementIterations.set(step.name, movementIteration);
|
||||
prefixWriter?.setMovementContext({
|
||||
movementName: step.name,
|
||||
iteration,
|
||||
maxIterations: pieceConfig.maxIterations,
|
||||
movementIteration,
|
||||
});
|
||||
out.info(`[${iteration}/${pieceConfig.maxIterations}] ${step.name} (${step.personaDisplayName})`);
|
||||
|
||||
// Log prompt content for debugging
|
||||
if (instruction) {
|
||||
@ -322,15 +430,18 @@ export async function executePiece(
|
||||
const movementIndex = pieceConfig.movements.findIndex((m) => m.name === step.name);
|
||||
const totalMovements = pieceConfig.movements.length;
|
||||
|
||||
const quiet = isQuietMode();
|
||||
const prefix = options.taskPrefix;
|
||||
const agentLabel = prefix ? `${prefix}:${step.personaDisplayName}` : step.personaDisplayName;
|
||||
displayRef.current = new StreamDisplay(agentLabel, quiet, {
|
||||
iteration,
|
||||
maxIterations: pieceConfig.maxIterations,
|
||||
movementIndex: movementIndex >= 0 ? movementIndex : 0,
|
||||
totalMovements,
|
||||
});
|
||||
// In parallel mode, StreamDisplay is not used (prefixWriter handles output).
|
||||
// In single mode, StreamDisplay renders stream events directly.
|
||||
if (!prefixWriter) {
|
||||
const quiet = isQuietMode();
|
||||
const agentLabel = step.personaDisplayName;
|
||||
displayRef.current = new StreamDisplay(agentLabel, quiet, {
|
||||
iteration,
|
||||
maxIterations: pieceConfig.maxIterations,
|
||||
movementIndex: movementIndex >= 0 ? movementIndex : 0,
|
||||
totalMovements,
|
||||
});
|
||||
}
|
||||
|
||||
// Write step_start record to NDJSON log
|
||||
const record: NdjsonStepStart = {
|
||||
@ -364,25 +475,26 @@ export async function executePiece(
|
||||
displayRef.current.flush();
|
||||
displayRef.current = null;
|
||||
}
|
||||
blankLine();
|
||||
prefixWriter?.flush();
|
||||
out.blankLine();
|
||||
|
||||
if (response.matchedRuleIndex != null && step.rules) {
|
||||
const rule = step.rules[response.matchedRuleIndex];
|
||||
if (rule) {
|
||||
const methodLabel = response.matchedRuleMethod ? ` (${response.matchedRuleMethod})` : '';
|
||||
status('Status', `${rule.condition}${methodLabel}`);
|
||||
out.status('Status', `${rule.condition}${methodLabel}`);
|
||||
} else {
|
||||
status('Status', response.status);
|
||||
out.status('Status', response.status);
|
||||
}
|
||||
} else {
|
||||
status('Status', response.status);
|
||||
out.status('Status', response.status);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
error(`Error: ${response.error}`);
|
||||
out.error(`Error: ${response.error}`);
|
||||
}
|
||||
if (response.sessionId) {
|
||||
status('Session', response.sessionId);
|
||||
out.status('Session', response.sessionId);
|
||||
}
|
||||
|
||||
// Write step_complete record to NDJSON log
|
||||
@ -408,8 +520,8 @@ export async function executePiece(
|
||||
|
||||
engine.on('movement:report', (_step, filePath, fileName) => {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
console.log(`\n📄 Report: ${fileName}\n`);
|
||||
console.log(content);
|
||||
out.logLine(`\n📄 Report: ${fileName}\n`);
|
||||
out.logLine(content);
|
||||
});
|
||||
|
||||
engine.on('piece:complete', (state) => {
|
||||
@ -445,8 +557,8 @@ export async function executePiece(
|
||||
: '';
|
||||
const elapsedDisplay = elapsed ? `, ${elapsed}` : '';
|
||||
|
||||
success(`Piece completed (${state.iteration} iterations${elapsedDisplay})`);
|
||||
info(`Session log: ${ndjsonLogPath}`);
|
||||
out.success(`Piece completed (${state.iteration} iterations${elapsedDisplay})`);
|
||||
out.info(`Session log: ${ndjsonLogPath}`);
|
||||
if (shouldNotify) {
|
||||
notifySuccess('TAKT', getLabel('piece.notifyComplete', undefined, { iteration: String(state.iteration) }));
|
||||
}
|
||||
@ -459,6 +571,7 @@ export async function executePiece(
|
||||
displayRef.current.flush();
|
||||
displayRef.current = null;
|
||||
}
|
||||
prefixWriter?.flush();
|
||||
abortReason = reason;
|
||||
sessionLog = finalizeSessionLog(sessionLog, 'aborted');
|
||||
|
||||
@ -492,8 +605,8 @@ export async function executePiece(
|
||||
: '';
|
||||
const elapsedDisplay = elapsed ? ` (${elapsed})` : '';
|
||||
|
||||
error(`Piece aborted after ${state.iteration} iterations${elapsedDisplay}: ${reason}`);
|
||||
info(`Session log: ${ndjsonLogPath}`);
|
||||
out.error(`Piece aborted after ${state.iteration} iterations${elapsedDisplay}: ${reason}`);
|
||||
out.info(`Session log: ${ndjsonLogPath}`);
|
||||
if (shouldNotify) {
|
||||
notifyError('TAKT', getLabel('piece.notifyAbort', undefined, { reason }));
|
||||
}
|
||||
@ -536,6 +649,7 @@ export async function executePiece(
|
||||
reason: abortReason,
|
||||
};
|
||||
} finally {
|
||||
prefixWriter?.flush();
|
||||
sigintCleanup?.();
|
||||
if (onAbortSignal && options.abortSignal) {
|
||||
options.abortSignal.removeEventListener('abort', onAbortSignal);
|
||||
|
||||
@ -52,7 +52,7 @@ function resolveTaskIssue(issueNumber: number | undefined): ReturnType<typeof fe
|
||||
* Execute a single task with piece.
|
||||
*/
|
||||
export async function executeTask(options: ExecuteTaskOptions): Promise<boolean> {
|
||||
const { task, cwd, pieceIdentifier, projectCwd, agentOverrides, interactiveUserInput, interactiveMetadata, startMovement, retryNote, abortSignal, taskPrefix } = options;
|
||||
const { task, cwd, pieceIdentifier, projectCwd, agentOverrides, interactiveUserInput, interactiveMetadata, startMovement, retryNote, abortSignal, taskPrefix, taskColorIndex } = options;
|
||||
const pieceConfig = loadPieceByIdentifier(pieceIdentifier, projectCwd);
|
||||
|
||||
if (!pieceConfig) {
|
||||
@ -77,12 +77,14 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<boolean>
|
||||
language: globalConfig.language,
|
||||
provider: agentOverrides?.provider,
|
||||
model: agentOverrides?.model,
|
||||
personaProviders: globalConfig.personaProviders,
|
||||
interactiveUserInput,
|
||||
interactiveMetadata,
|
||||
startMovement,
|
||||
retryNote,
|
||||
abortSignal,
|
||||
taskPrefix,
|
||||
taskColorIndex,
|
||||
});
|
||||
return result.success;
|
||||
}
|
||||
@ -101,16 +103,31 @@ export async function executeAndCompleteTask(
|
||||
cwd: string,
|
||||
pieceName: string,
|
||||
options?: TaskExecutionOptions,
|
||||
parallelOptions?: { abortSignal?: AbortSignal; taskPrefix?: string },
|
||||
parallelOptions?: { abortSignal?: AbortSignal; taskPrefix?: string; taskColorIndex?: number },
|
||||
): Promise<boolean> {
|
||||
const startedAt = new Date().toISOString();
|
||||
const executionLog: string[] = [];
|
||||
const taskAbortController = new AbortController();
|
||||
const externalAbortSignal = parallelOptions?.abortSignal;
|
||||
const taskAbortSignal = externalAbortSignal ? taskAbortController.signal : undefined;
|
||||
|
||||
const onExternalAbort = (): void => {
|
||||
taskAbortController.abort();
|
||||
};
|
||||
|
||||
if (externalAbortSignal) {
|
||||
if (externalAbortSignal.aborted) {
|
||||
taskAbortController.abort();
|
||||
} else {
|
||||
externalAbortSignal.addEventListener('abort', onExternalAbort, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { execCwd, execPiece, isWorktree, branch, baseBranch, startMovement, retryNote, autoPr, issueNumber } = await resolveTaskExecution(task, cwd, pieceName);
|
||||
|
||||
// cwd is always the project root; pass it as projectCwd so reports/sessions go there
|
||||
const taskSuccess = await executeTask({
|
||||
const taskRunPromise = executeTask({
|
||||
task: task.content,
|
||||
cwd: execCwd,
|
||||
pieceIdentifier: execPiece,
|
||||
@ -118,9 +135,12 @@ export async function executeAndCompleteTask(
|
||||
agentOverrides: options,
|
||||
startMovement,
|
||||
retryNote,
|
||||
abortSignal: parallelOptions?.abortSignal,
|
||||
abortSignal: taskAbortSignal,
|
||||
taskPrefix: parallelOptions?.taskPrefix,
|
||||
taskColorIndex: parallelOptions?.taskColorIndex,
|
||||
});
|
||||
|
||||
const taskSuccess = await taskRunPromise;
|
||||
const completedAt = new Date().toISOString();
|
||||
|
||||
if (taskSuccess && isWorktree) {
|
||||
@ -189,6 +209,10 @@ export async function executeAndCompleteTask(
|
||||
|
||||
error(`Task "${task.name}" error: ${getErrorMessage(err)}`);
|
||||
return false;
|
||||
} finally {
|
||||
if (externalAbortSignal) {
|
||||
externalAbortSignal.removeEventListener('abort', onExternalAbort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +244,7 @@ export async function runAllTasks(
|
||||
info(`Concurrency: ${concurrency}`);
|
||||
}
|
||||
|
||||
const result = await runWithWorkerPool(taskRunner, initialTasks, concurrency, cwd, pieceName, options);
|
||||
const result = await runWithWorkerPool(taskRunner, initialTasks, concurrency, cwd, pieceName, options, globalConfig.taskPollIntervalMs);
|
||||
|
||||
const totalCount = result.success + result.fail;
|
||||
blankLine();
|
||||
|
||||
@ -30,6 +30,8 @@ export interface PieceExecutionOptions {
|
||||
language?: Language;
|
||||
provider?: ProviderType;
|
||||
model?: string;
|
||||
/** Per-persona provider overrides (e.g., { coder: 'codex' }) */
|
||||
personaProviders?: Record<string, ProviderType>;
|
||||
/** Enable interactive user input during step transitions */
|
||||
interactiveUserInput?: boolean;
|
||||
/** Interactive mode result metadata for NDJSON logging */
|
||||
@ -42,6 +44,8 @@ export interface PieceExecutionOptions {
|
||||
abortSignal?: AbortSignal;
|
||||
/** Task name prefix for parallel execution output (e.g. "[task-name] output...") */
|
||||
taskPrefix?: string;
|
||||
/** Color index for task prefix (cycled mod 4 across concurrent tasks) */
|
||||
taskColorIndex?: number;
|
||||
}
|
||||
|
||||
export interface TaskExecutionOptions {
|
||||
@ -72,6 +76,8 @@ export interface ExecuteTaskOptions {
|
||||
abortSignal?: AbortSignal;
|
||||
/** Task name prefix for parallel execution output (e.g. "[task-name] output...") */
|
||||
taskPrefix?: string;
|
||||
/** Color index for task prefix (cycled mod 4 across concurrent tasks) */
|
||||
taskColorIndex?: number;
|
||||
}
|
||||
|
||||
export interface PipelineExecutionOptions {
|
||||
|
||||
@ -23,6 +23,8 @@ import {
|
||||
export type { CodexCallOptions } from './types.js';
|
||||
|
||||
const log = createLogger('codex-sdk');
|
||||
const CODEX_STREAM_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const CODEX_STREAM_ABORTED_MESSAGE = 'Codex execution aborted';
|
||||
|
||||
/**
|
||||
* Client for Codex SDK agent interactions.
|
||||
@ -55,6 +57,34 @@ export class CodexClient {
|
||||
? `${options.systemPrompt}\n\n${prompt}`
|
||||
: prompt;
|
||||
|
||||
let idleTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
const streamAbortController = new AbortController();
|
||||
const timeoutMessage = `Codex stream timed out after ${Math.floor(CODEX_STREAM_IDLE_TIMEOUT_MS / 60000)} minutes of inactivity`;
|
||||
let abortCause: 'timeout' | 'external' | undefined;
|
||||
|
||||
const resetIdleTimeout = (): void => {
|
||||
if (idleTimeoutId !== undefined) {
|
||||
clearTimeout(idleTimeoutId);
|
||||
}
|
||||
idleTimeoutId = setTimeout(() => {
|
||||
abortCause = 'timeout';
|
||||
streamAbortController.abort();
|
||||
}, CODEX_STREAM_IDLE_TIMEOUT_MS);
|
||||
};
|
||||
|
||||
const onExternalAbort = (): void => {
|
||||
abortCause = 'external';
|
||||
streamAbortController.abort();
|
||||
};
|
||||
|
||||
if (options.abortSignal) {
|
||||
if (options.abortSignal.aborted) {
|
||||
streamAbortController.abort();
|
||||
} else {
|
||||
options.abortSignal.addEventListener('abort', onExternalAbort, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
log.debug('Executing Codex thread', {
|
||||
agentType,
|
||||
@ -62,7 +92,10 @@ export class CodexClient {
|
||||
hasSystemPrompt: !!options.systemPrompt,
|
||||
});
|
||||
|
||||
const { events } = await thread.runStreamed(fullPrompt);
|
||||
const { events } = await thread.runStreamed(fullPrompt, {
|
||||
signal: streamAbortController.signal,
|
||||
});
|
||||
resetIdleTimeout();
|
||||
let content = '';
|
||||
const contentOffsets = new Map<string, number>();
|
||||
let success = true;
|
||||
@ -70,6 +103,7 @@ export class CodexClient {
|
||||
const state = createStreamTrackingState();
|
||||
|
||||
for await (const event of events as AsyncGenerator<CodexEvent>) {
|
||||
resetIdleTimeout();
|
||||
if (event.type === 'thread.started') {
|
||||
threadId = typeof event.thread_id === 'string' ? event.thread_id : threadId;
|
||||
emitInit(options.onStream, options.model, threadId);
|
||||
@ -172,15 +206,27 @@ export class CodexClient {
|
||||
};
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(error);
|
||||
emitResult(options.onStream, false, message, threadId);
|
||||
const errorMessage = streamAbortController.signal.aborted
|
||||
? abortCause === 'timeout'
|
||||
? timeoutMessage
|
||||
: CODEX_STREAM_ABORTED_MESSAGE
|
||||
: message;
|
||||
emitResult(options.onStream, false, errorMessage, threadId);
|
||||
|
||||
return {
|
||||
persona: agentType,
|
||||
status: 'blocked',
|
||||
content: message,
|
||||
content: errorMessage,
|
||||
timestamp: new Date(),
|
||||
sessionId: threadId,
|
||||
};
|
||||
} finally {
|
||||
if (idleTimeoutId !== undefined) {
|
||||
clearTimeout(idleTimeoutId);
|
||||
}
|
||||
if (options.abortSignal) {
|
||||
options.abortSignal.removeEventListener('abort', onExternalAbort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ export function mapToCodexSandboxMode(mode: PermissionMode): CodexSandboxMode {
|
||||
/** Options for calling Codex */
|
||||
export interface CodexCallOptions {
|
||||
cwd: string;
|
||||
abortSignal?: AbortSignal;
|
||||
sessionId?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
|
||||
@ -37,6 +37,7 @@ function createDefaultGlobalConfig(): GlobalConfig {
|
||||
enableBuiltinPieces: true,
|
||||
interactivePreviewMovements: 3,
|
||||
concurrency: 1,
|
||||
taskPollIntervalMs: 500,
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,11 +106,13 @@ export class GlobalConfigManager {
|
||||
minimalOutput: parsed.minimal_output,
|
||||
bookmarksFile: parsed.bookmarks_file,
|
||||
pieceCategoriesFile: parsed.piece_categories_file,
|
||||
personaProviders: parsed.persona_providers,
|
||||
branchNameStrategy: parsed.branch_name_strategy,
|
||||
preventSleep: parsed.prevent_sleep,
|
||||
notificationSound: parsed.notification_sound,
|
||||
interactivePreviewMovements: parsed.interactive_preview_movements,
|
||||
concurrency: parsed.concurrency,
|
||||
taskPollIntervalMs: parsed.task_poll_interval_ms,
|
||||
};
|
||||
validateProviderModelCompatibility(config.provider, config.model);
|
||||
this.cachedConfig = config;
|
||||
@ -170,6 +173,9 @@ export class GlobalConfigManager {
|
||||
if (config.pieceCategoriesFile) {
|
||||
raw.piece_categories_file = config.pieceCategoriesFile;
|
||||
}
|
||||
if (config.personaProviders && Object.keys(config.personaProviders).length > 0) {
|
||||
raw.persona_providers = config.personaProviders;
|
||||
}
|
||||
if (config.branchNameStrategy) {
|
||||
raw.branch_name_strategy = config.branchNameStrategy;
|
||||
}
|
||||
@ -185,6 +191,9 @@ export class GlobalConfigManager {
|
||||
if (config.concurrency !== undefined && config.concurrency > 1) {
|
||||
raw.concurrency = config.concurrency;
|
||||
}
|
||||
if (config.taskPollIntervalMs !== undefined && config.taskPollIntervalMs !== 500) {
|
||||
raw.task_poll_interval_ms = config.taskPollIntervalMs;
|
||||
}
|
||||
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ export {
|
||||
listPieces,
|
||||
listPieceEntries,
|
||||
type MovementPreview,
|
||||
type FirstMovementInfo,
|
||||
type PieceDirEntry,
|
||||
type PieceSource,
|
||||
type PieceWithSource,
|
||||
|
||||
@ -21,6 +21,7 @@ export {
|
||||
listPieces,
|
||||
listPieceEntries,
|
||||
type MovementPreview,
|
||||
type FirstMovementInfo,
|
||||
type PieceDirEntry,
|
||||
type PieceSource,
|
||||
type PieceWithSource,
|
||||
|
||||
@ -280,6 +280,7 @@ export function normalizePieceConfig(
|
||||
maxIterations: parsed.max_iterations,
|
||||
loopMonitors: normalizeLoopMonitors(parsed.loop_monitors, pieceDir, sections, context),
|
||||
answerAgent: parsed.answer_agent,
|
||||
interactiveMode: parsed.interactive_mode,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { join, resolve, isAbsolute } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type { PieceConfig, PieceMovement } from '../../../core/models/index.js';
|
||||
import type { PieceConfig, PieceMovement, InteractiveMode } from '../../../core/models/index.js';
|
||||
import { getGlobalPiecesDir, getBuiltinPiecesDir, getProjectConfigDir } from '../paths.js';
|
||||
import { getLanguage, getDisabledBuiltins, getBuiltinPiecesEnabled } from '../global/globalConfig.js';
|
||||
import { createLogger, getErrorMessage } from '../../../shared/utils/index.js';
|
||||
@ -219,24 +219,10 @@ function buildMovementPreviews(piece: PieceConfig, maxCount: number): MovementPr
|
||||
const movement = movementMap.get(currentName);
|
||||
if (!movement) break;
|
||||
|
||||
let personaContent = '';
|
||||
if (movement.personaPath) {
|
||||
try {
|
||||
personaContent = readFileSync(movement.personaPath, 'utf-8');
|
||||
} catch (err) {
|
||||
log.debug('Failed to read persona file for preview', {
|
||||
path: movement.personaPath,
|
||||
error: getErrorMessage(err),
|
||||
});
|
||||
}
|
||||
} else if (movement.persona) {
|
||||
personaContent = movement.persona;
|
||||
}
|
||||
|
||||
previews.push({
|
||||
name: movement.name,
|
||||
personaDisplayName: movement.personaDisplayName,
|
||||
personaContent,
|
||||
personaContent: readMovementPersona(movement),
|
||||
instructionContent: movement.instructionTemplate,
|
||||
allowedTools: movement.allowedTools ?? [],
|
||||
canEdit: movement.edit === true,
|
||||
@ -250,26 +236,86 @@ function buildMovementPreviews(piece: PieceConfig, maxCount: number): MovementPr
|
||||
return previews;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read persona content from a movement.
|
||||
* When personaPath is set, reads from file (returns empty on failure).
|
||||
* Otherwise uses inline persona string.
|
||||
*/
|
||||
function readMovementPersona(movement: PieceMovement): string {
|
||||
if (movement.personaPath) {
|
||||
try {
|
||||
return readFileSync(movement.personaPath, 'utf-8');
|
||||
} catch (err) {
|
||||
log.debug('Failed to read persona file', {
|
||||
path: movement.personaPath,
|
||||
error: getErrorMessage(err),
|
||||
});
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return movement.persona ?? '';
|
||||
}
|
||||
|
||||
/** First movement info for persona mode */
|
||||
export interface FirstMovementInfo {
|
||||
/** Persona prompt content */
|
||||
personaContent: string;
|
||||
/** Persona display name */
|
||||
personaDisplayName: string;
|
||||
/** Allowed tools for this movement */
|
||||
allowedTools: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get piece description by identifier.
|
||||
* Returns the piece name, description, workflow structure, and optional movement previews.
|
||||
* Returns the piece name, description, workflow structure, optional movement previews,
|
||||
* piece-level interactive mode default, and first movement info for persona mode.
|
||||
*/
|
||||
export function getPieceDescription(
|
||||
identifier: string,
|
||||
projectCwd: string,
|
||||
previewCount?: number,
|
||||
): { name: string; description: string; pieceStructure: string; movementPreviews: MovementPreview[] } {
|
||||
): {
|
||||
name: string;
|
||||
description: string;
|
||||
pieceStructure: string;
|
||||
movementPreviews: MovementPreview[];
|
||||
interactiveMode?: InteractiveMode;
|
||||
firstMovement?: FirstMovementInfo;
|
||||
} {
|
||||
const piece = loadPieceByIdentifier(identifier, projectCwd);
|
||||
if (!piece) {
|
||||
return { name: identifier, description: '', pieceStructure: '', movementPreviews: [] };
|
||||
}
|
||||
|
||||
const previews = previewCount && previewCount > 0
|
||||
? buildMovementPreviews(piece, previewCount)
|
||||
: [];
|
||||
|
||||
const firstMovement = buildFirstMovementInfo(piece);
|
||||
|
||||
return {
|
||||
name: piece.name,
|
||||
description: piece.description ?? '',
|
||||
pieceStructure: buildWorkflowString(piece.movements),
|
||||
movementPreviews: previewCount && previewCount > 0
|
||||
? buildMovementPreviews(piece, previewCount)
|
||||
: [],
|
||||
movementPreviews: previews,
|
||||
interactiveMode: piece.interactiveMode,
|
||||
firstMovement,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build first movement info for persona mode.
|
||||
* Reads persona content from the initial movement.
|
||||
*/
|
||||
function buildFirstMovementInfo(piece: PieceConfig): FirstMovementInfo | undefined {
|
||||
const movement = piece.movements.find((m) => m.name === piece.initialMovement);
|
||||
if (!movement) return undefined;
|
||||
|
||||
return {
|
||||
personaContent: readMovementPersona(movement),
|
||||
personaDisplayName: movement.personaDisplayName,
|
||||
allowedTools: movement.allowedTools ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ function isInsideGitRepo(cwd: string): boolean {
|
||||
function toCodexOptions(options: ProviderCallOptions): CodexCallOptions {
|
||||
return {
|
||||
cwd: options.cwd,
|
||||
abortSignal: options.abortSignal,
|
||||
sessionId: options.sessionId,
|
||||
model: options.model,
|
||||
permissionMode: options.permissionMode,
|
||||
|
||||
@ -20,6 +20,7 @@ export interface AgentSetup {
|
||||
/** Runtime options passed at call time */
|
||||
export interface ProviderCallOptions {
|
||||
cwd: string;
|
||||
abortSignal?: AbortSignal;
|
||||
sessionId?: string;
|
||||
model?: string;
|
||||
allowedTools?: string[];
|
||||
|
||||
@ -24,6 +24,17 @@ interactive:
|
||||
continue: "Continue editing"
|
||||
cancelled: "Cancelled"
|
||||
playNoTask: "Please specify task content: /play <task>"
|
||||
personaFallback: "No persona available for the first movement. Falling back to assistant mode."
|
||||
modeSelection:
|
||||
prompt: "Select interactive mode:"
|
||||
assistant: "Assistant"
|
||||
assistantDescription: "Ask clarifying questions before generating instructions"
|
||||
persona: "Persona"
|
||||
personaDescription: "Converse as the first agent's persona"
|
||||
quiet: "Quiet"
|
||||
quietDescription: "Generate instructions without asking questions"
|
||||
passthrough: "Passthrough"
|
||||
passthroughDescription: "Pass your input directly as task text"
|
||||
previousTask:
|
||||
success: "✅ Previous task completed successfully"
|
||||
error: "❌ Previous task failed: {error}"
|
||||
|
||||
@ -24,6 +24,17 @@ interactive:
|
||||
continue: "会話を続ける"
|
||||
cancelled: "キャンセルしました"
|
||||
playNoTask: "タスク内容を指定してください: /play <タスク内容>"
|
||||
personaFallback: "先頭ムーブメントにペルソナがありません。アシスタントモードにフォールバックします。"
|
||||
modeSelection:
|
||||
prompt: "対話モードを選択してください:"
|
||||
assistant: "アシスタント"
|
||||
assistantDescription: "確認質問をしてから指示書を作成"
|
||||
persona: "ペルソナ"
|
||||
personaDescription: "先頭エージェントのペルソナで対話"
|
||||
quiet: "クワイエット"
|
||||
quietDescription: "質問なしでベストエフォートの指示書を生成"
|
||||
passthrough: "パススルー"
|
||||
passthroughDescription: "入力をそのままタスクとして渡す"
|
||||
previousTask:
|
||||
success: "✅ 前回のタスクは正常に完了しました"
|
||||
error: "❌ 前回のタスクはエラーで終了しました: {error}"
|
||||
|
||||
118
src/shared/ui/TaskPrefixWriter.ts
Normal file
118
src/shared/ui/TaskPrefixWriter.ts
Normal file
@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Line-buffered, prefixed writer for task-level parallel execution.
|
||||
*
|
||||
* When multiple tasks run concurrently (takt run --concurrency N), each task's
|
||||
* output must be identifiable and line-aligned to prevent mid-line interleaving.
|
||||
* This class wraps process.stdout.write with line buffering and a colored
|
||||
* `[taskName]` prefix on every non-empty line.
|
||||
*
|
||||
* Design mirrors ParallelLogger (movement-level) but targets task-level output:
|
||||
* - Regular log lines (info, header, status) get the prefix
|
||||
* - Stream output gets line-buffered then prefixed
|
||||
* - Empty lines are passed through without prefix
|
||||
*/
|
||||
|
||||
import { stripAnsi } from '../utils/text.js';
|
||||
|
||||
/** ANSI color codes for task prefixes (cycled by task index) */
|
||||
const TASK_COLORS = ['\x1b[36m', '\x1b[33m', '\x1b[35m', '\x1b[32m'] as const;
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
export interface TaskPrefixWriterOptions {
|
||||
/** Task name used in the prefix */
|
||||
taskName: string;
|
||||
/** Color index for the prefix (cycled mod 4) */
|
||||
colorIndex: number;
|
||||
/** Override process.stdout.write for testing */
|
||||
writeFn?: (text: string) => void;
|
||||
}
|
||||
|
||||
export interface MovementPrefixContext {
|
||||
movementName: string;
|
||||
iteration: number;
|
||||
maxIterations: number;
|
||||
movementIteration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixed line writer for a single parallel task.
|
||||
*
|
||||
* All output goes through `writeLine` (complete lines) or `writeChunk`
|
||||
* (buffered partial lines). The prefix `[taskName]` is prepended to every
|
||||
* non-empty output line.
|
||||
*/
|
||||
export class TaskPrefixWriter {
|
||||
private readonly taskPrefix: string;
|
||||
private readonly writeFn: (text: string) => void;
|
||||
private movementContext: MovementPrefixContext | undefined;
|
||||
private lineBuffer = '';
|
||||
|
||||
constructor(options: TaskPrefixWriterOptions) {
|
||||
const color = TASK_COLORS[options.colorIndex % TASK_COLORS.length];
|
||||
const taskLabel = options.taskName.slice(0, 4);
|
||||
this.taskPrefix = `${color}[${taskLabel}]${RESET}`;
|
||||
this.writeFn = options.writeFn ?? ((text: string) => process.stdout.write(text));
|
||||
}
|
||||
|
||||
setMovementContext(context: MovementPrefixContext): void {
|
||||
this.movementContext = context;
|
||||
}
|
||||
|
||||
private buildPrefix(): string {
|
||||
if (!this.movementContext) {
|
||||
return `${this.taskPrefix} `;
|
||||
}
|
||||
|
||||
const { movementName, iteration, maxIterations, movementIteration } = this.movementContext;
|
||||
return `${this.taskPrefix}[${movementName}](${iteration}/${maxIterations})(${movementIteration}) `;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a complete line with prefix.
|
||||
* Multi-line text is split and each non-empty line gets the prefix.
|
||||
*/
|
||||
writeLine(text: string): void {
|
||||
const cleaned = stripAnsi(text);
|
||||
const lines = cleaned.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line === '') {
|
||||
this.writeFn('\n');
|
||||
} else {
|
||||
this.writeFn(`${this.buildPrefix()}${line}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a chunk of streaming text with line buffering.
|
||||
* Partial lines are buffered until a newline arrives, then output with prefix.
|
||||
*/
|
||||
writeChunk(text: string): void {
|
||||
const cleaned = stripAnsi(text);
|
||||
const combined = this.lineBuffer + cleaned;
|
||||
const parts = combined.split('\n');
|
||||
|
||||
const remainder = parts.pop() ?? '';
|
||||
this.lineBuffer = remainder;
|
||||
|
||||
for (const line of parts) {
|
||||
if (line === '') {
|
||||
this.writeFn('\n');
|
||||
} else {
|
||||
this.writeFn(`${this.buildPrefix()}${line}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any remaining buffered content.
|
||||
*/
|
||||
flush(): void {
|
||||
if (this.lineBuffer !== '') {
|
||||
this.writeFn(`${this.buildPrefix()}${this.lineBuffer}\n`);
|
||||
this.lineBuffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user